fix(pins): walk the action graph instead of glancing at it - #179
Merged
Conversation
check_transitive_action_pins claimed every reference reachable from a pinned action is itself immutable, and established none of it. It read YAML with a regular expression anchored on `uses:` as the first token of a line, so the ordinary composite form `- uses: owner/action@ref` matched nothing -- while the sibling expression for this repository's own workflows handled `(?:-\s*)?`. One group of difference. It did not recurse. A nested reference that was itself a SHA ended the walk, so depth two and beyond were never looked at. And it could not tell throttling from a refusal: ~43 sequential API calls with no pacing hit a secondary rate limit, and the 403 was reported as "unreachable" -- which reads as a broken third party rather than as this check asking too fast. maintenance.yml filed exactly that on its first run (#177). References are now read from the parsed document at any nesting, covering composite actions, reusable workflows, `jobs.<id>.uses` and both step spellings without encoding any of them. The walk is breadth-first with a visited set and bounded in depth and nodes, so a cycle terminates. Rate limits are retried with backoff and named as rate limits; an unreadable definition is a finding about the run rather than silence. Eleven self-tests exercise the walk over in-memory graphs with no network. Six mutations were each caught, including reverting to the regex and removing recursion. The real closure is clean at every depth.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes what
maintenance.ymlfiled automatically in #177 on its first successful run.Three gaps in a check that claimed a graph property
1. It read YAML with a regular expression.
uses:had to be the first non-whitespace token on the line, so the ordinarycomposite form was invisible:
The sibling expression in the same file, for this repository's own workflows,
handles
(?:-\s*)?. One group of difference.2. It did not recurse. A nested reference that was itself a SHA ended the walk.
workflow -> A@sha -> B@sha -> C@tagpassed.3. It could not tell throttling from a refusal. ~43 sequential API calls with no
pacing hit a secondary rate limit; the 403 was reported as
unreachable: HTTP Error 403: Forbidden, which reads as a broken third party ratherthan as this check asking too fast.
What it does now
References are read from the parsed document at any nesting — which covers
composite actions, reusable workflows,
jobs.<id>.uses, and both step spellingswithout encoding any of them, and cannot be defeated by key order.
The walk is breadth-first with a visited set and bounded in depth (6) and nodes
(400), so a cycle terminates and a hostile graph cannot run without end. Rate limits
are retried with backoff and named as rate limits. A missing definition stays a
finding about the pin; an unreadable one is a finding about the run rather than
silence.
Proof
Eleven self-tests run the walk over in-memory graphs — no network, so they assert
every run rather than whenever GitHub is reachable: both step forms, depth three, a
cycle, a reusable workflow,
jobs.<id>.uses, a missing definition, an unreadable one,an undigested image, and a local reference that must not be chased.
- uses:)One self-test caught a bug in its own fixture before it caught anything else: the
fake fetcher keyed on repository alone, conflating an action with a reusable workflow
in the same repository.
The real closure now walks clean at every depth (~39s for 43 roots).
Scope decision, stated rather than silently taken
The plan also called for materialising the closure into the tree and checking it
in the blocking tier. I did not do that, deliberately: a network-derived file that
core validates would go red on every Dependabot pin bump, since Dependabot cannot
regenerate it. That trades a real, recurring barrier for marginal gain now that the
advisory lane actually reports (#172, #176). Filed separately so it stays a decision
rather than an omission.
Verification
validate_all— all tiers OK withGH_TOKEN; self-tests pass without oneactionlint— clean