Policy
A policy decides, for each discovered uses: ref, whether it may move and into what form it is rewritten. It has three independent fields (packages/core/src/policy.ts):
| Field | Values | Meaning |
|---|---|---|
track | keep | major | exact | Whether/how far the ref may move. |
pin | keep | tag | sha | The form the rewritten ref takes. |
bump | patch | minor | major | latest | Upper bound on semver magnitude. |
Default policy: track: "major", pin: "keep", bump: "major".
track — whether the ref moves
keep: the ref is never version-bumped. The latest-tag lookup is skipped entirely (if (policy.track !== 'keep')). Combined withpin: "sha"this still pins the current ref to its SHA — it just won't change which version you're on.major: bump, but keep the user's granularity — a major-only ref (v3) stays major-only (→ v4) when such a moving tag exists.exact: bump and pin to the exact newest full tag (v3 → v4.2.1), never the major-only moving tag.
The magnitude limit (how far a bump may go) is
bump/withinCap;trackcontrols the form of the written ref.keepshort-circuits (no version change).majorvsexactdiffer inrenderTarget: onlymajorcollapses to avNmoving tag.
bump — magnitude cap (withinCap)
A candidate tag is only eligible if it is strictly greater than the current version and within the cap relative to the current version:
bump | Eligible candidate |
|---|---|
patch | same major and same minor |
minor | same major (any minor/patch) |
major | any (no constraint) |
latest | any (no constraint) |
Semver comparison uses Bun.semver; missing minor/patch count as 0; prereleases are excluded from the candidate set (semverTags() skips any tag with a prerelease).
pin — rewrite form
keep: rewrite as a tag/branch string (the bumped tag, or unchanged).sha: rewrite to the immutable 40-char commit SHA, with the tag string written as a trailing comment (tagComment).tag: the enum value exists in the schema; the resolver's pin branches arepin === 'sha'vs everything else, sotagbehaves likekeep(tag-form output) at the evaluation layer.
Granularity preservation (renderTarget)
When a semver ref is bumped, the user's granularity is preserved:
- If the current ref is major-only (e.g.
v3— parsed as semver withminor === null) and a matching major-only tag exists in the repo (e.g.v4), the rewrite stays major-only:v3→v4. The leadingvis kept iff the original had one. - Otherwise the rewrite uses the latest tag's full original string (e.g.
v3.1.0→v4.2.1).
Written-ref SHA (the "D2" behaviour)
For an outdated move that is not pinned to a SHA, the reported sha is the SHA the written ref actually resolves to, i.e. set.shaForExactTag(newRef) ?? targetSha. When the ref is narrowed to a moving major tag (v3 → v4), the v4 tag may point at a different commit than the newest full tag (v4.2.1); reporting the full tag's SHA would be misleading, so the moving tag's own SHA is reported. When pinning to SHA, the target tag's SHA is used directly.
Overrides — per-action, last-match-wins
overrides is an ordered list of { pattern, policy }. pattern is a glob over the action slug (owner/repo or host/owner/repo[/subpath], the ref stripped — see actionSlug). For each action, the base policy is taken and every matching override is applied in list order via { ...p, ...o.policy } — so the last matching override wins per field, and overrides are partial (only the fields they set are changed).
{
"policy": { "track": "major", "pin": "keep", "bump": "major" },
"overrides": [
{ "pattern": "actions/*", "policy": { "pin": "sha" } },
{ "pattern": "actions/checkout", "policy": { "bump": "minor" } },
],
}actions/checkout matches both; effective policy is { track: "major", pin: "sha", bump: "minor" }.
Worked examples
Findings below use the resolver's outcome kinds (packages/core/src/resolution.ts).
1. Bump v3 → v4, major-only granularity kept
- Ref:
actions/checkout@v3. Repo has tagsv4,v4.2.1,v3.6.0,v3. - Policy:
track: "major",pin: "keep",bump: "major". - Latest eligible =
v4.2.1. Current is major-only (v3,minor === null) and an exactv4tag exists →renderTargetyieldsv4. - Finding:
outdated,current: "v3",latest: "v4",sha= the SHA thev4tag points at (notv4.2.1's SHA). - Before:
uses: actions/checkout@v3 - After:
uses: actions/checkout@v4
If no exact v4 tag existed, the rewrite would be the full v4.2.1.
2. Pin a tag to its SHA with a tag comment
- Ref:
actions/checkout@v4. Repo:v4→abc...def. - Policy:
track: "keep",pin: "sha",bump: "major". track: "keep"skips the bump lookup;pin: "sha"resolvesv4's SHA.- Finding:
pinnable,current: "v4",sha: "abc...def",tagComment: "v4". - Before:
uses: actions/checkout@v4 - After:
uses: actions/checkout@abc...def # v4
3. Pin + bump together
- Ref:
actions/checkout@v3. Tags:v4→1111...,v3→0000.... - Policy:
track: "major",pin: "sha",bump: "major". - Bump finds
v4;renderTargetkeeps major-only →tagComment: "v4", SHA =v4's target SHA. - Finding:
pinnable,current: "v3",sha: "1111...",tagComment: "v4". - Before:
uses: actions/checkout@v3 - After:
uses: actions/checkout@1111... # v4
4. Unpin (actup pin --unpin)
pin writes the original ref into a trailing comment (@<sha> # v4). --unpin reverses that.
- Command:
actup pin --unpin(forcespolicy.pin = "tag"). parseVersionclassifies the SHA askind: "sha", soevaluateRefreturnspinnedin isolation. The engine then upgrades it: when the finding ispinned,policy.pin === "tag", and the line has a trailing# <ref>comment, it recovers that ref and emits anunpinnablefinding.- Before:
uses: actions/checkout@1111... # v4 - After:
uses: actions/checkout@v4 - A bare SHA with no trailing comment stays
pinnedand unchanged — there is no information to recover the original tag from.
5. Floating @main → suggest a pin target
- Ref:
actions/checkout@main.main/master/develop/trunkparse askind: "branch". - Finding:
floating,branch: "main",suggested= newest semver tag's original string (e.g."v4.2.1") ornullif the repo has no semver tags. - Before:
uses: actions/checkout@main - After (suggested):
uses: actions/checkout@v4.2.1(the suggestion; a branch is not auto-rewritten the way an outdated semver tag is).
6. Non-semver tag (e.g. @2024-05-01 or @nightly)
- Ref:
some/action@2024-05-01. Not SHA, not semver, not a known branch →kind: "other". - Policy
pin: "sha"and the tag is a known tag in the fetched set → Finding:pinnable,current: "2024-05-01",sha= that tag's SHA,tagComment: "2024-05-01". Supply-chain hardening still applies to date/custom tags without an extra network call.- Before:
uses: some/action@2024-05-01 - After:
uses: some/action@<sha> # 2024-05-01
- Before:
- Otherwise (no SHA pin, or unknown tag) → Finding:
unresolvable,reason: "non-semver tag — cannot compare"(severityerror). No rewrite.
Severity / actionability
| Finding | Severity | Actionable |
|---|---|---|
upToDate, pinned | info | no |
outdated, floating, pinnable, unpinnable | warn | yes |
unresolvable | error | no |