Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions pkg/cli/update_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/update_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 19 additions & 7 deletions pkg/cli/update_workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading