From 94a4ff7610b3d17e53ba97ad4389bd1052f3ad35 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 17 Jun 2026 00:53:15 +0100 Subject: [PATCH 1/4] perf(workflow): check embedded action pins before gh-api network call in ActionResolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ActionResolver.ResolveSHA method previously fell through to a gh-api subprocess call whenever the on-disk ActionCache had a miss. For builtin actions like actions/github-script@v9, this caused a ~1.2s subprocess invocation even though the exact SHA is already embedded in action_pins.json as v9.0.0 (semver-compatible with the requested v9 tag). This fix adds an embedded-pin check between the disk-cache lookup and the network call. For any action whose requested version is semver-compatible with an embedded pin, the resolver now returns the embedded SHA immediately without spawning a gh-api process. Impact: - TestCompileWorkflow_PerformanceRegression/small_workflow: 1.4s → 50ms (first compilation in a test run), then 2ms (subsequent runs with disk-cache hit) - TestCompileWorkflow_PerformanceRegression/medium_workflow: 1.2s → 2ms - Full pkg/workflow test suite: significantly faster for all tests that compile workflows with builtin actions --- pkg/workflow/action_resolver.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 118e5e2c7cd..2028120c3f2 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -6,8 +6,10 @@ import ( "strings" "time" + "github.com/github/gh-aw/pkg/actionpins" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/semverutil" ) var resolverLog = logger.New("workflow:action_resolver") @@ -47,7 +49,21 @@ func (r *ActionResolver) ResolveSHA(ctx context.Context, repo, version string) ( return sha, nil } - resolverLog.Printf("Cache miss for %s@%s, querying GitHub API", repo, version) + resolverLog.Printf("Cache miss for %s@%s, checking embedded action pins", repo, version) + + // Check embedded action pins for a semver-compatible version before making + // a network call. The embedded pins are the source-of-truth for known versions + // and are always available without network access. This avoids a ~1s gh-api + // subprocess for any action that is already covered by the embedded pin set. + for _, pin := range actionpins.GetActionPinsByRepo(repo) { + if semverutil.IsCompatible(pin.Version, version) { + resolverLog.Printf("Embedded pin hit for %s@%s → %s (%s)", repo, version, pin.SHA, pin.Version) + r.cache.Set(repo, version, pin.SHA) + return pin.SHA, nil + } + } + + resolverLog.Printf("No embedded pin for %s@%s, querying GitHub API", repo, version) resolverLog.Printf("This may take a moment as we query GitHub API at /repos/%s/git/ref/tags/%s", gitutil.ExtractBaseRepo(repo), version) // Resolve using GitHub CLI From 6aa755b08d5faddcb762d71c0439bf08acdd3d19 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 17 Jun 2026 01:06:35 +0100 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkg/workflow/action_resolver.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 2028120c3f2..9af1e389434 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -55,12 +55,23 @@ func (r *ActionResolver) ResolveSHA(ctx context.Context, repo, version string) ( // a network call. The embedded pins are the source-of-truth for known versions // and are always available without network access. This avoids a ~1s gh-api // subprocess for any action that is already covered by the embedded pin set. + requested := semverutil.EnsureVPrefix(version) + requestedVer := semverutil.ParseVersion(requested) + requestedIsPrecise := requestedVer != nil && requestedVer.IsPreciseVersion() + for _, pin := range actionpins.GetActionPinsByRepo(repo) { - if semverutil.IsCompatible(pin.Version, version) { - resolverLog.Printf("Embedded pin hit for %s@%s → %s (%s)", repo, version, pin.SHA, pin.Version) - r.cache.Set(repo, version, pin.SHA) - return pin.SHA, nil + pinVersion := semverutil.EnsureVPrefix(pin.Version) + if requestedIsPrecise { + if pinVersion != requested { + continue + } + } else if !semverutil.IsCompatible(pinVersion, requested) { + continue } + + resolverLog.Printf("Embedded pin hit for %s@%s → %s (%s)", repo, version, pin.SHA, pin.Version) + r.cache.Set(repo, version, pin.SHA) + return pin.SHA, nil } resolverLog.Printf("No embedded pin for %s@%s, querying GitHub API", repo, version) From ae230f0d8b53c59e6c95995f17ea6084602ad2b4 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 17 Jun 2026 01:21:57 +0100 Subject: [PATCH 3/4] fix: don't write on-disk cache for embedded pin hits in ActionResolver The previous change called r.cache.Set() when returning an embedded-pin hit, marking the on-disk ActionCache as dirty. This caused actions-lock.json to be written into mounted volumes when compiling inside Docker containers (e.g. the Alpine CI test). Since Docker containers run as root, the file was owned by root:root, preventing the host runner's cleanup step from deleting it. The embedded pins are always available in memory (action_pins.json is embedded at compile time), so there is no value in persisting them to the on-disk cache. Remove the r.cache.Set() call from the embedded-pin fast path. --- pkg/workflow/action_resolver.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 9af1e389434..c83c767bfc4 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -70,7 +70,10 @@ func (r *ActionResolver) ResolveSHA(ctx context.Context, repo, version string) ( } resolverLog.Printf("Embedded pin hit for %s@%s → %s (%s)", repo, version, pin.SHA, pin.Version) - r.cache.Set(repo, version, pin.SHA) + // Note: we intentionally do NOT call r.cache.Set() here. The embedded pins + // are always available in memory so there is nothing to persist, and writing + // to the on-disk cache would create root-owned files when compiling inside + // Docker containers (e.g. the Alpine CI test), preventing cleanup by the host. return pin.SHA, nil } From 5548abd9cb48375b4a49a28d445d39264c61bd70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:03:19 +0000 Subject: [PATCH 4/4] style: fix comment indentation in action_resolver.go (gofmt) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/action_resolver.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index c83c767bfc4..f586dda6011 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -70,10 +70,10 @@ func (r *ActionResolver) ResolveSHA(ctx context.Context, repo, version string) ( } resolverLog.Printf("Embedded pin hit for %s@%s → %s (%s)", repo, version, pin.SHA, pin.Version) - // Note: we intentionally do NOT call r.cache.Set() here. The embedded pins - // are always available in memory so there is nothing to persist, and writing - // to the on-disk cache would create root-owned files when compiling inside - // Docker containers (e.g. the Alpine CI test), preventing cleanup by the host. + // Note: we intentionally do NOT call r.cache.Set() here. The embedded pins + // are always available in memory so there is nothing to persist, and writing + // to the on-disk cache would create root-owned files when compiling inside + // Docker containers (e.g. the Alpine CI test), preventing cleanup by the host. return pin.SHA, nil }