Skip to content
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
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
}
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