diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index e65d2ed8676..5cd0acb6c89 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -6276,13 +6276,13 @@ jobs: - name: Download Go modules run: go mod download - name: Generate SBOM (SPDX format) - uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0 + uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0.20.10 with: artifact-name: sbom.spdx.json format: spdx-json output-file: sbom.spdx.json - name: Generate SBOM (CycloneDX format) - uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0 + uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0.20.10 with: artifact-name: sbom.cdx.json format: cyclonedx-json diff --git a/pkg/workflow/action_sha_checker.go b/pkg/workflow/action_sha_checker.go index cf7f4eb1f60..3dbe5537f1a 100644 --- a/pkg/workflow/action_sha_checker.go +++ b/pkg/workflow/action_sha_checker.go @@ -42,9 +42,10 @@ func ExtractActionsFromLockFile(lockFilePath string) ([]ActionUsage, error) { return nil, fmt.Errorf("failed to parse lock file YAML: %w", err) } - // Regular expression to match uses: owner/repo@sha - // This matches: owner/repo@40-char-hex-sha or owner/repo/subpath@40-char-hex-sha - usesPattern := regexp.MustCompile(`([a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(?:/[a-zA-Z0-9_.-]+)*)@([0-9a-f]{40})`) + // Regular expression to match uses: owner/repo@sha with optional version comment + // This matches: owner/repo@40-char-hex-sha # version + // Captures: (1) repo, (2) sha, (3) version (optional) + usesPattern := regexp.MustCompile(`([a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(?:/[a-zA-Z0-9_.-]+)*)@([0-9a-f]{40})(?:\s*#\s*([^\s]+))?`) actions := make(map[string]ActionUsage) // Use map to deduplicate @@ -62,12 +63,19 @@ func ExtractActionsFromLockFile(lockFilePath string) ([]ActionUsage, error) { continue } - actionSHACheckerLog.Printf("Found action: %s@%s", repo, sha) - - // Try to determine the version tag from action_pins.json + // Extract version from comment if present (match[3]) version := "" - if pin, found := GetActionPinByRepo(repo); found { - version = pin.Version + if len(match) >= 4 && match[3] != "" { + version = match[3] + actionSHACheckerLog.Printf("Found action: %s@%s (version: %s)", repo, sha, version) + } else { + // Fallback: try to determine the version tag from action_pins.json + if pin, found := GetActionPinByRepo(repo); found { + version = pin.Version + actionSHACheckerLog.Printf("Found action: %s@%s (version from pins: %s)", repo, sha, version) + } else { + actionSHACheckerLog.Printf("Found action: %s@%s (no version)", repo, sha) + } } actions[repo+"@"+sha] = ActionUsage{ @@ -187,7 +195,7 @@ func ValidateActionSHAsInLockFile(lockFilePath string, cache *ActionCache, verbo actionSHACheckerLog.Print("Saved updated action cache") } // Provide suggestion to fix the issue - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("To update action SHAs, run: gh aw compile --validate")) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("To apply updated action SHAs, recompile with: gh aw compile")) if verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d action(s) with available updates", updateCount))) } diff --git a/pkg/workflow/action_sha_checker_test.go b/pkg/workflow/action_sha_checker_test.go index c3b5c75d8b4..a0a22846ff6 100644 --- a/pkg/workflow/action_sha_checker_test.go +++ b/pkg/workflow/action_sha_checker_test.go @@ -192,3 +192,66 @@ func TestExtractActionsFromLockFileInvalidFile(t *testing.T) { t.Error("Expected error when reading non-existent file, got nil") } } + +func TestExtractActionsFromLockFileWithVersionComments(t *testing.T) { + // Create a temporary lock file with version comments + tmpDir := testutil.TempDir(t, "test-*") + lockFile := filepath.Join(tmpDir, "test.lock.yml") + + lockContent := ` +name: Test Workflow +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + - name: Run tests + run: npm test +` + + if err := os.WriteFile(lockFile, []byte(lockContent), 0644); err != nil { + t.Fatalf("Failed to create test lock file: %v", err) + } + + // Extract actions + actions, err := ExtractActionsFromLockFile(lockFile) + if err != nil { + t.Fatalf("ExtractActionsFromLockFile failed: %v", err) + } + + // Verify we extracted the expected actions with versions + if len(actions) != 3 { + t.Errorf("Expected 3 actions, got %d", len(actions)) + } + + // Create a map to easily look up actions by repo + actionMap := make(map[string]ActionUsage) + for _, action := range actions { + actionMap[action.Repo] = action + } + + // Verify versions were extracted correctly from comments + tests := []struct { + repo string + expectedVersion string + }{ + {"actions/checkout", "v5"}, + {"actions/setup-node", "v6"}, + {"actions/github-script", "v7.0.1"}, + } + + for _, tt := range tests { + action, found := actionMap[tt.repo] + if !found { + t.Errorf("Expected to find action %s, but it was not extracted", tt.repo) + continue + } + + if action.Version != tt.expectedVersion { + t.Errorf("For %s: expected version %s, got %s", tt.repo, tt.expectedVersion, action.Version) + } + } +}