Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions pkg/cli/copilot_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,32 @@ func renderCopilotSetupUpdateInstructions(filePath string, actionMode workflow.A
var setupCliUsesPattern = regexp.MustCompile(
`(?m)^(\s+uses:[ \t]*)"?(github/gh-aw(?:-actions)?/(?:actions/)?setup-cli@[^"\n]*)"?([ \t]*)$`)

// versionInWithPattern matches the version: parameter in the with: block that immediately
// follows any setup-cli uses: line (any ref format: version tag, SHA-pinned, or quoted).
// It is anchored to the same action repos as setupCliUsesPattern so that it only updates
// the version belonging to the setup-cli step, but is independent of the exact ref value.
// This allows it to correct pre-existing drift where the uses: comment and with: version:
// were already out of sync before the upgrade was run.
//
// Pattern breakdown:
//
// [ \t]+uses:[ \t]* — indented uses: key with optional surrounding spaces
// "?github/gh-aw(?:-actions)?/(?:actions/)?setup-cli@[^"\n]*"?
// — any setup-cli ref (version tag, SHA+comment, or quoted)
// [^\n]*\n — rest of the uses: line (e.g. trailing spaces)
// (?:[^\n]*\n)*? — zero or more lines between uses: and with: (non-greedy)
// [ \t]+with:[ \t]*\n — indented with: key
// (?:[^\n]*\n)*? — zero or more lines between with: and version: (non-greedy)
// [ \t]+version:[ \t]* — indented version: key (final part of the prefix captured as group 1)
// (\S+) — the version value (captured as group 2)
// ([ \t]*(?:\n|$)) — trailing whitespace and line terminator (captured as group 3)
//
// Note: In the full pattern, group 1 wraps the entire prefix from the setup-cli `uses:` line
// through the `version:` key (and following spaces), group 2 is just the version value, and
// group 3 is the trailing whitespace plus the line terminator.
var versionInWithPattern = regexp.MustCompile(
`(?s)([ \t]+uses:[ \t]*"?github/gh-aw(?:-actions)?/(?:actions/)?setup-cli@[^"\n]*"?[^\n]*\n(?:[^\n]*\n)*?[ \t]+with:[ \t]*\n(?:[^\n]*\n)*?[ \t]+version:[ \t]*)(\S+)([ \t]*(?:\n|$))`)

// upgradeSetupCliVersionInContent replaces the setup-cli action reference and the
// associated version: parameter in the raw YAML content using targeted regex
// substitutions, preserving all other formatting in the file.
Expand All @@ -325,13 +351,9 @@ func upgradeSetupCliVersionInContent(content []byte, actionMode workflow.ActionM
updated := setupCliUsesPattern.ReplaceAll(content, []byte("${1}"+newUses+"${3}"))

// 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|$))`)
// setup-cli uses: line. versionInWithPattern matches any valid setup-cli
// reference so it succeeds even when there was pre-existing drift between
// the uses: comment and the version: parameter before the upgrade was run.
updated = versionInWithPattern.ReplaceAll(updated, []byte("${1}"+version+"${3}"))

if bytes.Equal(content, updated) {
Expand Down
55 changes: 55 additions & 0 deletions pkg/cli/copilot_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,61 @@ jobs:
resolver: nil,
expectUpgrade: false,
},
{
name: "corrects drift: SHA-pinned uses comment ahead of with: version:",
content: `jobs:
copilot-setup-steps:
steps:
- name: Install gh-aw extension
uses: github/gh-aw/actions/setup-cli@cb7966564184443e601bd6135d5fbb534300070e # v0.58.0
with:
version: v0.53.6
`,
actionMode: workflow.ActionModeRelease,
version: "v0.60.0",
resolver: &mockSHAResolver{sha: "newsha123"},
expectUpgrade: true,
validate: func(t *testing.T, got string) {
if !strings.Contains(got, "uses: github/gh-aw/actions/setup-cli@newsha123 # v0.60.0") {
t.Errorf("Expected updated SHA-pinned uses: line, got:\n%s", got)
}
if !strings.Contains(got, "version: v0.60.0") {
t.Errorf("Expected with: version: updated to v0.60.0, got:\n%s", got)
}
if strings.Contains(got, "v0.53.6") {
t.Errorf("Stale version v0.53.6 should be gone, got:\n%s", got)
}
if strings.Contains(got, "v0.58.0") {
t.Errorf("Old comment version v0.58.0 should be gone, got:\n%s", got)
}
},
},
{
name: "corrects drift: version-tag uses ahead of with: version:",
content: `jobs:
copilot-setup-steps:
steps:
- name: Install gh-aw extension
uses: github/gh-aw/actions/setup-cli@v0.58.0
with:
version: v0.53.6
`,
actionMode: workflow.ActionModeRelease,
version: "v0.60.0",
resolver: nil,
expectUpgrade: true,
validate: func(t *testing.T, got string) {
if !strings.Contains(got, "uses: github/gh-aw/actions/setup-cli@v0.60.0") {
t.Errorf("Expected updated uses: line, got:\n%s", got)
}
if !strings.Contains(got, "version: v0.60.0") {
t.Errorf("Expected with: version: updated to v0.60.0, got:\n%s", got)
}
if strings.Contains(got, "v0.53.6") {
t.Errorf("Stale version v0.53.6 should be gone, got:\n%s", got)
}
},
},
}

for _, tt := range tests {
Expand Down
Loading