Skip to content

Resolve dependencies live on every run - #119

Open
nodeselector wants to merge 20 commits into
mainfrom
nodeselector-remove-rescan
Open

Resolve dependencies live on every run#119
nodeselector wants to merge 20 commits into
mainfrom
nodeselector-remove-rescan

Conversation

@nodeselector

@nodeselector nodeselector commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

What

Remove --rescan and make every networked gh actions-lock run live-resolve the current explicitly scoped dependency closure.

Why

Normal runs previously trusted mutable lockfile entries while still mutating the lockfile from a shallow workflow comparison. That made first generation and later runs behave differently and required a separate mode to catch moved refs. The simpler model is: networked runs resolve live; explicit flags control whether moved pins may advance.

How

  • Delete the public flag, cache seeding, pipeline fast path, nudges, conflicts, tests, and catalog scenarios that existed only for rescan.
  • Resolve current workflow refs and the recursively recorded closure, caching both successful and failed ref resolutions for the run.
  • Check recorded transitive refs for movement and reachability, not just direct workflow roots.
  • Retain a moved or partially resolved composite's recorded SHA and closure. --relock or --accept-moved advances it only after complete recursive resolution, then replaces obsolete child edges with the live graph.
  • Keep explicit-path authority unchanged, so scoped runs preserve untouched workflow entries and do not prune outside their scope.
  • Keep --verify-local offline and normal inconclusive results non-blocking.

Scope

Repository-transfer and canonical-NWO handling remain separate in PR #118. This PR stays independent and does not claim that outcome.

Risk

The behavior change increases GitHub API traffic for existing lockfiles. Dependabot does not pass --rescan, so its CLI contract is unchanged, but its REST-only path will also live-resolve each scoped closure after it adopts this binary. Its next pinned-binary bump should stage-test request volume and latency before release.

This is easy to roll back by reverting the commits.

Testing

  • go test ./...
  • make test-stub (34 scenarios)
  • script/release-test
  • Ruby syntax checks for the integration harness
  • Command-help and repository-wide checks confirming --rescan is absent

@nodeselector
nodeselector requested a review from a team as a code owner August 31, 2026 20:36
Copilot AI balanced review requested due to automatic review settings August 31, 2026 20:36

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.

Copilot review overview

🟡 Changes recommended

Live traversal can prune retained transitive pins, repository transfers remain unresolved, and failed resolutions can multiply API requests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 High severity · 2 Medium severity

New issues introduced by this change (3)
Severity Finding
Medium severity internal/​pipeline/​run.go — This does not implement the linked issue's repository-transfer outcome. Although the GraphQL…
Medium severity internal/​pipeline/​run.go — Failed refs are not cached, yet the prewarm error is discarded and DiagnoseParsed calls…
High severity internal/​pipeline/​run.go — Live traversal is not safe to use as the authority for pruning the retained lockfile closure. If a…
What changed in this PR

Makes all networked runs resolve scoped dependencies live while retaining moved pins unless explicitly advanced.

Changes:

  • Removes --rescan, lockfile cache seeding, and fast-path logic.
  • Surfaces moved-ref and inconclusive warnings.
  • Updates documentation and tests for default live resolution.
File Description
README.md Documents live resolution and --relock.
internal/​resolve/​resolver.go Removes lockfile cache seeding.
internal/​pipeline/​run.go Always resolves scoped workflows live.
internal/​pipeline/​run_test.go Removes obsolete fast-path tests.
internal/​pipeline/​parse.go Removes recorded-ref filtering.
internal/​pipeline/​checks/​parsed.go Removes fast-path helpers.
internal/​pipeline/​checks/​parsed_test.go Removes obsolete helper tests.
internal/​pipeline/​checks/​misleading.go Adds correct relock guidance.
internal/​pipeline/​checks/​finding.go Removes strict inconclusive gate helper.
cmd/​gh-actions-lock/​verify.go Makes verification read-only.
cmd/​gh-actions-lock/​verify_test.go Updates verification tests.
cmd/​gh-actions-lock/​run.go Removes the rescan CLI path.
cmd/​gh-actions-lock/​prune_workflow_test.go Adds live-resolution mocks.
cmd/​gh-actions-lock/​pin_summary.go Surfaces live-resolution warnings.
cmd/​gh-actions-lock/​pin_summary_test.go Tests warning detection.
cmd/​gh-actions-lock/​format/​terminal.go Renders moved and inconclusive warnings.
cmd/​gh-actions-lock/​format/​terminal_test.go Tests warning output.
cmd/​gh-actions-lock/​command_test.go Exercises default live resolution.
cmd/​gh-actions-lock/​check_json_golden_test.go Updates JSON test invocation.
test/​scenarios/​catalog.yml Removes rescan scenarios and updates expectations.
test/​integration/​run.rb Removes the rescan fixture.
test/​integration/​harness.rb Removes interactive rescan commands.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/pipeline/run.go Outdated
Comment thread internal/pipeline/run.go Outdated
Comment thread internal/pipeline/run.go Outdated

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.

Copilot review overview

🟡 Changes recommended

Transitive drift is not checked, and partial relocking can persist an incomplete dependency closure.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 High severity

New issues introduced by this change (2)
Severity Finding
High severity internal/​pipeline/​diagnose.goRecordedDeps is only consulted after RunChecks, while RunChecks builds its moved-ref index…
High severity internal/​lockfile/​state.go — Dropping recorded edges whenever the parent commit changes can persist an incomplete closure.…
Issues resolved since last review (3)
Severity Finding
High severity internal/​pipeline/​run.go — Live traversal is not safe to use as the authority for pruning the retained lockfile closure. If a… View resolved comment
Medium severity internal/​pipeline/​run.go — Failed refs are not cached, yet the prewarm error is discarded and DiagnoseParsed calls… View resolved comment
Medium severity internal/​pipeline/​run.go — This does not implement the linked issue's repository-transfer outcome. Although the GraphQL… View resolved comment

Comment thread internal/pipeline/diagnose.go
Comment thread internal/lockfile/state.go Outdated

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.

Copilot review overview

🟡 Changes recommended

Terminal --verify can silently omit resolver failures because the new warning path only runs in fix mode.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity cmd/​gh-actions-lock/​pin_summary.gorenderResolverWarning is only reached from the fix-mode summary, but --no-fix and --verify
Issues resolved since last review (2)
Severity Finding
High severity internal/​lockfile/​state.go — Dropping recorded edges whenever the parent commit changes can persist an incomplete closure.… View resolved comment
High severity internal/​pipeline/​diagnose.goRecordedDeps is only consulted after RunChecks, while RunChecks builds its moved-ref index… View resolved comment

Comment thread cmd/gh-actions-lock/pin_summary.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.

Copilot review overview

🟡 Changes recommended

Relocking can skip a moved transitive dependency when an unrelated new direct dependency is also present.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity internal/​pin/​plan.gorepinMoved does not ensure a moved transitive dependency is included in the re-resolution. If the…
Issues resolved since last review (1)
Severity Finding
High severity cmd/​gh-actions-lock/​pin_summary.gorenderResolverWarning is only reached from the fix-mode summary, but --no-fix and --verifyView resolved comment

Comment thread internal/pin/plan.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.

Copilot review overview

🟡 Changes recommended

Recorded transitive dependencies are omitted from diagnostic inventory for current lockfile graphs.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity internal/​pipeline/​diagnose.goRecordedDeps now contains the full lockfile closure, but the inventory populated immediately…
Issues resolved since last review (1)
Severity Finding
Medium severity internal/​pin/​plan.gorepinMoved does not ensure a moved transitive dependency is included in the re-resolution. If the… View resolved comment

Comment thread internal/pipeline/diagnose.go Outdated

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.

Copilot review overview

🟡 Changes recommended

Transitive finding order is nondeterministic, and resolver warnings are duplicated in terminal output.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 Medium severity

New issues introduced by this change (2)
Severity Finding
Medium severity cmd/​gh-actions-lock/​run.goformat.PresentResults now renders ReachabilityUnknown warnings, including this same f.Detail,…
Medium severity internal/​pipeline/​checks/​run.go — Adding every recorded closure entry to depIndex makes transitive findings nondeterministic:…
Issues resolved since last review (1)
Severity Finding
Medium severity internal/​pipeline/​diagnose.goRecordedDeps now contains the full lockfile closure, but the inventory populated immediately… View resolved comment
Suppressed comments (1)

cmd/gh-actions-lock/pin_summary.go:99

  • This repeats workflow-level resolver failures already emitted by format.PresentResults earlier in the fix-mode flow. When there are no unresolved record entries—the normal case for the aggregate ReachabilityUnknown finding—the same detail is printed again in the summary. Keep a single rendering path for this warning.
	} else {
		renderResolverWarning(console, report)

Comment thread cmd/gh-actions-lock/run.go
Comment thread internal/pipeline/checks/run.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.

Copilot review overview

🟡 Changes recommended

Live parent edges can contaminate retained closures, and direct bare-SHA references can receive false movement warnings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 High severity · 1 Medium severity

New issues introduced by this change (2)
Severity Finding
High severity internal/​pipeline/​diagnose.go — Do not merge the live parent graph into the recorded inventory before movement is accepted. If a…
Medium severity internal/​pipeline/​checks/​run.go — Adding every recorded pin here makes a symbolic lock root for a direct bare-SHA workflow reference…
Issues resolved since last review (1)
Severity Finding
High severity internal/​pipeline/​diagnose.go — A direct workflow use pinned by SHA is no longer recognized when its lockfile entry retains a… View resolved comment

Comment thread internal/pipeline/diagnose.go Outdated
Comment thread internal/pipeline/checks/run.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.

Copilot review overview

🔵 Needs a closer look

A scoped relock can discard shared transitive edges required by an out-of-scope workflow.

Review tier: Balanced
Findings: None

Issues resolved since last review (2)
Severity Finding
Medium severity internal/​pipeline/​checks/​run.go — Adding every recorded pin here makes a symbolic lock root for a direct bare-SHA workflow reference… View resolved comment
High severity internal/​pipeline/​diagnose.go — Do not merge the live parent graph into the recorded inventory before movement is accepted. If a… View resolved comment
Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

test/scenarios/catalog.yml:1912

  • The updated expectation retains the pin, but the scenario is still named dbot_transient_403_drops_pin. Rename it to describe the asserted retained-pin behavior so scenario selection and failures are not misleading.

internal/lockfile/state.go:469

  • When a scoped relock advances a parent shared with an out-of-scope workflow, this condition drops every previously merged uses edge because the commit changed. uses is a global union for an NWO@Ref (including different sub-action paths), so Save can then garbage-collect children still required by the untouched workflow, violating scoped-run preservation. Track edge contributions per workflow/path, or otherwise retain/recompute edges belonging to workflows outside the current scope before replacing the scoped graph.
			if existing.Commit == commit {
				for _, u := range existing.Uses {
					usesSet[u] = true
				}

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.

Copilot review overview

🔵 Needs a closer look

A moved ref combined with a newly fixable dependency can incorrectly produce a nonzero exit.

Review tier: Balanced
Findings: None

Suppressed comments (1)

internal/pin/plan.go:136

  • Default runs now emit ref-moved, but this remains non-blocking only when the planner returns before resolving anything. If the same workflow also has an unpinned/new ref, planning continues and informationalEntries converts the moved ref into an Investigate entry; both JSON mode and the terminal summary then return errSilent after successfully pinning the new ref. A moved ref should remain a warning regardless of unrelated fixable findings—exclude RefMoved from investigation/exit gating unless an explicit policy requires failure, and add a mixed moved+new-ref regression test.
	repinMoved := repinsMoved(opts) && wr.CountByCategory(checks.RefMoved) > 0 ||
		opts.AcceptMoved && wr.CountByCategory(checks.UnreachablePin) > 0

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.

Copilot review overview

🟡 Changes recommended

Per-workflow scoped commits can overwrite shared global pins with conflicting partial-resolution snapshots.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity internal/​pin/​commit.goSetScoped is applied one workflow at a time even though dependency entries are global. If two…

Comment thread internal/pin/commit.go Outdated

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.

Copilot review overview

🟡 Changes recommended

Pathless recorded transitive resolution and order-dependent scoped graph writes can produce incorrect results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 High severity

New issues introduced by this change (1)
Severity Finding
High severity internal/​lockfile/​state.go — Skipping every workflow in scopedWorkflows assumes each scoped plan will successfully replace its…
Pre-existing issues (1)
Severity Finding
High severity internal/​pin/​commit.goSetScoped is applied one workflow at a time even though dependency entries are global. If two… View comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

internal/pipeline/parse.go:143

  • GetClosure reconstructs dependencies from lockfile pins with Path == "" (internal/lockfile/convertor.go:35-43). Converting every recorded transitive dependency back into an ActionRef therefore asks ResolveAllRecursive to inspect the repository-root action.yml, not the subpath that the parent composite actually used. For a pin such as owner/repo/save@v1, a root composite can introduce unrelated traversal or even a false blocking CompositeLocalPathError. Resolve recorded closure entries with a ref-only lookup, or persist/recover their paths; recursive action-file discovery should start from path-aware current roots.
		refs = append(refs, parserlock.ActionRef{
			Owner: owner,
			Repo:  repo,
			Path:  d.Path,
			Ref:   d.Ref,

Comment thread internal/lockfile/state.go Outdated

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.

Copilot review overview

🟡 Changes recommended

Live graph reconciliation, ref casing, and integration fixture handling contain correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 3 High severity

New issues introduced by this change (2)
Severity Finding
High severity internal/​lockfile/​state.go — This always unions the prior Uses whenever the parent SHA is unchanged. A complete scan where an…
High severity internal/​pin/​commit.go — Lowercasing the entire NWO@​ref key conflates distinct case-sensitive Git refs such as Release
Pre-existing issues (1)
Severity Finding
High severity internal/​lockfile/​state.go — Skipping every workflow in scopedWorkflows assumes each scoped plan will successfully replace its… View comment
Issues resolved since last review (1)
Severity Finding
High severity internal/​pin/​commit.goSetScoped is applied one workflow at a time even though dependency entries are global. If two… View resolved comment
Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

test/integration/run.rb:349

  • This is a snapshot of the mutable v4 ref and feeds live-token scenarios such as dbot_onboarded_clean, which requires zero findings. Since normal runs now resolve v4 live, the next upstream move will emit ref-moved and break those scenarios until this constant is manually updated again. Resolve the fixture head during setup or use an immutable ref for clean live scenarios, keeping movement behavior in stubbed tests.
    test/integration/run.rb:586
  • The generated GraphQL query contains only the $expr0 placeholder; the actual ref is sent in body["variables"]["expr0"]. These branches therefore never match, so the main and v4.2.0 stubs always return CHECKOUT_SHA. Select the fixture SHA from the expression variable instead.
    internal/lockfile/state.go:477
  • ponytail is unrelated to the invariant being documented and makes this comment confusing. Remove the stray label so the comment starts with the lockfile rationale.
				// ponytail: actions.lock stores a global edge union, so keep the

internal/pipeline/diagnose.go:66

  • The recursive parent map is discarded here, and the live dependencies are used only for ref checks; inventory and planning below still use only the recorded graph. If a workflow switches from owner/action/old@v1 to owner/action/new@v1, the lock key and SHA are unchanged but the composite children may differ, so no finding triggers replanning and the lockfile keeps the old closure. Carry the successfully resolved live graph into planning and reconcile it when resolution is complete, while retaining the recorded graph for moved or partial resolutions.
		liveDeps, _, resolveErr = r.ResolveAllRecursive(ctx, pw.Refs)

Comment thread internal/lockfile/state.go Outdated
Comment thread internal/pin/commit.go Outdated
if !isPinOrVerified(e.Resolution) {
continue
}
key := strings.ToLower(e.NWO + "@" + e.Ref)

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.

Copilot review overview

🟡 Changes recommended

Incomplete and blocked resolutions can lose recorded or partially resolved graph edges, and obsolete moved dependencies can prevent required graph replacement.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 3 High severity

New issues introduced by this change (2)
Severity Finding
High severity internal/​lockfile/​state.go — Incomplete updates still contain successfully resolved edges. Skipping their entire ParentMap
High severity internal/​pipeline/​diagnose.go — A CompositeLocalPathError sets LiveComplete false and causes the blocking-error branch below to…
Pre-existing issues (1)
Severity Finding
High severity internal/​pin/​commit.go — Lowercasing the entire NWO@​ref key conflates distinct case-sensitive Git refs such as ReleaseView comment
Issues resolved since last review (2)
Severity Finding
High severity internal/​lockfile/​state.go — This always unions the prior Uses whenever the parent SHA is unchanged. A complete scan where an… View resolved comment
High severity internal/​lockfile/​state.go — Skipping every workflow in scopedWorkflows assumes each scoped plan will successfully replace its… View resolved comment
Suppressed comments (1)

internal/pin/plan.go:139

  • This rejects the whole live graph for movement in any recorded dependency, including one that is no longer in the live closure. For example, after switching sub-action paths at the same repository/ref, an obsolete recorded child that has moved makes movementRejected true; because the root key is unchanged there is no structural finding, so planning retains the old child and never locks the new live child. Only movement for dependencies still present in wr.LiveDeps should veto graph replacement.
	movementRejected := wr.CountByCategory(checks.RefMoved) > 0 && !repinsMoved(opts) ||
		wr.CountByCategory(checks.UnreachablePin) > 0 && !opts.AcceptMoved
	useLiveGraph := wr.LiveComplete && (wr.LiveGraphChanged || repinMoved) && !movementRejected

Comment on lines +436 to 438
if !update.ReplaceGraph {
continue
}
Comment on lines +70 to +72
wr.LiveDeps, wr.LiveParents = alignLiveRootKeys(liveDeps, liveParents, pw.RecordedDeps, pw.Refs)
wr.LiveComplete = recursiveErr == nil
wr.LiveGraphChanged = wr.LiveComplete && !sameGraph(wr.LiveDeps, wr.LiveParents, pw.RecordedDeps, pw.RecordedParents)
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