Repair lock entries that record their own SHA as the ref by nodeselector · Pull Request #198 · github/gh-actions-lock · GitHub
Skip to content

Repair lock entries that record their own SHA as the ref - #198

Merged
nodeselector merged 35 commits into
mainfrom
nodeselector-repair-sha-recorded-ref
Oct 20, 2026
Merged

nodeselector merged 35 commits into
mainfrom
nodeselector-repair-sha-recorded-ref

Conversation

@nodeselector

@nodeselector nodeselector commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

A lock entry keyed by a full SHA whose ref: is that same SHA no longer fails the whole lockfile load: --verify, --verify-local and --no-fix report it as a not-pinned error and write nothing. A default or --relock run re-records it with the tag at that commit, else a branch containing it, after confirming its repository ID and commit; a commit on no tag or branch, or an entry whose identity can't be confirmed, still blocks. A run given workflow paths leaves other workflows' entries as they are and warns about them, and an invalid lockfile gets the same guidance in every mode, naming its first bad entry in file order (keys that don't parse as pins are checked last).

@nodeselector
nodeselector requested a review from a team as a code owner October 20, 2026 16:16
Copilot AI max review requested due to automatic review settings October 20, 2026 16:16

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

Repair paths can lose existing pins or bypass integrity checks on retained dependencies.

4 open issues
What changed in this PR

Adds a repair path for lock entries that record their own commit SHA as the ref, instead of rejecting the entire lockfile.

Changes:

  • Re-record affected entries using a containing tag or branch.
  • Report them in read-only modes and improve recovery guidance.
  • Add regression coverage for repairs, verification, and host preservation.
File Description
internal/​pipeline/​parity.go Avoid looking up own-SHA refs as branches or tags.
internal/​pipeline/​checks/​structural.go Report own-SHA entries as not pinned.
internal/​pipeline/​checks/​run.go Register the new structural check.
internal/​pipeline/​checks/​run_test.go Assert blocking repair findings.
internal/​pin/​repair.go Plan transitive entry repairs.
internal/​pin/​record.go Track repair metadata.
internal/​pin/​plan.go Integrate repairs and preserve SHA uses.
internal/​pin/​commit.go Apply entry-ref repairs.
internal/​lockfile/​state.go Load, identify, and update repairable entries.
internal/​lockfile/​entryref_test.go Cover loading and diagnostic ordering.
internal/​lockfile/​convertor.go Identify own-SHA recorded refs.
cmd/​gh-actions-lock/​verify.go Report affected entries during verification.
cmd/​gh-actions-lock/​verify_test.go Cover unselected and deleted workflows.
cmd/​gh-actions-lock/​runlog_test.go Update the invalid-lockfile fixture.
cmd/​gh-actions-lock/​run.go Preserve host bindings and append findings.
cmd/​gh-actions-lock/​proxima_test.go Verify recorded-host preservation.
cmd/​gh-actions-lock/​pin_summary.go Display re-recorded refs.
cmd/​gh-actions-lock/​parity_test.go Cover repair and read-only behavior.
cmd/​gh-actions-lock/​lockrecovery.go Expand invalid-lockfile recovery guidance.
cmd/​gh-actions-lock/​lockrecovery_test.go Assert updated guidance.
cmd/​gh-actions-lock/​format/​terminal.go Render repository errors with remediation.

Review effort: Max

Comment thread internal/lockfile/state.go Outdated
Comment thread internal/pin/repair.go Outdated
Comment thread internal/pin/repair.go
Comment thread internal/pipeline/checks/structural.go

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, repair planning, and cross-mode compatibility need fixes and human validation.

3 open issues
4 previously missed issues

In code that hasn't changed since last review

Medium severity Composite-only findings omit the structured required_by parent

cmd/​gh-actions-lock/​verify.go:149

For an entry reached only through a composite, set ParentNWO as well as naming the parent in Detail. format/json.go:129-131 populates required_by only from ParentNWO, so these new findings omit the structured parent that parity findings provide (internal/pipeline/parity.go:361). JSON consumers otherwise have to parse the human-readable detail to identify the composite to upgrade.

Medium severity YAML key sorting misorders quoted and commented dependency entries

internal/​lockfile/​state.go:169

The file-order sort misses accepted YAML key forms. entryLines recognizes only standalone single-quoted keys, so a double-quoted or unquoted key, or one followed by a comment, receives the fallback position. An invalid entry in one of those forms therefore sorts after a later single-quoted invalid entry, reporting the wrong first error. Collect canonical dependency keys and source lines from YAML mapping nodes rather than relying on the quoting style.

Medium severity README documents incorrect own-SHA loading and exit-code behavior

internal/​lockfile/​state.go:179

Please update README.md:63-67 for this exception. It still says every backend-rejected entry ref, including a SHA, fails loading with exit 2. Own-SHA entries now load, produce not-pinned errors with exit 1 in read-only modes, and can be repaired by fix runs. Document those outcomes separately from refs that remain corrupt-lockfile errors; exit-code behavior is also part of the Dependabot compatibility guidance in RELEASING.md:44-53.

Medium severity Same-commit SHA repairs are dropped by inventory and closure deduplication

internal/​pin/​plan.go:316

A repair can disappear before ReRecorded is assigned. If a workflow locks both actions/cache@<sha> and a tag at the same commit, buildPinnedEntries skips the SHA dependency because the tag already populated inventorySHA. planEntryRepairs also skips that entry because Closure marks direct roots as parentless, even when a composite uses them too. Commit then drops the SHA root; if a composite retains it, the original invalid entry survives the successful fix run. Exempt own-SHA repair candidates from buildPinnedEntries' inventory deduplication guard, and cover this same-commit case.

3 resolved since last review

Review effort: Max

Comment thread internal/pin/repair.go Outdated
Comment thread internal/pipeline/parity.go

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.

Comment thread cmd/gh-actions-lock/verify.go
Comment thread internal/pin/repair.go
Comment thread internal/pin/repair.go
Comment thread internal/pipeline/parity.go
nodeselector and others added 12 commits October 20, 2026 13:59
A full-SHA key whose recorded ref is that same SHA failed the whole
lockfile load (exit 2) in every mode, leaving no way to repair it. The
Actions backend rejects the pin, so it is still an error, but now as a
finding: the entry loads as unrecorded, and read-only modes report it
as not-pinned (exit 1) and write nothing.

Only a workflow's own pin loads this way. Every other invalid recorded
ref, a SHA that differs from its key, and an entry no workflow lists
directly still fail the load.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A fix run (default or --relock) now resolves such a pin like any fresh
full-SHA pin: the tag at the commit, else a branch containing it. It
keeps the full-SHA `uses:`, records the ref it found, and names the entry
in a one-line notice. A commit no tag or branch contains, such as an
impostor commit, still fails with "not on any tag or branch" and nothing
is written.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entries were checked in map order, so a lockfile with several bad
entries named a different one on each run. Check them in file order.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
--verify-local prefixed "opening lockfile:" and left out the guidance
the other modes give.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Restoring from version control can't help when the committed file is
itself the problem, so the guidance also says to move it aside and run
`gh actions-lock`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Get leaves the entry out, but the stale check reads the lockfile's
workflow list, so the entry is still reported.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Loading no longer fails on a pin that records its own SHA, so a run
given workflow paths could pass while another workflow's pin is one the
Actions backend rejects. --verify, --no-fix and --verify-local now
report each such entry as an error and point at a full-scan run, which
is the only kind allowed to rewrite that workflow. Nothing is written.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The unscanned-entry check only ran for path-scoped runs, so a full scan
passed while a deleted workflow's lock entry still recorded its own SHA.
Report every unscanned workflow's entry, whether it was left out or no
longer exists.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
An entry recording its own SHA as its ref still failed the whole load
when no workflow listed it directly: reached only through another
entry's `uses:`, or by nothing. Every mode exited 2.

Such entries now load too. Read-only modes report each one no scanned
workflow lists, naming where it sits, and exit 1 without writing. A fix
run re-records a transitive one with the tag at its commit, else a
branch containing it, leaving its parent unchanged, and Save drops an
orphan as before. Parity checks only the repository and commit of such
an entry, never the SHA as a ref. A commit no tag or branch contains
still blocks and nothing is written.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
AllDeps leaves such entries out so the resolver re-resolves them, but
host binding read it too, so the entry's recorded host was dropped: a
github.com pin on a tenant could reach a tenant namesake, and a pin to
another host no longer failed. Bind hosts from every recorded entry.

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 23 commits October 20, 2026 13:59
A fix run given workflow paths must not rewrite workflows it wasn't
given, so it left another workflow's entry recording its own SHA in
place and still said every workflow was valid. It now names each such
entry with "run `gh actions-lock` without a path to repair it" and no
longer prints "All N workflows valid". The warning is non-blocking: the
selected workflows are written and the exit code is unchanged, while
read-only runs keep reporting the same entry as an error.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
AllDeps left out entries recording their own SHA so they wouldn't seed
ref resolution, which made host binding need a separate view. AllDeps
now returns every entry and lockfile.Seedable decides what may seed ref
resolution and branch hints. No behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
checkStale held the rule inline: a root is used by its key, or by its
commit when a `uses:` names that full SHA. Move it to lockfile.UsedRoot
so parity and planning can share it. No behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
State.Get left such pins out so the run would re-resolve them, but every
reader of the workflow's recorded pins took that to mean "not recorded".
Parity then skipped the pin's stored closure, so an entry under a seeded
composite was saved without its repository or commit being checked.

Get now returns every recorded entry. The not-pinned finding on the pin
asks planning to prune and re-resolve it (Finding.Reresolve, read by
checks.PrunesDependency), and parity checks it as a recorded root,
looking up only its repository and commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The --no-onboard gate rewrote its not-pinned finding as
onboarding-required, so --verify and --no-fix passed the entry, and a
fix run dropped its ref from planning and saved the workflow without
it. The entry exists, so the finding marks a tracked action.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Parity counted a recorded root as used only when a `uses:` named its
key, while the stale check also counts a `uses:` naming its commit. A
tag-keyed root whose `uses:` was rewritten to the commit was neither
stale nor parity-checked, so its stored closure went unverified. Parity
now uses the same rule (lockfile.UsedRoot); Closure hands it each root's
entry.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entry repair walked every recorded root, so an own-SHA child of a stale
root blocked a run that was about to drop it, and a stale own-SHA root
that a kept entry still used was claimed as a root, skipped, and saved
unrepaired. Repair now shares retainedLockTargets' stale-root set
(staleRoots), so it walks only kept roots and reaches such an entry as
a child.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A fix run said "use a current upstream tag instead", and read-only runs
gave only the full-scan step, though the workflow doesn't use that
commit itself. Name the entry that does, and point at upgrading or
replacing it, as parity does for a composite's pin.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Audit filtered them inline; pin.writers names the set so entry repair
can preview the same write. No behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entry repair walked the selected workflows' old lock roots, so it missed
an entry the write keeps some other way: a composite a newly locked
workflow reuses from the lock writes its stored `uses:`, and the
own-SHA child under it was saved unrepaired, or with no tag or branch
containing it, saved at all. Repair now previews the write with the code
Commit uses, over the same workflows Audit checks (pin.writers), and
repairs or blocks on every own-SHA entry still reachable from a workflow
it updates. It runs between planning and the audit, so the audit sees
the repairs. A same-commit tag and SHA test stays green as a control.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
updateLockfile applied every planned repair, so a preview that leaves a
workflow out, as Audit's does once it blocks a writer, still repaired
entries only that workflow reaches. A repair now names the written
workflows that reach its entry and applies only when one is written.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Parity checks such an entry's repository and commit only, with no ref,
so a check that couldn't finish fell through as an ordinary recorded pin
and only warned; the run could then repair and save it unconfirmed.
disposeParity now treats an inconclusive own-SHA check like a fresh
pin's: an error that is never excused or deferred.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The finding named it only in its detail text; set ParentNWO so --json's
required_by carries it, as parity findings do.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The first-bad-entry order matched only single-quoted keys on their own
line, so a double-quoted, plain or commented key sorted last and a later
bad entry was reported first. Read key lines from the parsed YAML
mapping; a lockfile whose YAML can't be read for them is reported as
invalid instead of falling back to another order.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
README still said every backend-rejected entry ref fails loading with
exit 2. Describe the exception, and list its exit-code change under
RELEASING's Dependabot compatibility check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the root-replacement input pair to the write-following repair test:
an existing workflow drops its locked root for a composite another
workflow already locked. Both rows fail before repair previewed the
write and pass now.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use checks.UnreachableRemediation for the composite advice instead of a
second copy of its wording. No behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lockfile.RootUse returns which `uses:` covers a root: one naming its key,
else one naming its commit. UsedRoot is now a thin wrapper over it. No
behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The parser rejects a full-SHA key whose commit differs, so an own-SHA
entry's key names the commit its repair starts from. Test only.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The own-SHA check matched a workflow's use to the entry by exact key, so
a use naming the SHA in other letter case got no finding, and the
repository-level pass skipped the entry because a scanned workflow
listed it: read-only modes passed an entry the Actions backend rejects.
The check now finds the covering use with lockfile.RootUse, the rule
the stale check uses, and describes the entry itself. The repository
pass skips an entry only when a workflow finding already blocks on it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Parity bound a tag-keyed root to the first `uses:` covering it, so an
earlier use of its commit SHA won over the tag use, and rename advice
and the rename rewrite targeted the SHA use. Parity now asks
lockfile.RootUse, which prefers the use naming the key.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A run onboarding a workflow onto a composite another workflow locks
reaches the composite's own-SHA child without any check of it: the
reused composite carries no manifest, so parity never sees the child.
Repair then rewrote it on containment alone, keeping a repo ID that may
no longer match. Each repair candidate now gets one batched check of
its recorded repository ID and commit, and a candidate that fails or
can't be checked blocks the run with advice to run --relock. Runs with
no candidate make no extra request, and an entry the run doesn't
rewrite is never checked.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The identity check compared repo IDs only when both were known, so a
response without one, or an entry without a recorded one, passed, and
with no API client every candidate passed unchecked. A candidate is now
admitted only when the recorded and live repo IDs are both present and
equal and the commit is found; anything else, including no client,
blocks it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nodeselector
nodeselector changed the base branch from nodeselector-186-followup-finding-disposition to main October 20, 2026 20:59
@nodeselector
nodeselector force-pushed the nodeselector-repair-sha-recorded-ref branch from 4ebd4bb to 23c1542 Compare October 20, 2026 22:35
@nodeselector
nodeselector requested a max review from Copilot October 20, 2026 22:35
@nodeselector
nodeselector merged commit dacafb7 into main Oct 20, 2026
12 checks passed

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.

Comment thread internal/pin/repair.go
Comment on lines +55 to +58
for _, lp := range preview.Closure(workflowfile.KeyFromPath(wp.Path), all) {
key := lp.Pin.String()
if !lockfile.RecordsOwnSHA(lp.Pin, lp.Action.Ref) || unresolved[key] {
continue
// rootRef is the workflow ref that still uses a recorded root, by
// the same rule as the stale check (lockfile.RootUse): the use
// naming its key, else one naming its commit.
use := lockfile.RootUse(recorded)
store, err = lockfile.LoadState(".", nil)
lockPath = filepath.Join(workflowsDir, "actions.lock")
}
store, err := lockfile.LoadStateAt(lockPath, nil)
Comment on lines +473 to +475
pin := parserlock.Pin{Owner: owner, Repo: repo, Ref: sha}.Canonical()
action, ok := s.file.Dependencies[pin.String()]
return ok && RecordsOwnSHA(pin, action.Ref)
Comment thread internal/pin/plan.go
Comment on lines +381 to +385
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.

2 participants