From 313bcbea7e1a865efa7fc066526cbe3a64922694 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:58:28 +0000 Subject: [PATCH 1/3] Initial plan From c4cdd22ba76606259cf0e906a375044981b8f408 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:17:26 +0000 Subject: [PATCH 2/3] fix: SHA-pin setup-cli action references in maintenance workflow generation The generateInstallCLISteps function was using mutable version tags (e.g., @v0.64.0) for the setup-cli action instead of SHA-pinned references, inconsistent with other actions in the same workflows. Fix: Add an ActionSHAResolver parameter to generateInstallCLISteps and a resolveActionRef helper that attempts to resolve the SHA when a resolver is available, falling back to tag-based reference when not. Also add a TestGenerateMaintenanceWorkflow_SetupCLISHAPinning test and sub-tests for SHA-pinned setup-cli in release/action modes. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/adf6231f-013f-433f-9f5f-3017ff35b7d8 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/maintenance_workflow.go | 29 ++++++-- pkg/workflow/maintenance_workflow_test.go | 91 ++++++++++++++++++++++- 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 48a1d81f89c..5edf8ad459a 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -16,7 +16,8 @@ var maintenanceLog = logger.New("workflow:maintenance_workflow") // In dev mode: builds from source using Setup Go + Build gh-aw (./gh-aw binary available) // In release mode: installs the released CLI via the setup-cli action (gh aw available) // In action mode: installs the released CLI via the gh-aw-actions/setup-cli action (gh aw available) -func generateInstallCLISteps(actionMode ActionMode, version string, actionTag string) string { +// When resolver is non-nil, attempts to resolve the setup-cli action to a SHA-pinned reference. +func generateInstallCLISteps(actionMode ActionMode, version string, actionTag string, resolver ActionSHAResolver) string { if actionMode == ActionModeDev { return ` - name: Setup Go uses: ` + GetActionPin("actions/setup-go") + ` @@ -37,8 +38,10 @@ func generateInstallCLISteps(actionMode ActionMode, version string, actionTag st // Action mode: use setup-cli action from external gh-aw-actions repository if actionMode == ActionModeAction { + actionRepo := GitHubActionsOrgRepo + "/setup-cli" + ref := resolveActionRef(actionRepo, cliTag, resolver) return ` - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@` + cliTag + ` + uses: ` + ref + ` with: version: ` + cliTag + ` @@ -46,14 +49,30 @@ func generateInstallCLISteps(actionMode ActionMode, version string, actionTag st } // Release mode: use setup-cli action (consistent with copilot-setup-steps.yml) + actionRepo := GitHubOrgRepo + "/actions/setup-cli" + ref := resolveActionRef(actionRepo, cliTag, resolver) return ` - name: Install gh-aw - uses: github/gh-aw/actions/setup-cli@` + cliTag + ` + uses: ` + ref + ` with: version: ` + cliTag + ` ` } +// resolveActionRef attempts to resolve an action repo@tag to a SHA-pinned reference +// using the provided resolver. If the resolver is nil or resolution fails, it returns +// the tag-based reference (repo@tag). +func resolveActionRef(actionRepo, tag string, resolver ActionSHAResolver) string { + if resolver != nil && tag != "" && tag != "dev" { + sha, err := resolver.ResolveSHA(actionRepo, tag) + if err == nil && sha != "" { + return formatActionReference(actionRepo, sha, tag) + } + maintenanceLog.Printf("Failed to resolve SHA for %s@%s: %v, falling back to tag reference", actionRepo, tag, err) + } + return actionRepo + "@" + tag +} + // getCLICmdPrefix returns the CLI command prefix based on action mode. // In dev mode: "./gh-aw" (local binary built from source) // In release mode: "gh aw" (installed via gh extension) @@ -312,7 +331,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag)) + yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Run operation uses: ` + GetActionPin("actions/github-script") + ` env: @@ -399,7 +418,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag)) + yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Compile workflows run: | ` + getCLICmdPrefix(actionMode) + ` compile --validate --verbose diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index f7ddc36398d..1c04ee582f8 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -425,7 +425,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { func TestGenerateInstallCLISteps(t *testing.T) { t.Run("dev mode generates Setup Go and Build gh-aw steps", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeDev, "v1.0.0", "") + result := generateInstallCLISteps(ActionModeDev, "v1.0.0", "", nil) if !strings.Contains(result, "Setup Go") { t.Errorf("Dev mode should include Setup Go step, got:\n%s", result) } @@ -438,7 +438,7 @@ func TestGenerateInstallCLISteps(t *testing.T) { }) t.Run("release mode generates setup-cli action step", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "") + result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "", nil) if !strings.Contains(result, "github/gh-aw/actions/setup-cli@v1.0.0") { t.Errorf("Release mode should use setup-cli action with version, got:\n%s", result) } @@ -451,11 +451,52 @@ func TestGenerateInstallCLISteps(t *testing.T) { }) t.Run("release mode uses actionTag over version", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "v2.0.0") + result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "v2.0.0", nil) if !strings.Contains(result, "setup-cli@v2.0.0") { t.Errorf("Release mode should use actionTag v2.0.0, got:\n%s", result) } }) + + t.Run("release mode with resolver uses SHA-pinned setup-cli reference", func(t *testing.T) { + tmpDir := t.TempDir() + cache := NewActionCache(tmpDir) + cache.Set("github/gh-aw/actions/setup-cli", "v1.0.0", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + resolver := NewActionResolver(cache) + + result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "", resolver) + expectedRef := "github/gh-aw/actions/setup-cli@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # v1.0.0" + if !strings.Contains(result, expectedRef) { + t.Errorf("Release mode with resolver should use SHA-pinned setup-cli reference %q, got:\n%s", expectedRef, result) + } + // Must not contain the bare mutable tag + if strings.Contains(result, "setup-cli@v1.0.0") { + t.Errorf("Release mode with resolver must not use mutable tag setup-cli@v1.0.0, got:\n%s", result) + } + }) + + t.Run("action mode with resolver uses SHA-pinned setup-cli reference", func(t *testing.T) { + tmpDir := t.TempDir() + cache := NewActionCache(tmpDir) + cache.Set("github/gh-aw-actions/setup-cli", "v1.0.0", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + resolver := NewActionResolver(cache) + + result := generateInstallCLISteps(ActionModeAction, "v1.0.0", "", resolver) + expectedRef := "github/gh-aw-actions/setup-cli@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # v1.0.0" + if !strings.Contains(result, expectedRef) { + t.Errorf("Action mode with resolver should use SHA-pinned setup-cli reference %q, got:\n%s", expectedRef, result) + } + // Must not contain the bare mutable tag + if strings.Contains(result, "setup-cli@v1.0.0") { + t.Errorf("Action mode with resolver must not use mutable tag setup-cli@v1.0.0, got:\n%s", result) + } + }) + + t.Run("release mode without resolver falls back to tag reference", func(t *testing.T) { + result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "", nil) + if !strings.Contains(result, "github/gh-aw/actions/setup-cli@v1.0.0") { + t.Errorf("Release mode without resolver should fall back to tag reference, got:\n%s", result) + } + }) } func TestGetCLICmdPrefix(t *testing.T) { @@ -541,3 +582,47 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { } }) } + +func TestGenerateMaintenanceWorkflow_SetupCLISHAPinning(t *testing.T) { + setupCLISHA := "cccccccccccccccccccccccccccccccccccccccc" + + workflowDataListWithResolver := func(resolver *ActionResolver) []*WorkflowData { + return []*WorkflowData{ + { + Name: "test-workflow", + ActionResolver: resolver, + ActionPinWarnings: make(map[string]bool), + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + Expires: 48, + }, + }, + }, + } + } + + t.Run("release mode with resolver SHA-pins setup-cli in run_operation", func(t *testing.T) { + tmpDir := t.TempDir() + cache := NewActionCache(tmpDir) + cache.Set("github/gh-aw/actions/setup-cli", "v1.0.0", setupCLISHA) + resolver := NewActionResolver(cache) + + err := GenerateMaintenanceWorkflow(workflowDataListWithResolver(resolver), tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + yaml := string(content) + expectedRef := "github/gh-aw/actions/setup-cli@" + setupCLISHA + " # v1.0.0" + if !strings.Contains(yaml, expectedRef) { + t.Errorf("Expected SHA-pinned setup-cli reference %q in generated workflow, got:\n%s", expectedRef, yaml) + } + // Bare tag must not appear + if strings.Contains(yaml, "setup-cli@v1.0.0") { + t.Errorf("Generated workflow must not use mutable tag setup-cli@v1.0.0; got:\n%s", yaml) + } + }) +} From 4e3d3fbaa67cd3c21b7ccab14220879750a30e34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:42:36 +0000 Subject: [PATCH 3/3] fix: address reviewer feedback on resolveActionRef and test hermeticity - resolveActionRef: log only on actual error, not on empty-SHA-no-error case - TestGenerateMaintenanceWorkflow_SetupCLISHAPinning: seed setup action cache entry to prevent real gh api calls (hermetic test) Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3e259f0b-9912-4006-bc44-d891d2943a39 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/maintenance_workflow.go | 5 +++-- pkg/workflow/maintenance_workflow_test.go | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 5edf8ad459a..bceef12a29c 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -65,10 +65,11 @@ func generateInstallCLISteps(actionMode ActionMode, version string, actionTag st func resolveActionRef(actionRepo, tag string, resolver ActionSHAResolver) string { if resolver != nil && tag != "" && tag != "dev" { sha, err := resolver.ResolveSHA(actionRepo, tag) - if err == nil && sha != "" { + if err != nil { + maintenanceLog.Printf("Failed to resolve SHA for %s@%s: %v, falling back to tag reference", actionRepo, tag, err) + } else if sha != "" { return formatActionReference(actionRepo, sha, tag) } - maintenanceLog.Printf("Failed to resolve SHA for %s@%s: %v, falling back to tag reference", actionRepo, tag, err) } return actionRepo + "@" + tag } diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 1c04ee582f8..3947d0b928c 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -605,6 +605,10 @@ func TestGenerateMaintenanceWorkflow_SetupCLISHAPinning(t *testing.T) { tmpDir := t.TempDir() cache := NewActionCache(tmpDir) cache.Set("github/gh-aw/actions/setup-cli", "v1.0.0", setupCLISHA) + // Also seed the setup action to keep the test hermetic (GenerateMaintenanceWorkflow + // calls ResolveSetupActionReference with the same resolver, which would otherwise + // attempt a real gh api call on a cache miss). + cache.Set("github/gh-aw/actions/setup", "v1.0.0", "dddddddddddddddddddddddddddddddddddddddd") resolver := NewActionResolver(cache) err := GenerateMaintenanceWorkflow(workflowDataListWithResolver(resolver), tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false)