| name | actions-pin |
| description | Audit and upgrade the SHA-pinned GitHub Actions referenced by `.github/workflows/**` and `.github/actions/**`, with a supply-chain quarantine and automatic step-back to the previous aged version. Default is minor-only (stay within current majors); pass `major` to also bump major versions; pass a number or `days=N` to set the exclusion window (`PIN_ACTIONS_MIN_AGE_DAYS`, default 14). The version source of truth is the trailing tag comment on each `uses: owner/repo@<sha> # <tag>` line; `.github/actions-pin.toml` is the resolved tag→SHA lockfile, driven by `make pin-actions-resolve` / `pin-actions-apply` / `pin-actions-check` (backed by `scripts/pin-actions`). For each target major the skill prefers the moving major tag when its latest is aged, else steps back to the newest exact version older than the exclusion window, else holds — so a freshly published (possibly compromised) release is never adopted. Verifies with `make pin-actions-check` + `make actions-lint`. Major bumps additionally verify `with:` input compatibility and are held (not auto-applied) on a breaking change. Sibling of `tools-upgrade` (which covers `mise.toml`, not Actions). Use on a routine cadence or after an Actions security advisory. |
GitHub Actions Pin Upgrade
This skill audits and upgrades the SHA-pinned GitHub Actions in .github/workflows/** and .github/actions/**, with a supply-chain quarantine gate plus an automatic step-back: releases newer than the exclusion window (PIN_ACTIONS_MIN_AGE_DAYS, default 14) are never adopted; instead the skill pins the newest version that is already older than the window. A freshly-published (possibly compromised) version is thus never pulled in before upstream has time to detect and revoke it.
It is the sibling of tools-upgrade — that skill covers mise.toml [tools]; this one covers GitHub Actions pins. They share the same quarantine philosophy but operate on different SSOTs.
A Japanese reference translation is available at SKILL.ja.md in the same directory (not loaded as a skill; for human reference only).
How Pinning Works in This Repo
Read this before doing anything — the mechanism determines every step below.
- Each external reference is pinned as
uses: owner/repo[/sub]@<40-hex-sha> # <tag>. The tag in the trailing comment is the version source of truth, not the @<sha> part.
.github/actions-pin.toml is the lockfile: "owner/repo@<tag>" = "<sha>" (SSOT for apply, regenerated by resolve).
make pin-actions-resolve — reads the comment tag of every uses:, resolves it to a commit SHA via git ls-remote (annotated tags are dereferenced to the commit), applies the quarantine, and rewrites the lockfile. Env PIN_ACTIONS_MIN_AGE_DAYS (default 14) controls the gate; when a moving-tag's latest SHA is inside the window it keeps the existing pin (its own built-in step-back for moving tags).
make pin-actions-apply — rewrites each uses: @<sha> from the lockfile, keeping # <tag>.
make pin-actions-check — verifies pins match the lockfile without writing (CI / hook).
- A moving major tag (
# v6) auto-advances to the latest within-major release on the next resolve. A same-major refresh is therefore just resolve + apply. A major bump requires editing the comment tag (# v6 → # v7) first. An exact-version comment (# 0.35.0) never moves on resolve; bumping it requires editing the comment.
When to Use
Use this skill when:
- Routine periodic refresh of pinned Actions SHAs (default minor-only mode)
- Bumping Actions to newer major versions (
major argument)
- After a GitHub Actions security advisory
Do NOT use this skill for:
mise.toml tool versions — use /tools-upgrade
- Go itself — use
/go-upgrade
- Go module dependencies — use
make tidy-lib
- Local composite actions (
uses: ./...) — they have no @ref and are not pinned
Arguments
Parse the invocation arguments (order-independent); they drive behavior so the strategy is NOT asked interactively:
| Token | Meaning | Default |
|---|
major (or --major) | Also bump to newer major versions. Absent → minor-only (stay within current majors). | minor-only |
a bare integer, or days=N (or --days N) | Exclusion window in days = PIN_ACTIONS_MIN_AGE_DAYS, used both by the skill's step-back computation and passed to make pin-actions-resolve. | 14 |
Examples: /actions-pin (minor, 14d) · /actions-pin major (minor+major, 14d) · /actions-pin major 30 (major, 30d) · /actions-pin 21 (minor, 21d).
The exclusion days must be a non-negative integer. 0 disables the quarantine (adopt even brand-new releases) — only honor it when the user explicitly passes 0, and surface the supply-chain risk.
AI Modification Scope
Per the "Exception: Skill Execution" clause in CLAUDE.md, the following paths may be modified while this skill runs:
.github/workflows/*.{yml,yaml} — the uses: comment tags + the @<sha> (written by make pin-actions-apply)
.github/actions/*/action.{yml,yaml} — same
.github/actions-pin.toml — the lockfile (written by make pin-actions-resolve)
The following remain protected even during skill execution:
AGENTS.md / CLAUDE.md
- Generated files (
**/*.gen.go, *.sql.go, *_mock.go, **/openapi.gen.yaml, generated content under docs/)
- Any file unrelated to the pin upgrade. Do NOT change
with: inputs, step logic, or scripts/pin-actions — if a bump needs an input change, surface it and stop.
The Target-Selection Rule (core of this skill)
For each action, the target major M is its current major in minor-only mode, or the latest available major in major mode. Pick the pin for M as follows (N = exclusion days, cutoff = now - N days):
- Moving tag, aged — if a moving major tag
vM exists and its latest resolved SHA is older than the cutoff → pin # vM (preferred: keeps auto-advancing on future runs).
- Step back to previous aged exact — else (the
vM head is inside the window, or no moving vM tag exists) → choose the newest exact release vM.x.y whose published_at is older than the cutoff, and pin # vM.x.y. This is the "use the one-previous version" behavior — it reaches M now while honoring the quarantine.
- Hold — if no release in
M is older than the cutoff (e.g. M is brand-new and only vM.0.0 exists, still fresh) → leave the action unchanged and report it as held.
Notes on the rule:
- In minor-only mode,
M is the current major, so step 1 normally applies and make pin-actions-resolve does the work (it keeps the existing pin when the within-major head is fresh — equivalent to step 2). The skill only edits a comment tag when it must force a step-back to an exact version (a just-released patch makes the head fresh) or to bump an exact-pinned action's patch line.
- In major mode,
M is the new major with no existing lockfile key, so a fresh vM head would be skipped by resolve (→ apply "missing"). Step 2 (exact step-back) is what makes the new major adoptable now; step 3 holds it otherwise.
- An exact step-back (step 2) deviates from the moving-major convention. Record in the commit that it can be returned to
# vM once vM ages. sigstore/cosign-installer (no moving v4 tag) is a permanent step-2 case.
Execution Steps
0. Pre-flight: vendor consistency + token
make pin-actions-* runs go run ./scripts/pin-actions, which compiles against vendor/. vendor/ is gitignored, so a parallel checkout (e.g. a go.mod upgrade branch) can leave it inconsistent with the current branch's go.mod, and the go run fails with vendor/modules.txt: ... inconsistent. If so, run go mod vendor once to re-sync, then proceed. Also export a token so resolve (GitHub API for release dates) is not rate-limited:
export GITHUB_TOKEN="$(gh auth token)"
1. Parse Arguments and Inventory
Parse the arguments (above) into <MODE> (minor / major) and <N> (exclusion days). Then:
- Read
.github/actions-pin.toml for the current tag → sha set.
- Grep
uses: across .github/workflows/ and .github/actions/ to map each external action to its file locations and current comment tag (note actions referenced in multiple files).
2. Query Releases and Compute the Target Pin
For each distinct external action, fetch its release list with dates (gh api repos/<owner>/<repo>/releases -q '.[] | "\(.tag_name)\t\(.published_at)\t\(.prerelease)"'; skip pre-releases). Determine the target major M per <MODE>, then apply the Target-Selection Rule to compute one of: pin # vM / pin exact # vM.x.y (step-back) / hold. Account for tag-format changes across majors (e.g. aquasecurity/trivy-action went 0.35.0 → v0.36.0) — the comment tag must match the upstream tag string exactly or resolve fails with ref not found. For step 1 candidates also confirm the moving vM tag actually exists (git ls-remote … vM); if absent, fall to step 2.
3. Verify with: for Major Bumps
resolve / apply / actionlint catch syntax, NOT semantic input changes. For every action whose major changes, read its release notes / action.yml and compare against every with: block this repo uses. Examples seen: peter-evans/create-pull-request renamed git-token→branch-token (v8); actions/upload-pages-artifact v5 excludes dotfiles and requires deploy-pages@v4+. If the repo's actual inputs remain compatible → keep the bump. If a breaking input change applies → hold the action and report the required change; do not auto-apply. (Minor-only refreshes within a major skip this check.)
4. Display Plan and Confirm
Print a Japanese summary: bumps to apply (moving # vM / exact step-back # vM.x.y, each with the chosen version + its age), held items (with reason: still-fresh new major / breaking with: / no aged release), and unchanged. Then confirm the concrete set via AskUserQuestion (multiSelect: true when several independent bumps are offered) so the step-back and hold decisions are visible before any write.
5. Edit Comment Tags
For each approved bump, edit the trailing comment tag of the relevant uses: line(s) to the computed target (# v7, or exact # v4.1.0). Leave the @<sha> as-is (apply rewrites it). When an action appears in multiple files with an identical uses: line, a per-file replace-all is appropriate; when several distinct actions share the same old comment in one file (e.g. three # v3 lines), match the full unique uses: line so only the intended one changes. Leave held / unchanged actions untouched.
6. Resolve → Apply
export GITHUB_TOKEN="$(gh auth token)"
make pin-actions-resolve PIN_ACTIONS_MIN_AGE_DAYS=<N>
make pin-actions-apply
resolve re-resolves every referenced tag (current-major actions also get their latest aged within-major SHA) and prints ⚠️ ... 既存ピンを維持 for any whose within-major head is inside the window — expected, not a failure. If resolve aborts with ref "vN" が見つかりません, the moving-major tag does not exist — that action should have been a step-2 exact pin; fix and re-run. If it aborts on vendor inconsistency, run step 0's go mod vendor.
7. Verify
make pin-actions-check
make actions-lint
Report OK / FAIL per command. Do NOT auto-roll-back on failure — the user decides.
8. Final Report
Summarize: actions bumped (moving / exact step-back), actions SHA-refreshed, actions held (with reason), verification result. List any exact-version pins introduced so the user knows to revisit them once aged. Do NOT commit, stage, or push — the user runs /commit (these changes are CI:-prefixed) manually.
Notes
- Step-back is the default response to the quarantine, not a hold. Holding happens only when no aged release exists in the target major. This is the behavior the arguments are designed around.
- Quarantine vs new majors: the gate keys off SHA age, and a new major has no prior lockfile entry, so a fresh major's moving tag is skipped by
resolve until it ages — which is exactly why step 2 pins an aged exact version instead.
- Not every action has a moving major tag. Always
git ls-remote the vM tag before assuming # vM resolves (sigstore/cosign-installer is the known exception → permanent exact pin).
actionlint ≠ semantic safety. It validates workflow syntax, not whether a bumped action's inputs/behavior still match usage. The with: review (step 3) is mandatory for major bumps.
- annotated-tag deref:
resolve returns the dereferenced commit SHA (refs/tags/vM^{}), so the lockfile SHA can differ from a naive git ls-remote vM line.
GITHUB_TOKEN: resolve queries the GitHub API for release dates; without a token it hits the 60 req/h anonymous limit. Export gh auth token.
- Idempotency: a second run shows everything pinned;
pin-actions-check passes.
- The skill never auto-pushes.
Checklist
Confirm before reporting completion: