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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# actupdate

`actupdate` updates GitHub Action references in workflow YAML files to the latest
stable major version.
eligible stable version.

## What It Does

Expand Down Expand Up @@ -76,10 +76,11 @@ Each release asset is named like `actupdate_linux_amd64.tar.gz` and contains the

- Only stable semver tags are considered
- Pre-release tags such as `-rc`, `-beta`, and `-alpha` are ignored
- Updates only move to the latest stable major
- Updates move to the latest eligible stable version
- `--cooldown-days` can exclude newer tags until they have aged past the
configured threshold
- Same-major patch or minor bumps are not applied in v1
- When a newer major exists, moving major tags such as `v6` are preferred over
exact tags such as `v6.2.1`
Comment on lines +82 to +83
- If any candidate update cannot be verified, the tool prints the failures and
does not rewrite any files

Expand Down
11 changes: 3 additions & 8 deletions cmd/actupdate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ func useColor(out io.Writer) bool {
func buildReport(ctx context.Context, scans []workflows.FileScan, client *gh.Client, cooldown time.Duration) (plan.Report, []workflows.Change, bool, error) {
report := plan.Report{}
var changes []workflows.Change
repoResults := map[string]repoOutcome{}
hadVerificationFailure := false

for _, scan := range scans {
Expand Down Expand Up @@ -254,20 +253,16 @@ func buildReport(ctx context.Context, scans []workflows.FileScan, client *gh.Cli
continue
}

currentMajor, err := actionspec.ParseMajor(spec.Ref)
currentVersion, err := actionspec.ParseStableVersion(spec.Ref)
if err != nil {
entry.Status = plan.StatusSkipped
entry.Reason = "non-semver ref"
report.Add(entry)
continue
}

outcome, ok := repoResults[spec.Repo]
if !ok {
resolution, resolveErr := client.ResolveLatestMajor(ctx, spec.Repo, currentMajor, cooldown)
outcome = repoOutcome{Resolution: resolution, Err: resolveErr}
repoResults[spec.Repo] = outcome
}
resolution, resolveErr := client.ResolveLatestStable(ctx, spec.Repo, currentVersion, cooldown)
outcome := repoOutcome{Resolution: resolution, Err: resolveErr}

if outcome.Err != nil {
entry.Status = plan.StatusError
Expand Down
41 changes: 41 additions & 0 deletions cmd/actupdate/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,47 @@ func TestRunCooldownDaysLeavesTooNewMajorUnchanged(t *testing.T) {
}
}

func TestRunUpdatesOlderExactTagEvenWhenSameRepoHasNewerRef(t *testing.T) {
repo := t.TempDir()
workflowDir := filepath.Join(repo, ".github", "workflows")
if err := os.MkdirAll(workflowDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
workflowPath := filepath.Join(workflowDir, "build_wheels.yml")
original := "steps:\n - uses: pypa/cibuildwheel@v3.3\n - uses: pypa/cibuildwheel@v3.0\n"
if err := os.WriteFile(workflowPath, []byte(original), 0o644); err != nil {
t.Fatalf("write workflow: %v", err)
}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/repos/pypa/cibuildwheel/tags" {
http.NotFound(w, r)
return
}
fmt.Fprint(w, `[{"name":"v3.3"},{"name":"v3.0"},{"name":"v2.9"}]`)
}))
defer server.Close()

var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := run([]string{"--repo", repo, "--yes"}, strings.NewReader(""), &stdout, &stderr, server.Client(), server.URL)
if exitCode != exitOK {
t.Fatalf("expected exit 0, got %d stderr=%s", exitCode, stderr.String())
}

updated, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("read workflow: %v", err)
}
got := string(updated)
if strings.Count(got, "pypa/cibuildwheel@v3.3") != 2 {
t.Fatalf("expected both refs to end at v3.3, got %q", got)
}
if !strings.Contains(stdout.String(), "pypa/cibuildwheel@v3.0 -> @v3.3") {
t.Fatalf("expected same-major upgrade in output, got %q", stdout.String())
}
}

func TestRunVerificationFailurePreventsWrites(t *testing.T) {
repo := t.TempDir()
workflowDir := filepath.Join(repo, ".github", "workflows")
Expand Down
Loading