From 0820299aae42c5b299df4b763d7122d305b43386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20Desgagn=C3=A9?= Date: Thu, 14 May 2026 21:06:00 -0400 Subject: [PATCH 1/2] Stop rewriting ${GITHUB_*} env vars to ${{github.*}} in workflows --- lib/ghb/workflow/workflow.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ghb/workflow/workflow.rb b/lib/ghb/workflow/workflow.rb index 5801133..d3c6577 100644 --- a/lib/ghb/workflow/workflow.rb +++ b/lib/ghb/workflow/workflow.rb @@ -116,11 +116,11 @@ def write(file, header: '') FileUtils.mkdir_p(File.dirname(file)) content = header + to_h.deep_stringify_keys.to_yaml({ line_width: -1 }) - # Convert old-style ${GITHUB_*} patterns to new-style ${{github.*}} - content.gsub!(/\$\{GITHUB_([A-Z_]+)\}/) do |_match| - var_name = ::Regexp.last_match(1).downcase - "${{github.#{var_name}}}" - end + # NOTE: do NOT rewrite ${GITHUB_*} -> ${{github.*}}. Shell `run:` blocks + # legitimately use the ${GITHUB_*} env-var form (set by the runner), and + # ${{github.*}} inside a shell expression is opaque to shellcheck and + # trips SC2193. Authors writing if:/env:/with: should use ${{github.*}} + # directly; we won't auto-translate one form into the other. # Convert secrets.GITHUB_TOKEN to secrets.GH_PAT for higher rate limits content.gsub!('${{secrets.GITHUB_TOKEN}}', '${{secrets.GH_PAT}}') unless file.include?('auto-merge') From 68ed826408f20a54a95d09ef68629ffbab73c0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20Desgagn=C3=A9?= Date: Thu, 14 May 2026 21:17:18 -0400 Subject: [PATCH 2/2] Scope ${GITHUB_*} -> ${{github.*}} rewrite to non-run: contexts Previously the workflow writer applied a blanket regex substitution across the rendered YAML, which: - Translated ${GITHUB_*} in shell `run:` bodies, where the env-var form is the runner-exported syntax shellcheck expects (the ${{github.*}} form is opaque to shellcheck and trips SC2193), and - Was still needed for env:/if:/with:/concurrency:/run-name: values, where ${GITHUB_*} is *not* shell-expanded and the canonical expression form is required. Walk the parsed YAML tree before serialization and only rewrite values whose enclosing key is not `run`. Shell bodies stay verbatim; every other context still gets the canonical ${{github.*}} form. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/ghb/workflow/workflow.rb | 32 +++++++++++++++++++++++------- spec/ghb/workflow/workflow_spec.rb | 15 ++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/ghb/workflow/workflow.rb b/lib/ghb/workflow/workflow.rb index d3c6577..6d69dd7 100644 --- a/lib/ghb/workflow/workflow.rb +++ b/lib/ghb/workflow/workflow.rb @@ -8,6 +8,9 @@ module GHB class Workflow + GITHUB_ENV_VAR_REGEX = /\$\{GITHUB_([A-Z_]+)\}/ + private_constant :GITHUB_ENV_VAR_REGEX + def initialize(name) @name = name @run_name = nil @@ -114,13 +117,8 @@ def read(file) def write(file, header: '') FileUtils.mkdir_p(File.dirname(file)) - content = header + to_h.deep_stringify_keys.to_yaml({ line_width: -1 }) - - # NOTE: do NOT rewrite ${GITHUB_*} -> ${{github.*}}. Shell `run:` blocks - # legitimately use the ${GITHUB_*} env-var form (set by the runner), and - # ${{github.*}} inside a shell expression is opaque to shellcheck and - # trips SC2193. Authors writing if:/env:/with: should use ${{github.*}} - # directly; we won't auto-translate one form into the other. + data = rewrite_github_refs(to_h.deep_stringify_keys) + content = header + data.to_yaml({ line_width: -1 }) # Convert secrets.GITHUB_TOKEN to secrets.GH_PAT for higher rate limits content.gsub!('${{secrets.GITHUB_TOKEN}}', '${{secrets.GH_PAT}}') unless file.include?('auto-merge') @@ -140,5 +138,25 @@ def to_h hash[:jobs] = @jobs.transform_values(&:to_h) hash end + + private + + # Rewrite ${GITHUB_*} -> ${{github.*}} in YAML values, but skip shell `run:` + # bodies - there ${GITHUB_*} is the runner-exported env-var form and + # ${{github.*}} is opaque to shellcheck (SC2193). + def rewrite_github_refs(node) + case node + when Hash + node.each_with_object({}) do |(key, value), acc| + acc[key] = key.to_s == 'run' ? value : rewrite_github_refs(value) + end + when Array + node.map { |item| rewrite_github_refs(item) } + when String + node.gsub(GITHUB_ENV_VAR_REGEX) { "${{github.#{::Regexp.last_match(1).downcase}}}" } + else + node + end + end end end diff --git a/spec/ghb/workflow/workflow_spec.rb b/spec/ghb/workflow/workflow_spec.rb index 61aa805..d9b786f 100644 --- a/spec/ghb/workflow/workflow_spec.rb +++ b/spec/ghb/workflow/workflow_spec.rb @@ -295,6 +295,21 @@ end end + it 'preserves ${GITHUB_*} inside shell run: blocks' do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations + workflow.do_job(:build) do + do_runs_on('ubuntu-latest') + do_step('echo sha') do + do_run('echo "$GITHUB_SHA ${GITHUB_REF}"') + end + end + workflow.write(temp_file) + + expect(File).to(have_received(:write)) do |_, content| + expect(content).to(include('${GITHUB_REF}')) + expect(content).not_to(include('${{github.ref}}')) + end + end + it 'converts secrets.GITHUB_TOKEN to secrets.GH_PAT' do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations workflow.do_env({ TOKEN: '${{secrets.GITHUB_TOKEN}}' }) workflow.write(temp_file)