Skip to content

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):

FieldValuesMeaning
trackkeep | major | exactWhether/how far the ref may move.
pinkeep | tag | shaThe form the rewritten ref takes.
bumppatch | minor | major | latestUpper 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 with pin: "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; track controls the form of the written ref. keep short-circuits (no version change). major vs exact differ in renderTarget: only major collapses to a vN moving 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:

bumpEligible candidate
patchsame major and same minor
minorsame major (any minor/patch)
majorany (no constraint)
latestany (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 are pin === 'sha' vs everything else, so tag behaves like keep (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 with minor === null) and a matching major-only tag exists in the repo (e.g. v4), the rewrite stays major-only: v3v4. The leading v is kept iff the original had one.
  • Otherwise the rewrite uses the latest tag's full original string (e.g. v3.1.0v4.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 (v3v4), 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).

jsonc
{
	"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 v3v4, major-only granularity kept

  • Ref: actions/checkout@v3. Repo has tags v4, 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 exact v4 tag exists → renderTarget yields v4.
  • Finding: outdated, current: "v3", latest: "v4", sha = the SHA the v4 tag points at (not v4.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: v4abc...def.
  • Policy: track: "keep", pin: "sha", bump: "major".
  • track: "keep" skips the bump lookup; pin: "sha" resolves v4'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: v41111..., v30000....
  • Policy: track: "major", pin: "sha", bump: "major".
  • Bump finds v4; renderTarget keeps 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 (forces policy.pin = "tag").
  • parseVersion classifies the SHA as kind: "sha", so evaluateRef returns pinned in isolation. The engine then upgrades it: when the finding is pinned, policy.pin === "tag", and the line has a trailing # <ref> comment, it recovers that ref and emits an unpinnable finding.
  • Before: uses: actions/checkout@1111... # v4
  • After: uses: actions/checkout@v4
  • A bare SHA with no trailing comment stays pinned and 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/trunk parse as kind: "branch".
  • Finding: floating, branch: "main", suggested = newest semver tag's original string (e.g. "v4.2.1") or null if 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
  • Otherwise (no SHA pin, or unknown tag) → Finding: unresolvable, reason: "non-semver tag — cannot compare" (severity error). No rewrite.

Severity / actionability

FindingSeverityActionable
upToDate, pinnedinfono
outdated, floating, pinnable, unpinnablewarnyes
unresolvableerrorno