Trace every locked commit to its recorded ref by nodeselector · Pull Request #186 · github/gh-actions-lock · GitHub
Skip to content

Trace every locked commit to its recorded ref - #186

Merged
nodeselector merged 49 commits into
mainfrom
nodeselector-174-trace-recorded-ref
Oct 20, 2026
Merged

nodeselector merged 49 commits into
mainfrom
nodeselector-174-trace-recorded-ref

Conversation

@nodeselector

@nodeselector nodeselector commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Every locked commit is now checked, in every mode, against the history of its recorded tag or branch, looked up by exact name the way the Actions backend resolves it, so a fork-only commit can no longer pass under the upstream action's name (closes #174).
A recorded ref that was rewritten or deleted so it no longer contains the locked commit, a recorded ref that isn't a real tag or branch, and a malformed uses: now block with a clear message instead of passing silently; an exact tag that only moved ahead warns under --verify, and --relock --accept-moved re-records a direct full-SHA pin whose recorded ref no longer vouches for it.
Bugs already present in v0.2.0 that this work surfaced are fixed in the follow-up #197.

nodeselector and others added 3 commits October 19, 2026 19:11
Recover local fix fcdd43df96c77d5a376a3c27de8d03f07f6317c5 and retain both community normalization safeguards.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Recorded tags now re-peel before repair (#174) and branch normalization
needs a head proof (#165), so same-run conflicting repairs can no longer
be planned. Fixtures ported from candidate commit 3eb0100.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Parity now resolves recorded branch heads and fails closed when the
recorded ref is inconclusive (#174). Stub fixtures ported from candidate
commits 3eb0100 and 22e894c.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
nodeselector and others added 4 commits October 19, 2026 19:24
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CHECKOUT_SHA was the actions/checkout v6.0.2 commit while the fixtures
record it under v4 and main. Recorded-ref tracing (#174) correctly rejects
that; use the real v4.2.2 commit the stub tag list already names.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Restores the #167 same-run scenario with a recorded tag instead of
swapping it for a branch, which only exercised the missing-head exit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the original --verify bodies, adjusted for ref-qualified lookups and
fail-closed inconclusive checks; replace the mode cross-products with one
default/--no-fix test, and drop full-SHA and entry-ref cases that
TestParityFinding_FullSHAKey and TestValidEntryRef already cover.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nodeselector
nodeselector removed this pull request from stack #188 October 20, 2026 02:35
The backend resolver, not the runner, looks up and rejects lock entry
refs; say so in comments, docs and the missing-ref finding detail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nodeselector
nodeselector marked this pull request as ready for review October 20, 2026 02:55
@nodeselector
nodeselector requested a review from a team as a code owner October 20, 2026 02:55
Copilot AI max review requested due to automatic review settings October 20, 2026 02:55
nodeselector and others added 2 commits October 19, 2026 19:55
Tags move, and the Actions backend accepts a pin whose commit is still in
its tag's history. Every lock entry ref is now checked by reachability:
a tag that lost the commit still blocks and an inconclusive check still
fails closed, but a tag that moved ahead only warns under --verify.

Dropping the separate tag-equality lookup also saves the extra REST
commit call when a ref still points at its locked commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
An error-severity finding blocks the run, so it never renders as a
warning. Under --verify the terminal now shows an exact tag that moved
ahead of its locked commit, matching the JSON warning, and the
inconclusive-check remediation names the Actions backend.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved provenance-checking defects can produce lockfiles that fail subsequent verification.

4 open issues
What changed in this PR

Strengthens the CLI’s lockfile verification by tracing locked commits to their recorded upstream refs.

Changes:

  • Adds recorded-ref checks across online modes.
  • Validates lockfile refs and rechecks tags before SHA-to-tag rewrites.
  • Updates documentation, regression tests, and integration fixtures.
File Description
test/​scenarios/​catalog.yml Updates fixtures and blocking-error expectations.
test/​integration/​run.rb Refreshes checkout data and verification stubs.
README.md Documents provenance and ref requirements.
internal/​pipeline/​verify_local_test.go Updates SHA-entry metadata.
internal/​pipeline/​run.go Revises parity filtering during relock.
internal/​pipeline/​parity.go Checks recorded refs and commit ancestry.
internal/​pipeline/​parity_test.go Tests targets and full-SHA verdicts.
internal/​pin/​plan.go Verifies tags before rewriting SHA refs.
internal/​pin/​plan_test.go Covers verified-tag repair behavior.
internal/​pin/​normalization_test.go Adds tag-verification fixtures.
internal/​lockfile/​state.go Validates refs when loading and recording.
internal/​lockfile/​state_test.go Supplies provenance for SHA fixtures.
internal/​lockfile/​entryref_test.go Tests recorded-ref validation.
internal/​lockfile/​convertor.go Defines recorded-ref validation rules.
internal/​ghapi/​graphql_parity.go Adds tag-first, branch-fallback lookups.
internal/​ghapi/​graphql_parity_test.go Tests lookup precedence and fallback.
cmd/​gh-actions-lock/​selfrepository_test.go Supports tag verification in fixtures.
cmd/​gh-actions-lock/​proxima_test.go Handles qualified parity lookups.
cmd/​gh-actions-lock/​parity_test.go Expands mode and provenance coverage.
cmd/​gh-actions-lock/​no_onboard_sha_test.go Records branch provenance in fixtures.
cmd/​gh-actions-lock/​command_test.go Supports explicit recorded-ref fixtures.

Review effort: Max

Comment thread internal/lockfile/convertor.go Outdated
Comment thread internal/pipeline/parity.go Outdated
Comment thread internal/pipeline/parity.go Outdated
Comment thread internal/pipeline/run.go Outdated
@nodeselector
nodeselector added this pull request to stack #191 October 20, 2026 03:16
nodeselector and others added 9 commits October 19, 2026 20:26
A ref like main~1 or v1@{1} is not a tag or branch name, but a commit-ish
lookup evaluates it as a revision. Validate names with git
check-ref-format rules, matching actions-lockfile's RefName: an entry ref
that fails makes the lockfile unreadable, and parity never builds a
lookup expression from one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
--relock drops findings for recorded pins because it replaces them, but
it also dropped them for pins it had just resolved, so it could write a
lock that the next --verify rejects.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A full-SHA entry whose recorded tag or branch no longer vouches for it
recommended --accept-moved, which then refused it. --accept-moved now
accepts it as a warning and re-records a tag or branch that contains the
commit, chosen by reverse lookup; a commit no branch contains still fails.
Fresh full-SHA pins are no longer checked against a pre-planning ref.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Actions backend resolver looks a recorded ref up as a tag before a
branch, so reverse lookup now skips a containing branch whose name is
also a tag.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A ref counts only if a tag or branch exists by exactly that name: GraphQL
also asks ref(qualifiedName:), and REST reads git/ref/{tags|heads}/<name>.
Like the Actions backend, names ending in -g plus four or more hex
digits are not accepted as recorded or uses: refs, so an entry with one
makes the lockfile unreadable and a uses: ref with one is never locked.

Adapted from 2134d14 and 93b2f4a.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
--accept-moved now prints the old and new recorded ref when it keeps a
full-SHA pin's commit but records a new tag or branch for it. A ref this
run resolved that isn't a tag or branch is titled "Not a tag or branch";
"Ref no longer exists" is kept for a ref the lockfile recorded.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A tool failure now prints before the run log path instead of after it.
Inconclusive checks and the Dependabot proxy abort show a short cause on
the terminal, such as "could not check v4 history: GitHub returned 403
Forbidden"; --json and the run log keep the full error.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A full-SHA pin whose recorded tag or branch no longer exists was titled
"Pinned commit not in upstream history". It is now "Ref no longer
exists", like any recorded ref that is gone.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
nodeselector and others added 5 commits October 19, 2026 23:42
Tag precedence keyed off the tag peeling to a commit, so a tag that
targets a tree or blob fell through to a same-named branch and parity
checked the branch's history. Precedence now keys off the tag existing,
on both the GraphQL and REST paths; such a tag resolves to no commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reverse lookup filtered tag names with the backend rule only, and after
deciding whether to read more pages. An unrecordable tag on the first page
(release-g1234) then stopped the search for a recordable tag on a later
page, and SHA-shaped or refs/ tag and branch names could be chosen and
rejected later by the lockfile. Candidates now use RecordableRefName, the
same predicate as ValidEntryRef, before any paging or selection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The planner's SHA repair picked the recorded branch when its head was the
locked commit, without the tag-shadow check reverse lookup does, so
rewriting to @main could name a same-named tag's commit instead. The
repair asks refCandidates, which now also applies the name policy and
the shadow check to that branch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
--verify and --no-fix return before planning, so --accept-moved could
pass a full-SHA pin whose recorded ref is gone without ever finding or
validating a replacement. Only runs that write accept it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The line search matched the first occurrence of the key, which is
usually the workflow's list item, or the prefix of a longer key. Match
the whole quoted mapping key instead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive verification changes span many packages and retain unresolved repair and failure-handling defects.

1 open issue
8 previously missed issues

In code that hasn't changed since last review

Medium severity Replacement matching ignores workflow and original pin identity

cmd/​gh-actions-lock/​pin_summary.go:255

This matches any entry with the same repository and commit, even one from another workflow. If one workflow newly pins @v5 at commit A while a full-SHA pin elsewhere is repaired to record v6 at A, the summary can incorrectly report v5 as the accepted replacement. Setting seen then hides the actual repair. Match the finding's workflow and original pin or rewrite identity, not just its repository and commit.

Medium severity SSO guidance is lost when shortening HTTP errors

internal/​ghapi/​brief.go:15

StatusCode unwraps SSORateLimitedError to its HTTP error, so this replaces its authorization guidance with only a status line. It also removes the SAML-enforcement message from ordinary 403 errors. Parity failures now block the run, but fix mode does not print the separate read-only SSO hints, leaving users without the required next step. Preserve actionable SSO messages while shortening other HTTP errors and omitting request URLs.

Medium severity Tag lookup misses recorded tags beyond the first page

internal/​pin/​plan.go:663

LookupTag searches a cached listing backed by Client.ListTags, which reads only the first 100 tags. A full-SHA pin recorded under an older tag outside that page now silently skips normalization, even when that tag still points exactly at the locked commit. Use an exact lookup of the recorded tag and compare its peeled commit, rather than treating absence from the first page as absence upstream. Add coverage with the recorded tag beyond that page.

Medium severity Relock filters suppress moved-tag verification warnings

internal/​pipeline/​parity.go:160

With --verify --relock or --verify --accept-moved, the relock filter discards this recorded RefMoved warning, while the freshly resolved pin already matches the live head. The diagnostic RefMoved finding has TagMoved=false, so the terminal suppresses it and verification succeeds without the documented moved-tag warning. Preserve this warning through the filter, and ensure warning deduplication keeps its TagMoved marker rather than the earlier diagnostic finding.

Medium severity Relock can discard blocking parity failures after transient errors

internal/​pipeline/​parity.go:166

Under --relock outside the Dependabot proxy, resolution can fail transiently and leave a recorded symbolic pin without a fresh replacement. If parity also fails, keep(UnreachablePin, symbolicPin) is false here, and the next filter discards ReachabilityUnknown. Diagnosis leaves only a non-blocking resolution warning, so planning can retain the old pin and the command succeeds without verifying it. Keep recorded parity errors blocking even during relocking; cover a locked @v4 whose resolution and parity requests both return 503.

Medium severity Moved-tag warnings omit transitive composite parents

internal/​pipeline/​parity.go:290

This new warning drops the transitive parent that parityFinding derives from lp.Parent. When an exact tag used inside a composite moves, the warning omits which composite requires it, and the JSON finding loses required_by. Pass the locked pin or its parent into movedTagFinding and populate ParentNWO and the detail suffix consistently with other parity findings.

Medium severity Relock cannot follow moved tags for SHA-keyed pins

internal/​pipeline/​parity.go:298

For a full-SHA uses: whose recorded exact tag moved ahead, --relock resolves the SHA to the same commit. The new guard in narrowVerifiedEntries correctly refuses to rewrite it to the moved tag, so this command does not lock the tag's current commit and can leave the same warning on the next verification. For SHA-keyed pins, explain that following the tag requires changing uses:; retain the current advice for symbolic pins.

Medium severity Mixed direct and composite SHA pins leave stale state

internal/​pipeline/​run.go:173

If a workflow uses both a composite and the same full-SHA child directly, Closure now reports the child with no parent, so --accept-moved downgrades its failed check here. Planning can normalize only the direct use to a tag, while State.Set retains the composite's edge to the original SHA entry and leaves that entry's recorded ref unchanged. The command succeeds, but the next --verify rejects the child again. Repair the original SHA-keyed entry too, or keep this case blocking; add a regression covering both consumers.

Review effort: Max

Comment thread internal/ghapi/refs.go
nodeselector and others added 4 commits October 20, 2026 00:31
Repairing a full-SHA pin and reverse lookup can both look up the same
recorded ref by exact name. Cache found and missing answers for the
invocation and coalesce concurrent lookups; failed lookups are retried.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
v0.2.0 rewrote a full-SHA `uses:` to its recorded ref without proof. Since
#174 the repair requires proof, but only checked semver tags and branches:
a non-semver tag (recorded like a branch) was never normalized.

Repair now looks the recorded ref up by exact name, as the backend resolves
it (tag first), and rewrites only when it names the locked commit. A moved
tag, a branch a tag shadows, or a containing branch is left as a SHA.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The --accept-moved re-record line matched pinned entries by repository and
commit, so another workflow's pin of the same commit under a different ref
could be reported as the new recorded ref. Match the finding's own lock
key, kept or rewritten from it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A --verify warning for a tag that moved ahead of a composite's pin did not
name the composite (no required_by), and a full-SHA pin was told to run
--relock, which never changes a full-SHA `uses:`, so the warning stayed.

Name the parent like other parity findings. Advise a full-SHA pin to point
`uses:` at the tag, or, for a composite's pin, to upgrade the composite.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Proxima lookup panics and history-check filtering gaps remain unresolved.

1 open issue
1 resolved since last review

Review effort: Max

Comment thread internal/ghapi/refs.go
LookupRefName, and ResolveRefName through it, read refs on the routing
client instead of the repository's selected host, so on a tenant host
SHA-pin repair crashed. Route it like every other repository method, and
test every exported repository method on a tenant client so a new one
can't skip host selection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Relock can retain unverified commits, and accepted SHA-pin repairs can preserve invalid entries or delete pins and their children.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Match diagnostic keys without lowercasing ref names

internal/​lockfile/​state.go:74

Lowercasing the entire key makes this diagnostic confuse distinct Git refs. If entries for owner/repo@Main and owner/repo@main exist and only the latter has an invalid ref, this returns the line of @Main instead. Match the original mapping key without folding its ref's case.

1 resolved since last review

Review effort: Max

The diagnostic line lookup lowercased the whole key, so with entries for
owner/repo@V1 and owner/repo@v1 it could cite the wrong one. Compare
canonical pin keys instead (parserlock.ParsePin): owner and repo fold,
refs stay case-sensitive, as everywhere else the lockfile is keyed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved provenance bypasses, SHA-repair inconsistencies, unbounded API scans, and reporting and documentation issues remain.

No open issues 🎉

Review effort: Max

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Relock can bypass history failures, and SHA repairs can retain invalid entries or leave workflow pins unlocked.

No open issues 🎉

Review effort: Max

@nodeselector
nodeselector merged commit 8bb4bb6 into main Oct 20, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Full-SHA lock entries can accept a fork-only commit under the upstream action name

2 participants