Skip to content

fix: gh aw upgrade wraps SHA-pinned uses: value in quotes, including inline comment#19679

Merged
pelikhan merged 6 commits intomainfrom
copilot/fix-gh-aw-upgrade-comments
Mar 5, 2026
Merged

fix: gh aw upgrade wraps SHA-pinned uses: value in quotes, including inline comment#19679
pelikhan merged 6 commits intomainfrom
copilot/fix-gh-aw-upgrade-comments

Conversation

Copy link
Contributor

Copilot AI commented Mar 5, 2026

  • Replace yaml.Marshal/Unmarshal upgrade path with raw regex search-and-replace
  • Add upgradeSetupCliVersionInContent (regex-based, preserves file formatting)
  • Move setupCliUsesPattern to package-level variable (pre-compiled)
  • Remove upgradeSetupCliVersion, unquoteUsesValues, usesQuotePattern
  • Replace old struct-based tests with TestUpgradeSetupCliVersionInContent
  • Rewrite TestUpgradeCopilotSetupSteps_SHAPinnedNoQuotes to verify content preservation
  • Add TestUpgradeSetupCliVersionInContent_ExactPreservation — exact byte-level comparison verifying YAML comments, blank lines, field ordering, indentation, and unrelated steps are all unchanged
  • Revert actions/setup-cli/install.sh to its original pre-PR state
Original prompt

This section details on the original issue you should resolve

<issue_title>Bug: gh aw upgrade wraps uses value in quotes, including the inline comment</issue_title>
<issue_description>## Bug: gh aw upgrade wraps uses value in quotes, including the inline comment

Repository: github/gh-aw
Command: gh aw upgrade
Version: v0.53.2

Summary

After running gh aw upgrade, the copilot-setup-steps.yml file (and potentially other generated workflow files) has the uses: field for the setup-cli action wrapped in double quotes that include the inline # vX.Y.Z comment. Because the value is quoted, YAML treats the comment as part of the string rather than as a comment, causing GitHub Actions to attempt to resolve the full string — including the # v0.53.2 suffix — as a git ref.

Steps to Reproduce

  1. Have an existing copilot-setup-steps.yml in .github/workflows/.
  2. Run gh aw upgrade.
  3. Observe the generated uses: field for the setup-cli step.

Actual Output

- name: Install gh-aw extension
  uses: "github/gh-aw/actions/setup-cli@bd9c0ca491e6334a2797ef56ad6ee89958d54ab9 # v0.53.2"
  with:
    version: v0.53.2

Expected Output

- name: Install gh-aw extension
  uses: github/gh-aw/actions/setup-cli@bd9c0ca491e6334a2797ef56ad6ee89958d54ab9 # v0.53.2
  with:
    version: v0.53.2

Error

The workflow fails immediately with:

Error: Unable to resolve action `github/gh-aw@bd9c0ca491e6334a2797ef56ad6ee89958d54ab9 # v0.53.2`,
unable to find version `bd9c0ca491e6334a2797ef56ad6ee89958d54ab9 # v0.53.2`

Root Cause

In YAML, wrapping a value in double quotes causes the entire string — including a trailing # comment — to be treated as the literal value. The uses: field must be unquoted so that YAML parses # v0.53.2 as a comment and GitHub Actions only sees the SHA as the ref.

Workaround

Remove the surrounding quotes manually:

uses: github/gh-aw/actions/setup-cli@bd9c0ca491e6334a2797ef56ad6ee89958d54ab9 # v0.53.2

Possibly Related

Environment

  • OS: macOS
  • gh-aw version: v0.53.2
  • GitHub Actions runner: 2.332.0
    </issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI and others added 2 commits March 5, 2026 03:20
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
When upgrading copilot-setup-steps.yml to a SHA-pinned action reference
(e.g. `org/repo@<sha> # vX.Y.Z`), the YAML marshaler quoted the entire
string because '#' is a special YAML character. This caused GitHub Actions
to treat the comment as part of the action ref, resulting in an unresolvable
git ref error.

Fix: add an `unquoteUsesValues` post-processing step after yaml.Marshal that
strips double-quotes from `uses:` lines, restoring the '#...' suffix to a
proper YAML comment.

Regression test added: TestUpgradeCopilotSetupSteps_SHAPinnedNoQuotes

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix bug where gh aw upgrade wraps uses value in quotes fix: gh aw upgrade wraps SHA-pinned uses: value in quotes, including inline comment Mar 5, 2026
@pelikhan
Copy link
Contributor

pelikhan commented Mar 5, 2026

@copilot ensure minimal formatting change is applied to the .yml. In particular do not marshal/unmarshall as it completely changes the structure of the file. Instead you need to rely on regular expression search and replace.

Instead of parsing the workflow into a struct, marshaling back to YAML
(which reformats the entire file), and post-processing quotes, directly
operate on the raw file bytes using two targeted regex substitutions:

1. setupCliUsesPattern - replaces the uses: line (handles quoted/unquoted,
   SHA-pinned with comment, version-tag), compiled once at package level.
2. versionInWithPattern - replaces the version: value in the with: block
   immediately following, built at call time with the escaped new ref so
   only the correct step is touched (cannot be pre-compiled).

Remove upgradeSetupCliVersion, unquoteUsesValues, usesQuotePattern and the
now-unused errors/goccy-yaml imports. Update tests to exercise the new
content-based helper.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot ensure minimal formatting change is applied to the .yml. In particular do not marshal/unmarshall as it completely changes the structure of the file. Instead you need to rely on regular expres...

Done in 3dda0a5. The upgrade path now operates entirely on raw file bytes using two targeted regex substitutions — no yaml.Unmarshal/yaml.Marshal involved. The setupCliUsesPattern is compiled once at package level; the versionInWithPattern is built at call time (commented to explain why it can't be pre-compiled). All other file content — comments, indentation, field ordering, whitespace — is preserved unchanged.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.2.3
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.2.3 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.2.3 --jq .object.sha GOSUMDB GOWORK 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ck &#39;scripts/**/*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v2.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha GOSUMDB GOWORK 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ck &#39;scripts/**/*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha heck &#39;**/*.cjs&#39; GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ck &#39;scripts/**/*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v3.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v3.0.0 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v3.0.0 --jq .object.sha GOSUMDB GOWORK 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ck &#39;scripts/**/*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan
Copy link
Contributor

pelikhan commented Mar 5, 2026

@copilot add tests to ensure yaml is not shuffled or other comments are not lost

Add TestUpgradeSetupCliVersionInContent_ExactPreservation which provides
a rich workflow YAML containing:
- top-level, job-level, and step-level comments
- inline comments on individual fields (on:, uses:, run:, path filters)
- multiple steps including one after the upgraded step
- blank lines and extra with: parameters

It constructs the expected output manually (identical to input except the
two target lines) and compares with string equality, so any accidental
shuffle, comment drop, or whitespace change causes a clear line-by-line
diff in the failure output.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot add tests to ensure yaml is not shuffled or other comments are not lost

Added TestUpgradeSetupCliVersionInContent_ExactPreservation in 30969ab. The test uses a workflow file that contains top-level comments, job-level comments, step-level comments, inline comments on individual fields (on:, uses:, run:, path filters), blank lines, extra with: parameters, and steps before and after the upgraded step. It constructs the expected output manually and does an exact string equality comparison — any accidental shuffle, dropped comment, or whitespace change will produce a line-by-line diff in the failure output.

@pelikhan pelikhan marked this pull request as ready for review March 5, 2026 04:16
Copilot AI review requested due to automatic review settings March 5, 2026 04:16
@pelikhan
Copy link
Contributor

pelikhan commented Mar 5, 2026

@copilot revert install.sh

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot revert install.sh

Done in 70510beactions/setup-cli/install.sh is restored to its original pre-PR state.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes gh aw upgrade rewriting uses: lines into quoted strings (including inline # comments) by switching from YAML re-marshal to targeted in-place edits, aiming to preserve workflow formatting and comments.

Changes:

  • Replace struct-based YAML unmarshal/marshal upgrade with regex-based byte-preserving upgrade logic for setup-cli references.
  • Update/expand tests to validate SHA-pinned upgrades remain unquoted and that non-target YAML content is preserved exactly.
  • Update actions/setup-cli/install.sh install path logic and bump versions / add job concurrency in a generated workflow lock file.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
pkg/cli/copilot_setup.go Implements regex-based setup-cli uses: + with.version rewriting to avoid YAML reformatting/quote issues.
pkg/cli/copilot_setup_test.go Reworks tests to validate unquoted SHA refs and exact byte preservation outside the two modified lines.
actions/setup-cli/install.sh Changes gh extension install behavior/output handling during installation.
.github/workflows/constraint-solving-potd.lock.yml Bumps embedded tool versions and adds job-level concurrency configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +277 to +282
// setupCliUsesPattern matches the uses: line for github/gh-aw/actions/setup-cli.
// It handles unquoted version-tag refs, unquoted SHA-pinned refs (with trailing comment),
// and quoted refs produced by some YAML marshalers (e.g. "...@sha # vX.Y.Z").
var setupCliUsesPattern = regexp.MustCompile(
`(?m)^(\s+uses:[ \t]*)"?(github/gh-aw/actions/setup-cli@[^"\n]*)"?([ \t]*)$`)

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setupCliUsesPattern won’t match a common quoting style where only the action ref is quoted and an inline comment follows outside the quotes, e.g. uses: "github/gh-aw/actions/setup-cli@<sha>" # vX.Y.Z. Because the pattern anchors to end-of-line right after the optional closing quote, this case will be skipped and the upgrade won’t run. Consider extending the regex to allow an optional #... comment after the closing quote (while still stripping the quotes around the ref).

Copilot uses AI. Check for mistakes.
Comment on lines +304 to +313
// Replace the version: value in the with: block immediately following the
// setup-cli uses: line. A combined multiline match is used so that only the
// version: parameter belonging to this specific step is updated.
// This pattern cannot be pre-compiled at package level because it embeds
// the runtime value newUses (which varies with version and resolver output).
escapedNewUses := regexp.QuoteMeta(newUses)
versionInWithPattern := regexp.MustCompile(
`(?s)(uses:[ \t]*` + escapedNewUses + `[^\n]*\n(?:[^\n]*\n)*?[ \t]+with:[ \t]*\n(?:[^\n]*\n)*?[ \t]+version:[ \t]*)(\S+)([ \t]*(?:\n|$))`)
updated = versionInWithPattern.ReplaceAll(updated, []byte("${1}"+version+"${3}"))

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

versionInWithPattern can span across subsequent steps because it allows arbitrary lines between the matched uses: line and the first later with:/version: block. If the setup-cli step lacks a with: block (or lacks version:), this regex can end up updating a different step’s with.version later in the job. Consider restricting the match to the current step boundary (e.g., stop before the next ^\s*-\s at the same indentation) or switch to a small line-based scan starting at the matched uses: line and only rewriting within that step’s indented block.

Suggested change
// Replace the version: value in the with: block immediately following the
// setup-cli uses: line. A combined multiline match is used so that only the
// version: parameter belonging to this specific step is updated.
// This pattern cannot be pre-compiled at package level because it embeds
// the runtime value newUses (which varies with version and resolver output).
escapedNewUses := regexp.QuoteMeta(newUses)
versionInWithPattern := regexp.MustCompile(
`(?s)(uses:[ \t]*` + escapedNewUses + `[^\n]*\n(?:[^\n]*\n)*?[ \t]+with:[ \t]*\n(?:[^\n]*\n)*?[ \t]+version:[ \t]*)(\S+)([ \t]*(?:\n|$))`)
updated = versionInWithPattern.ReplaceAll(updated, []byte("${1}"+version+"${3}"))
// Replace the version: value in the with: block that belongs to the same
// step as the setup-cli uses: line. We perform a small line-based scan
// starting at each matching uses: line and only rewrite within that step's
// indented block, stopping before the next step at the same or lesser
// indentation.
updatedStr := string(updated)
lines := strings.Split(updatedStr, "\n")
for i := 0; i < len(lines); i++ {
line := lines[i]
if !strings.Contains(line, "uses:") || !strings.Contains(line, newUses) {
continue
}
// Indentation (spaces/tabs) of the uses: line; used to detect step boundary.
trimmed := strings.TrimLeft(line, " \t")
usesIndent := len(line) - len(trimmed)
// Scan forward within this step to find a with: block and its version: entry.
withIndent := -1
for j := i + 1; j < len(lines); j++ {
stepLine := lines[j]
stepTrimmed := strings.TrimLeft(stepLine, " \t")
stepIndent := len(stepLine) - len(stepTrimmed)
// Stop when we reach the start of another step at the same or less indentation.
if strings.HasPrefix(stepTrimmed, "- ") && stepIndent <= usesIndent {
break
}
if withIndent == -1 {
// Look for the with: line belonging to this step.
if strings.HasPrefix(strings.TrimSpace(stepLine), "with:") {
withIndent = stepIndent
}
continue
}
// Once inside the with: block, stop if indentation decreases to the with: level or less.
if stepIndent <= withIndent {
break
}
// Look for the version: line within the with: block.
versionIdx := strings.Index(stepLine, "version:")
if versionIdx == -1 {
continue
}
prefix := stepLine[:versionIdx+len("version:")]
rest := stepLine[versionIdx+len("version:"):]
restTrim := strings.TrimLeft(rest, " \t")
leadingWSLen := len(rest) - len(restTrim)
leadingWS := rest[:leadingWSLen]
// Identify the existing version token (first non-whitespace sequence) to replace.
endOfToken := len(restTrim)
for k, r := range restTrim {
if r == ' ' || r == '\t' {
endOfToken = k
break
}
}
tail := ""
if endOfToken < len(restTrim) {
tail = restTrim[endOfToken:]
}
lines[j] = prefix + leadingWS + version + tail
// Only update the first version: for this uses: occurrence.
break
}
}
updated = []byte(strings.Join(lines, "\n"))

Copilot uses AI. Check for mistakes.
@pelikhan pelikhan merged commit bc9fa0c into main Mar 5, 2026
77 checks passed
@pelikhan pelikhan deleted the copilot/fix-gh-aw-upgrade-comments branch March 5, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: gh aw upgrade wraps uses value in quotes, including the inline comment

3 participants