From edbe379f758bbc0bdc81e070ab5b7d96afbd492b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:09:54 +0000 Subject: [PATCH 1/3] Initial plan From e73eda4a922d602d9f8e4fc011afa27c3a2722b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:44:12 +0000 Subject: [PATCH 2/3] fix: prune stale gh-aw-actions entries from actions-lock.json after compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a compiler update (e.g., v0.67.1 → v0.67.3), actions-lock.json accumulated stale entries for github/gh-aw-actions/* at old versions that were no longer referenced by any compiled workflow. Add PruneStaleGHAWEntries() to ActionCache that removes entries for the gh-aw-actions repository whose version doesn't match the current compiler version. Called automatically after compilation (before cache save) so that only the version actually used by compiled workflows survives. Pruning is skipped for dev/dirty builds (non-release versions) to avoid accidentally removing valid entries during development. Fixes github/gh-aw#2321 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/bb868d18-3505-4313-b83a-3f0fd273e126 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/cli/compile_file_operations.go | 1 + pkg/cli/compile_pipeline.go | 24 +++++++ pkg/workflow/action_cache.go | 45 +++++++++++++ pkg/workflow/action_cache_test.go | 101 +++++++++++++++++++++++++++++ pkg/workflow/compiler_types.go | 6 ++ 5 files changed, 177 insertions(+) diff --git a/pkg/cli/compile_file_operations.go b/pkg/cli/compile_file_operations.go index 8eb20b8047c..5b0cdc82f99 100644 --- a/pkg/cli/compile_file_operations.go +++ b/pkg/cli/compile_file_operations.go @@ -221,6 +221,7 @@ func compileModifiedFilesWithDependencies(compiler *workflow.Compiler, depGraph successCount := stats.Total - stats.Errors if actionCache != nil { + pruneStaleActionCacheEntries(compiler, actionCache) if err := actionCache.Save(); err != nil { compileHelpersLog.Printf("Failed to save action cache: %v", err) if verbose { diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index 9121b88c6a0..d666c298b60 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -475,6 +475,9 @@ func runPostProcessing( // to check for expires fields, so we skip it when compiling specific files to avoid // unnecessary parsing and warnings from unrelated workflows + // Prune stale gh-aw-actions entries before saving + pruneStaleActionCacheEntries(compiler, actionCache) + // Save action cache (errors are logged but non-fatal) _ = saveActionCache(actionCache, config.Verbose) @@ -517,12 +520,33 @@ func runPostProcessingForDirectory( } } + // Prune stale gh-aw-actions entries before saving + pruneStaleActionCacheEntries(compiler, actionCache) + // Save action cache (errors are logged but non-fatal) _ = saveActionCache(actionCache, config.Verbose) return nil } +// pruneStaleActionCacheEntries removes stale gh-aw-actions entries from the +// action cache whose version does not match the compiler's current version. +// This prevents actions-lock.json from accumulating entries for old compiler +// releases that are no longer referenced by any compiled workflow. +func pruneStaleActionCacheEntries(compiler *workflow.Compiler, actionCache *workflow.ActionCache) { + if actionCache == nil { + return + } + + // Determine the effective version: use actionTag if set, otherwise compiler version + version := compiler.GetActionTag() + if version == "" { + version = compiler.GetVersion() + } + + actionCache.PruneStaleGHAWEntries(version, compiler.EffectiveActionsRepo()) +} + // outputResults outputs compilation results in the requested format func outputResults( stats *CompilationStats, diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go index 48ed232e494..2403c420cb0 100644 --- a/pkg/workflow/action_cache.go +++ b/pkg/workflow/action_cache.go @@ -437,6 +437,51 @@ func (c *ActionCache) deduplicateEntries() { } } +// PruneStaleGHAWEntries removes entries from the cache for the gh-aw-actions +// repository whose version does not match the current compiler version. +// +// When the compiler is updated (e.g., from v0.67.1 to v0.67.3), previously +// compiled workflows referenced setup@v0.67.1 but the new compiler pins to +// setup@v0.67.3. Without pruning, both entries survive in actions-lock.json, +// leaving a stale entry that is never referenced by any compiled lock file. +// +// Only prunes when the current version is a release version (starts with "v"). +// Dev builds, empty versions, and other non-release versions are skipped to +// avoid accidentally removing valid entries during development. +// +// Parameters: +// - currentVersion: the compiler version that is currently in use (e.g., "v0.67.3") +// - actionsRepoPrefix: the org/repo prefix for gh-aw-actions (e.g., "github/gh-aw-actions") +func (c *ActionCache) PruneStaleGHAWEntries(currentVersion string, actionsRepoPrefix string) { + if currentVersion == "" || actionsRepoPrefix == "" { + return + } + // Only prune for release versions (e.g., "v0.67.3"), not dev/dirty builds + if !strings.HasPrefix(currentVersion, "v") { + return + } + + var toDelete []string + for key, entry := range c.Entries { + if !strings.HasPrefix(entry.Repo, actionsRepoPrefix+"/") { + continue + } + if entry.Version != currentVersion { + actionCacheLog.Printf("Pruning stale gh-aw-actions entry: %s (version %s != current %s)", key, entry.Version, currentVersion) + toDelete = append(toDelete, key) + } + } + + for _, key := range toDelete { + delete(c.Entries, key) + } + + if len(toDelete) > 0 { + c.dirty = true + actionCacheLog.Printf("Pruned %d stale gh-aw-actions entries, %d entries remaining", len(toDelete), len(c.Entries)) + } +} + // isMorePreciseVersion returns true if v1 is more precise than v2 // For example: "v4.3.0" is more precise than "v4" func isMorePreciseVersion(v1, v2 string) bool { diff --git a/pkg/workflow/action_cache_test.go b/pkg/workflow/action_cache_test.go index f5bf5acb588..ba709df648f 100644 --- a/pkg/workflow/action_cache_test.go +++ b/pkg/workflow/action_cache_test.go @@ -612,3 +612,104 @@ func TestActionCacheInputs(t *testing.T) { t.Error("Expected created entry to have the given inputs") } } + +// TestPruneStaleGHAWEntries tests that stale gh-aw-actions entries are pruned +func TestPruneStaleGHAWEntries(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + cache := NewActionCache(tmpDir) + + // Set up a scenario that mirrors the bug: + // - An old setup action entry from a previous compiler version + // - A current setup action entry from the current compiler version + // - A non-gh-aw-actions entry that should be preserved + cache.Set("github/gh-aw-actions/setup", "v0.67.1", "sha_old") + cache.Set("github/gh-aw-actions/setup", "v0.67.3", "sha_new") + cache.Set("actions/checkout", "v5", "sha_checkout") + + if len(cache.Entries) != 3 { + t.Fatalf("Expected 3 entries before pruning, got %d", len(cache.Entries)) + } + + // Prune stale entries for version v0.67.3 + cache.PruneStaleGHAWEntries("v0.67.3", "github/gh-aw-actions") + + // Should have 2 entries: current setup + checkout + if len(cache.Entries) != 2 { + t.Errorf("Expected 2 entries after pruning, got %d", len(cache.Entries)) + } + + // The old setup entry should be gone + if _, exists := cache.Entries["github/gh-aw-actions/setup@v0.67.1"]; exists { + t.Error("Expected stale setup@v0.67.1 to be pruned") + } + + // The current setup entry should remain + if _, exists := cache.Entries["github/gh-aw-actions/setup@v0.67.3"]; !exists { + t.Error("Expected current setup@v0.67.3 to remain") + } + + // Non-gh-aw-actions entries should remain + if _, exists := cache.Entries["actions/checkout@v5"]; !exists { + t.Error("Expected actions/checkout@v5 to remain") + } +} + +// TestPruneStaleGHAWEntriesMultipleActions tests pruning with multiple gh-aw-actions +func TestPruneStaleGHAWEntriesMultipleActions(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + cache := NewActionCache(tmpDir) + + // Multiple gh-aw-actions at the old version, plus one at the current version + cache.Set("github/gh-aw-actions/setup", "v0.67.1", "sha1") + cache.Set("github/gh-aw-actions/setup", "v0.67.3", "sha2") + cache.Set("github/gh-aw-actions/create-issue", "v0.67.1", "sha3") + cache.Set("github/gh-aw-actions/create-issue", "v0.67.3", "sha4") + cache.Set("actions/checkout", "v5", "sha5") + + cache.PruneStaleGHAWEntries("v0.67.3", "github/gh-aw-actions") + + // Should keep only the v0.67.3 entries + checkout + if len(cache.Entries) != 3 { + t.Errorf("Expected 3 entries after pruning, got %d", len(cache.Entries)) + } + + if _, exists := cache.Entries["github/gh-aw-actions/setup@v0.67.1"]; exists { + t.Error("Expected stale setup@v0.67.1 to be pruned") + } + if _, exists := cache.Entries["github/gh-aw-actions/create-issue@v0.67.1"]; exists { + t.Error("Expected stale create-issue@v0.67.1 to be pruned") + } +} + +// TestPruneStaleGHAWEntriesNoOp tests that pruning is a no-op for non-release or empty versions +func TestPruneStaleGHAWEntriesNoOp(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + cache := NewActionCache(tmpDir) + + cache.Set("github/gh-aw-actions/setup", "v0.67.1", "sha1") + cache.Set("actions/checkout", "v5", "sha2") + + // Should be a no-op for "dev" version (not a release) + cache.PruneStaleGHAWEntries("dev", "github/gh-aw-actions") + if len(cache.Entries) != 2 { + t.Errorf("Expected 2 entries (no pruning for dev), got %d", len(cache.Entries)) + } + + // Should be a no-op for empty version + cache.PruneStaleGHAWEntries("", "github/gh-aw-actions") + if len(cache.Entries) != 2 { + t.Errorf("Expected 2 entries (no pruning for empty version), got %d", len(cache.Entries)) + } + + // Should be a no-op for empty prefix + cache.PruneStaleGHAWEntries("v0.67.3", "") + if len(cache.Entries) != 2 { + t.Errorf("Expected 2 entries (no pruning for empty prefix), got %d", len(cache.Entries)) + } + + // Should be a no-op for dirty dev builds (e.g., "abc123-dirty") + cache.PruneStaleGHAWEntries("abc123-dirty", "github/gh-aw-actions") + if len(cache.Entries) != 2 { + t.Errorf("Expected 2 entries (no pruning for dirty build), got %d", len(cache.Entries)) + } +} diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 0245de556d7..2ed42e82c39 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -247,6 +247,12 @@ func (c *Compiler) effectiveActionsRepo() string { return GitHubActionsOrgRepo } +// EffectiveActionsRepo returns the actions repository used for action mode references. +// Returns the override if set, otherwise returns the default GitHubActionsOrgRepo. +func (c *Compiler) EffectiveActionsRepo() string { + return c.effectiveActionsRepo() +} + // GetVersion returns the version string used by the compiler func (c *Compiler) GetVersion() string { return c.version From f7bac524d65b47c87bbb423c99d7ef82e4d342b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:47:04 +0000 Subject: [PATCH 3/3] fix: address code review feedback - handle v*-dirty builds and add comments Agent-Logs-Url: https://github.com/github/gh-aw/sessions/bb868d18-3505-4313-b83a-3f0fd273e126 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/cli/compile_pipeline.go | 4 +++- pkg/workflow/action_cache.go | 4 ++-- pkg/workflow/action_cache_test.go | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index d666c298b60..561d8d1a605 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -538,7 +538,9 @@ func pruneStaleActionCacheEntries(compiler *workflow.Compiler, actionCache *work return } - // Determine the effective version: use actionTag if set, otherwise compiler version + // Determine the effective version: actionTag takes precedence when explicitly + // set (e.g., via --action-tag for testing against a specific release), otherwise + // fall back to the compiler's built-in version from the binary. version := compiler.GetActionTag() if version == "" { version = compiler.GetVersion() diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go index 2403c420cb0..67827ee8629 100644 --- a/pkg/workflow/action_cache.go +++ b/pkg/workflow/action_cache.go @@ -456,8 +456,8 @@ func (c *ActionCache) PruneStaleGHAWEntries(currentVersion string, actionsRepoPr if currentVersion == "" || actionsRepoPrefix == "" { return } - // Only prune for release versions (e.g., "v0.67.3"), not dev/dirty builds - if !strings.HasPrefix(currentVersion, "v") { + // Only prune for clean release versions (e.g., "v0.67.3"), not dev/dirty builds + if !strings.HasPrefix(currentVersion, "v") || strings.Contains(currentVersion, "-") { return } diff --git a/pkg/workflow/action_cache_test.go b/pkg/workflow/action_cache_test.go index ba709df648f..b9c5e86476d 100644 --- a/pkg/workflow/action_cache_test.go +++ b/pkg/workflow/action_cache_test.go @@ -712,4 +712,10 @@ func TestPruneStaleGHAWEntriesNoOp(t *testing.T) { if len(cache.Entries) != 2 { t.Errorf("Expected 2 entries (no pruning for dirty build), got %d", len(cache.Entries)) } + + // Should be a no-op for dirty release builds (e.g., "v0.67.3-dirty") + cache.PruneStaleGHAWEntries("v0.67.3-dirty", "github/gh-aw-actions") + if len(cache.Entries) != 2 { + t.Errorf("Expected 2 entries (no pruning for dirty release build), got %d", len(cache.Entries)) + } }