diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go index 04ec324cce..a68737476c 100644 --- a/pkg/cli/update_actions.go +++ b/pkg/cli/update_actions.go @@ -70,6 +70,9 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error { updateLog.Printf("Loaded %d action entries from actions-lock.json", len(actionsLock.Entries)) + // Per-invocation cache: key = "repo|version", avoids repeated API calls for the same action + cache := make(map[string]latestReleaseResult) + // Track updates var updatedActions []string var failedActions []string @@ -83,15 +86,23 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error { // When disableReleaseBump is set, only core actions (actions/*) bypass the --major flag. effectiveAllowMajor := !disableReleaseBump || allowMajor || isCoreAction(entry.Repo) - // Check for latest release - latestVersion, latestSHA, err := getLatestActionRelease(entry.Repo, entry.Version, effectiveAllowMajor, verbose) - if err != nil { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check %s: %v", entry.Repo, err))) + // Check for latest release, using the cache to avoid redundant API calls. + cacheKey := entry.Repo + "|" + entry.Version + result, cached := cache[cacheKey] + if !cached { + latestVersion, latestSHA, err := getLatestActionRelease(entry.Repo, entry.Version, effectiveAllowMajor, verbose) + if err != nil { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check %s: %v", entry.Repo, err))) + } + failedActions = append(failedActions, entry.Repo) + continue } - failedActions = append(failedActions, entry.Repo) - continue + result = latestReleaseResult{version: latestVersion, sha: latestSHA} + cache[cacheKey] = result } + latestVersion := result.version + latestSHA := result.sha // Check if update is available if latestVersion == entry.Version && latestSHA == entry.SHA { @@ -176,7 +187,7 @@ func getLatestActionRelease(repo, currentVersion string, allowMajor, verbose boo updateLog.Printf("Using base repository: %s for action: %s", baseRepo, repo) // Use gh CLI to get releases - output, err := workflow.RunGHCombined("Fetching releases...", "api", fmt.Sprintf("/repos/%s/releases", baseRepo), "--jq", ".[].tag_name") + output, err := workflow.RunGHCombined(fmt.Sprintf("Fetching releases for %s...", baseRepo), "api", fmt.Sprintf("/repos/%s/releases", baseRepo), "--jq", ".[].tag_name") if err != nil { // Check if this is an authentication error outputStr := string(output) diff --git a/pkg/cli/update_command_test.go b/pkg/cli/update_command_test.go index 03414c4eb3..c534151236 100644 --- a/pkg/cli/update_command_test.go +++ b/pkg/cli/update_command_test.go @@ -894,7 +894,7 @@ func TestResolveLatestRef_CommitSHA(t *testing.T) { // in authenticated environments it will succeed. Either outcome is // acceptable — the key invariant is that the SHA is correctly // identified (tested above) and the function does not panic. - _, _ = resolveLatestRef("test/repo", sha, false, false) + _, _ = resolveLatestRef("test/repo", sha, false, false, make(map[string]string)) } // TestResolveLatestRef_NotCommitSHA tests that non-SHA refs are handled appropriately diff --git a/pkg/cli/update_workflows.go b/pkg/cli/update_workflows.go index c51dab7a3f..14f7ef64ca 100644 --- a/pkg/cli/update_workflows.go +++ b/pkg/cli/update_workflows.go @@ -45,9 +45,12 @@ func UpdateWorkflows(workflowNames []string, allowMajor, force, verbose bool, en var failedUpdates []updateFailure // Update each workflow + // Per-invocation cache: key = "repo|currentRef", avoids repeated API calls for the same repo + releaseCache := make(map[string]string) + for _, wf := range workflows { updateLog.Printf("Updating workflow: %s (source: %s)", wf.Name, wf.SourceSpec) - if err := updateWorkflow(wf, allowMajor, force, verbose, engineOverride, noStopAfter, stopAfter, noMerge); err != nil { + if err := updateWorkflow(wf, allowMajor, force, verbose, engineOverride, noStopAfter, stopAfter, noMerge, releaseCache); err != nil { updateLog.Printf("Failed to update workflow %s: %v", wf.Name, err) failedUpdates = append(failedUpdates, updateFailure{ Name: wf.Name, @@ -156,7 +159,7 @@ func findWorkflowsWithSource(workflowsDir string, filterNames []string, verbose } // resolveLatestRef resolves the latest ref for a workflow source -func resolveLatestRef(repo, currentRef string, allowMajor, verbose bool) (string, error) { +func resolveLatestRef(repo, currentRef string, allowMajor, verbose bool, releaseCache map[string]string) (string, error) { updateLog.Printf("Resolving latest ref: repo=%s, currentRef=%s, allowMajor=%v", repo, currentRef, allowMajor) if verbose { @@ -166,7 +169,7 @@ func resolveLatestRef(repo, currentRef string, allowMajor, verbose bool) (string // Check if current ref is a tag (looks like a semantic version) if isSemanticVersionTag(currentRef) { updateLog.Print("Current ref is semantic version tag, resolving latest release") - return resolveLatestRelease(repo, currentRef, allowMajor, verbose) + return resolveLatestRelease(repo, currentRef, allowMajor, verbose, releaseCache) } // Check if current ref is a commit SHA (40-character hex string) @@ -256,15 +259,22 @@ func getLatestBranchCommitSHA(repo, branch string) (string, error) { } // resolveLatestRelease resolves the latest compatible release for a workflow source -func resolveLatestRelease(repo, currentRef string, allowMajor, verbose bool) (string, error) { +func resolveLatestRelease(repo, currentRef string, allowMajor, verbose bool, releaseCache map[string]string) (string, error) { updateLog.Printf("Resolving latest release for repo %s (current: %s, allowMajor=%v)", repo, currentRef, allowMajor) if verbose { fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Checking for latest release (current: %s, allow major: %v)", currentRef, allowMajor))) } + // Check cache before making an API call + cacheKey := repo + "|" + currentRef + if cached, ok := releaseCache[cacheKey]; ok { + updateLog.Printf("Cache hit for %s (current: %s): %s", repo, currentRef, cached) + return cached, nil + } + // Get all releases using gh CLI - output, err := workflow.RunGH("Fetching releases...", "api", fmt.Sprintf("/repos/%s/releases", repo), "--jq", ".[].tag_name") + output, err := workflow.RunGH(fmt.Sprintf("Fetching releases for %s...", repo), "api", fmt.Sprintf("/repos/%s/releases", repo), "--jq", ".[].tag_name") if err != nil { return "", fmt.Errorf("failed to fetch releases: %w", err) } @@ -282,6 +292,7 @@ func resolveLatestRelease(repo, currentRef string, allowMajor, verbose bool) (st if verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Current version is not valid, using latest release: "+latestRelease)) } + releaseCache[cacheKey] = latestRelease return latestRelease, nil } @@ -315,11 +326,12 @@ func resolveLatestRelease(repo, currentRef string, allowMajor, verbose bool) (st fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Found newer release: "+latestCompatible)) } + releaseCache[cacheKey] = latestCompatible return latestCompatible, nil } // updateWorkflow updates a single workflow from its source -func updateWorkflow(wf *workflowWithSource, allowMajor, force, verbose bool, engineOverride string, noStopAfter bool, stopAfter string, noMerge bool) error { +func updateWorkflow(wf *workflowWithSource, allowMajor, force, verbose bool, engineOverride string, noStopAfter bool, stopAfter string, noMerge bool, releaseCache map[string]string) error { updateLog.Printf("Updating workflow: name=%s, source=%s, force=%v, noMerge=%v", wf.Name, wf.SourceSpec, force, noMerge) if verbose { @@ -341,7 +353,7 @@ func updateWorkflow(wf *workflowWithSource, allowMajor, force, verbose bool, eng } // Resolve latest ref - latestRef, err := resolveLatestRef(sourceSpec.Repo, currentRef, allowMajor, verbose) + latestRef, err := resolveLatestRef(sourceSpec.Repo, currentRef, allowMajor, verbose, releaseCache) if err != nil { return fmt.Errorf("failed to resolve latest ref: %w", err) } diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 103020bbd7..e56bed4331 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -50,7 +50,7 @@ "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, - "actions/setup-dotnet@v4.3.1": { + "actions/setup-dotnet@v5.1.0": { "repo": "actions/setup-dotnet", "version": "v4.3.1", "sha": "67a3573c9a986a3f9c594539f4ab511d57bb3ce9"