Skip to content
Open
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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/devcontainers/features/copilot-cli:latest": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/git-lfs:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/node:1": {
"version": "24"
}
Expand Down
10 changes: 10 additions & 0 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
"version": "v5.6.0",
"sha": "40f1582b2485089dde7abd97c1529aa768e1baff"
},
"actions/setup-go@v6": {
"repo": "actions/setup-go",
"version": "v6",
"sha": "4b73464bb391d4059bd26b0524d20df3927bd417"
},
"actions/setup-go@v6.2.0": {
"repo": "actions/setup-go",
"version": "v6.2.0",
Expand Down Expand Up @@ -110,6 +115,11 @@
"version": "v6",
"sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f"
},
"anchore/sbom-action@v0": {
"repo": "anchore/sbom-action",
"version": "v0",
"sha": "17ae1740179002c89186b61233e0f892c3118b11"
},
"anchore/sbom-action@v0.22.2": {
"repo": "anchore/sbom-action",
"version": "v0.22.2",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-coach.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/go-logger.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/hourly-ci-cleaner.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .github/workflows/release.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/tidy.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions pkg/cli/pr_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,4 @@ func TestNewPRTransferSubcommand(t *testing.T) {
if repoFlag == nil {
t.Error("Expected --repo flag to exist")
}

// Check that --verbose flag exists
verboseFlag := cmd.Flags().Lookup("verbose")
if verboseFlag == nil {
t.Error("Expected --verbose flag to exist")
}
}
6 changes: 3 additions & 3 deletions pkg/workflow/action_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,9 @@ func TestApplyActionPinToStep(t *testing.T) {
func TestGetActionPinsSorting(t *testing.T) {
pins := getActionPins()

// Verify we got all the pins (37 as of February 2026)
if len(pins) != 37 {
t.Errorf("getActionPins() returned %d pins, expected 37", len(pins))
// Verify we got all the pins (39 as of February 2026)
if len(pins) != 39 {
t.Errorf("getActionPins() returned %d pins, expected 39", len(pins))
}

// Verify they are sorted by version (descending) then by repository name (ascending)
Expand Down
42 changes: 22 additions & 20 deletions pkg/workflow/action_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ const (
// - actionMode: The action mode (dev or release)
// - version: The version string to use for release mode
// - actionTag: Optional override tag/SHA (takes precedence over version when in release mode)
// - data: Optional WorkflowData for SHA resolution (can be nil for standalone use)
// - resolver: Optional ActionSHAResolver for dynamic SHA resolution (can be nil for standalone use)
//
// Returns:
// - For dev mode: "./actions/setup" (local path)
// - For release mode with data: "github/gh-aw/actions/setup@<sha> # <version>" (SHA-pinned)
// - For release mode without data: "github/gh-aw/actions/setup@<version>" (tag-based, SHA resolved later)
// - For release mode with resolver: "github/gh-aw/actions/setup@<sha> # <version>" (SHA-pinned)
// - For release mode without resolver: "github/gh-aw/actions/setup@<version>" (tag-based, SHA resolved later)
// - Falls back to local path if version is invalid in release mode
Comment on lines +29 to 31
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The docstring examples use “# ”, but this function passes the selected tag (actionTag override or version) to formatActionReference, so the comment will be “# ” (not necessarily the compiler version). Updating the comment/examples would avoid misleading callers when actionTag differs from version.

Suggested change
// - For release mode with resolver: "github/gh-aw/actions/setup@<sha> # <version>" (SHA-pinned)
// - For release mode without resolver: "github/gh-aw/actions/setup@<version>" (tag-based, SHA resolved later)
// - Falls back to local path if version is invalid in release mode
// - For release mode with resolver: "github/gh-aw/actions/setup@<sha> # <tag>" (SHA-pinned, where <tag> is actionTag or version)
// - For release mode without resolver: "github/gh-aw/actions/setup@<tag>" (tag-based, SHA resolved later)
// - Falls back to local path if tag is invalid (empty or "dev") in release mode

Copilot uses AI. Check for mistakes.
func ResolveSetupActionReference(actionMode ActionMode, version string, actionTag string, data *WorkflowData) string {
func ResolveSetupActionReference(actionMode ActionMode, version string, actionTag string, resolver ActionSHAResolver) string {
localPath := "./actions/setup"

// Dev mode - return local path
Expand All @@ -55,25 +55,23 @@ func ResolveSetupActionReference(actionMode ActionMode, version string, actionTa
}

// Construct the remote reference with tag: github/gh-aw/actions/setup@tag
remoteRef := fmt.Sprintf("%s/%s@%s", GitHubOrgRepo, actionPath, tag)

// If WorkflowData is available, try to resolve the SHA
if data != nil {
actionRepo := fmt.Sprintf("%s/%s", GitHubOrgRepo, actionPath)
pinnedRef, err := GetActionPinWithData(actionRepo, tag, data)
if err != nil {
// In strict mode, GetActionPinWithData returns an error
actionRefLog.Printf("Failed to pin action %s@%s: %v", actionRepo, tag, err)
return ""
}
if pinnedRef != "" {
// Successfully resolved to SHA
actionRepo := fmt.Sprintf("%s/%s", GitHubOrgRepo, actionPath)
remoteRef := fmt.Sprintf("%s@%s", actionRepo, tag)

// If a resolver is available, try to resolve the SHA
if resolver != nil {
sha, err := resolver.ResolveSHA(actionRepo, tag)
if err == nil && sha != "" {
pinnedRef := formatActionReference(actionRepo, sha, tag)
actionRefLog.Printf("Release mode: resolved %s to SHA-pinned reference: %s", remoteRef, pinnedRef)
return pinnedRef
}
if err != nil {
actionRefLog.Printf("Failed to resolve SHA for %s@%s: %v", actionRepo, tag, err)
}
}

// If WorkflowData is not available or SHA resolution failed, return tag-based reference
// If no resolver or SHA resolution failed, return tag-based reference
// This is for backward compatibility with standalone workflow generators
actionRefLog.Printf("Release mode: using tag-based remote action reference: %s (SHA will be resolved later)", remoteRef)
return remoteRef
Expand Down Expand Up @@ -104,11 +102,15 @@ func (c *Compiler) resolveActionReference(localActionPath string, data *Workflow
// For ./actions/setup, check for compiler-level actionTag override first
if localActionPath == "./actions/setup" {
// Use compiler actionTag if available, otherwise check features
var resolver ActionSHAResolver
if data != nil && data.ActionResolver != nil {
resolver = data.ActionResolver
}
if c.actionTag != "" {
return ResolveSetupActionReference(c.actionMode, c.version, c.actionTag, data)
return ResolveSetupActionReference(c.actionMode, c.version, c.actionTag, resolver)
}
if !hasActionTag {
return ResolveSetupActionReference(c.actionMode, c.version, "", data)
return ResolveSetupActionReference(c.actionMode, c.version, "", resolver)
}
}

Expand Down
12 changes: 3 additions & 9 deletions pkg/workflow/action_reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,20 +341,14 @@ func TestResolveSetupActionReference(t *testing.T) {
}

func TestResolveSetupActionReferenceWithData(t *testing.T) {
t.Run("release mode with WorkflowData resolves SHA", func(t *testing.T) {
t.Run("release mode with resolver resolves SHA", func(t *testing.T) {
// Create mock action resolver and cache
cache := NewActionCache("")
resolver := NewActionResolver(cache)

data := &WorkflowData{
ActionResolver: resolver,
ActionCache: cache,
StrictMode: false,
}

// The resolver will fail to resolve github/gh-aw/actions/setup@v1.0.0
// since it's not a real tag, but it should fall back gracefully
ref := ResolveSetupActionReference(ActionModeRelease, "v1.0.0", "", data)
ref := ResolveSetupActionReference(ActionModeRelease, "v1.0.0", "", resolver)

// Without a valid pin or successful resolution, should return tag-based reference
expectedRef := "github/gh-aw/actions/setup@v1.0.0"
Expand All @@ -363,7 +357,7 @@ func TestResolveSetupActionReferenceWithData(t *testing.T) {
}
})

t.Run("release mode with nil data returns tag-based reference", func(t *testing.T) {
t.Run("release mode with nil resolver returns tag-based reference", func(t *testing.T) {
ref := ResolveSetupActionReference(ActionModeRelease, "v1.0.0", "", nil)
expectedRef := "github/gh-aw/actions/setup@v1.0.0"
if ref != expectedRef {
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/action_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (

var resolverLog = logger.New("workflow:action_resolver")

// ActionSHAResolver is the minimal interface for resolving an action tag to its commit SHA.
type ActionSHAResolver interface {
ResolveSHA(repo, version string) (string, error)
}

// ActionResolver handles resolving action SHAs using GitHub CLI
type ActionResolver struct {
cache *ActionCache
Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
"version": "v5.6.0",
"sha": "40f1582b2485089dde7abd97c1529aa768e1baff"
},
"actions/setup-go@v6": {
"repo": "actions/setup-go",
"version": "v6",
"sha": "4b73464bb391d4059bd26b0524d20df3927bd417"
},
"actions/setup-go@v6.2.0": {
"repo": "actions/setup-go",
"version": "v6.2.0",
Expand Down Expand Up @@ -110,6 +115,11 @@
"version": "v6",
"sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f"
},
"anchore/sbom-action@v0": {
"repo": "anchore/sbom-action",
"version": "v0",
"sha": "17ae1740179002c89186b61233e0f892c3118b11"
},
"anchore/sbom-action@v0.22.2": {
"repo": "anchore/sbom-action",
"version": "v0.22.2",
Expand Down
9 changes: 6 additions & 3 deletions pkg/workflow/maintenance_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,12 @@ jobs:
`)

// Get the setup action reference (local or remote based on mode)
// Pass nil for data since maintenance workflow doesn't have WorkflowData
// In release mode without data, it will return tag-based reference
setupActionRef := ResolveSetupActionReference(actionMode, version, actionTag, nil)
// Use the first available WorkflowData's ActionResolver to enable SHA pinning
var resolver ActionSHAResolver
if len(workflowDataList) > 0 && workflowDataList[0].ActionResolver != nil {
resolver = workflowDataList[0].ActionResolver
Comment on lines +154 to +155
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The comment says this uses the “first available WorkflowData's ActionResolver”, but the code only checks workflowDataList[0] (and doesn’t guard against workflowDataList[0] being nil). This can miss a resolver provided by later entries or panic if the slice contains a nil element. Consider iterating until you find the first non-nil WorkflowData with a non-nil ActionResolver (or explicitly document that index 0 is required).

Suggested change
if len(workflowDataList) > 0 && workflowDataList[0].ActionResolver != nil {
resolver = workflowDataList[0].ActionResolver
for _, wfData := range workflowDataList {
if wfData != nil && wfData.ActionResolver != nil {
resolver = wfData.ActionResolver
break
}

Copilot uses AI. Check for mistakes.
}
setupActionRef := ResolveSetupActionReference(actionMode, version, actionTag, resolver)

// Add checkout step only in dev mode (for local action paths)
if actionMode == ActionModeDev {
Expand Down
37 changes: 37 additions & 0 deletions pkg/workflow/maintenance_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,43 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) {
}
})

t.Run("release mode with action-tag and resolver uses SHA-pinned ref", func(t *testing.T) {
tmpDir := t.TempDir()
// Set up an action resolver with a cached SHA for the setup action
cache := NewActionCache(tmpDir)
cache.Set("github/gh-aw/actions/setup", "v0.47.4", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
resolver := NewActionResolver(cache)

workflowDataListWithResolver := []*WorkflowData{
{
Name: "test-workflow",
ActionResolver: resolver,
ActionPinWarnings: make(map[string]bool),
SafeOutputs: &SafeOutputsConfig{
CreateIssues: &CreateIssuesConfig{
Expires: 48,
},
},
},
}

err := GenerateMaintenanceWorkflow(workflowDataListWithResolver, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", 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)
}
expectedRef := "github/gh-aw/actions/setup@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # v0.47.4"
if !strings.Contains(string(content), expectedRef) {
t.Errorf("Expected SHA-pinned ref %q, got:\n%s", expectedRef, string(content))
}
if strings.Contains(string(content), "uses: ./actions/setup") {
t.Errorf("Expected no local path in release mode with action-tag, got:\n%s", string(content))
}
})

t.Run("dev mode ignores action-tag and uses local path", func(t *testing.T) {
tmpDir := t.TempDir()
err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "v0.47.4", false)
Expand Down
Loading