mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #307] [MERGED] feat(workflow-pins): assert every uses: ref is SHA-pinned, and pin the last two #310
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#310
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/307
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 9/18/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15255-pin-actions-sha📝 Commits (4)
5614a72feat(workflow-pins): assert everyuses:ref is SHA-pinned, and pin the last two5156fb6fix(workflow-pins): hold container images to a digest, and stop the flow scan reading prose as structure19be329docs(workflow-pins): unnest the KNOWN_UNPINNED entry-format code spanbbd595eMerge remote-tracking branch 'origin/main' into matt/be-15255-pin-actions-sha📊 Changes
8 files changed (+1107 additions, -29 deletions)
View changed files
📝
.github/workflow-pins/README.md(+78 -2)📝
.github/workflow-pins/check_workflow_pins.py(+362 -11)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+599 -1)📝
.github/workflows/cursor-review.yml(+2 -2)📝
.github/workflows/pr-size.yml(+2 -2)📝
.github/workflows/test-refresh-reviewers.yml(+2 -2)📝
.github/workflows/test-workflow-pins.yml(+55 -3)📝
AGENTS.md(+7 -6)📄 Description
ELI-5
AGENTS.mdsays to pin every GitHub Action by its full commit SHA, because a tag can be moved by whoever owns the action. Two refs in.github/workflows/test-refresh-reviewers.ymlwere still on tags — and they had drifted twice since anyone noticed (@v6→@v7→@v7.0.0), because Dependabot only ever swaps a tag for a narrower tag and never writes a SHA. This pins both, and adds the lint that stops them coming back.What changed
The two pins.
actions/checkout@v7→@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1andactions/setup-python@v7.0.0→@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0— the SHAs already used at 73 and 13 other sites here. I verified against GitHub that both tags currently resolve to exactly those commits (gh api repos/actions/<a>/git/matching-refs/tags/v), so this freezes the code those steps already ran rather than changing it.The assertion.
check_action_pinsin.github/workflow-pins/check_workflow_pins.pyfails anyuses:under.github/workflows/whose ref is not a full 40-hex SHA. It is a separate walk rather than a fourth branch ofcheck_dir, becausecheck_dirreturns early for any file that does not declare aworkflows_refinput — which is most of that directory, and is precisely where both floating refs lived. A pin rule that only covered reusables would have missed the exact drift it exists to catch.KNOWN_UNPINNEDis the debt list, with the same self-draining contract asKNOWN_EXEMPT: a stale entry is an error. Entries are(workflow filename, full uses value)pairs — the file alone would pre-exempt every other action in it, the ref alone would pre-exempt the same floating ref wherever it were copied, and keying on the value means a Dependabot tag bump retires the entry rather than leaving it silently protecting nothing. That last part is how this debt hid in the first place. It ships empty: both refs it was written for are pinned in this change.What the walker does and does not read. Block (
- uses: a/b@ref) and flow spellings, the latter including YAML's implicit single-pair form (steps: [uses: a/b@ref]); skips./paths, the one form naming nothing pinnable, and REPORTS the skip; refuses auses:whose value it cannot read on its line (uses: >-, or the value on the line below) rather than passing it unchecked; and skips|/>block-scalar text, a single-linerun:(shell, not YAML), a trailing comment, and any match inside a quoted scalar — which together are the whole reason this is not agrep. A container image is NOT skipped: it is held to@sha256:<64-hex>, becausedocker://alpine:3.20is a mutable tag on exactly the terms@v7is. The two gaps that remain (a key spelled through a YAML escape; an action input literally nameduses) are recorded as documented limits in the README — both need a YAML parser, a dependency this repo deliberately does not carry.A one-scan refactor.
_in_block_scalarnow answers from a new single-pass_block_scalar_mask. Calling the per-index form once peruses:line made the lint 6.7× slower (0.76s → 5.1s); with the mask it is 0.79s. Behaviour is unchanged, deliberately — the state machine is the same code moved, pinned by an equivalence test asserting the mask equals the per-index answer at every index, on top of the 341 pre-existing tests that already exercise it.Version comments. The four outliers on the artifact actions are normalised to the majority
# v7/# v8spelling. Every SHA in the repo now carries exactly one spelling.create-github-app-token's two deliberate pins are untouched.Deviation from the plan — the smoke-test fixtures are now SHA-pinned
The plan said explicitly not to touch the
@abcfixtures intest-workflow-pins.yml. I changed two of them, in the heredocs that write temp-dir fixtures, and this is the one judgment call worth a reviewer's attention.The block-scalar skip handles the stated hazard: the lint does not flag those lines where they sit, and
check_workflow_pins.pyexits 0 on the repo (155 refs pinned). But the heredocs are written out to real files that the smoke steps then lint with--workflows-dir, where they are structure. Leaving them on@abchad two consequences:steps:warns but does not fail" asserts exit 0 and would have gone red outright — CI failure on a ref that is not what it tests.@abcis not load-bearing in either fixture — neither step's assertion depends on the ref being a non-SHA. One existing unit test (test_notices_never_fail_the_run) does the same substitution locally on its shared fixture, for the same reason and with the same reasoning in a comment; the ~145 other@abcstring fixtures are untouched, because they exercisecheck_dir, which this change does not touch.I also added a fourth smoke step covering the pin check at the CLI, on a fixture that declares no
workflows_refat all — so nothing but the pin check can account for its non-zero exit.Review round — the panel's ten findings (
5156fb6)Eight fixed, one declined with the census, one recorded as a documented limit.
The Medium, and the only hole a floating ref could have walked through.
_unpinnableexempted everydocker://value on the grounds that images are "immutable by registry digest" — but nothing checked that a digest was there, and the suite encodeddocker://alpine:3.20, a mutable tag, as PASSING. Images are now held to@sha256:<64-hex>, matched whole, so a truncated digest fails too, and a 40-hex commit SHA does not pin an image any more than asha256:digest pins an action. Local paths remain the one unpinnable form.The silent miss. YAML's implicit single-pair flow form (
steps: [uses: a/b@v7]) carries neither{nor,, so it matched neither pattern and passed unchecked — the one direction this lint must never fail in.[joins the boundary class.Three false positives on compliant workflows, all in the mid-line flow scan: a trailing comment (
# was {uses: a/b@v7}), a single-linerun: echo '{"uses": "a/b@v1"}', and a flow mapping split across physical lines, where the block pattern claimed the continuation line and carried}]into the ref. The scan now runs comment-stripped; a single-linerun:is skipped on its own key (anchored on the LINE's key, sosteps: [{id: a, run: …}, {uses: …}]is untouched); and_uses_valuetrims trailing flow punctuation. Remaining matches are quote-masked on the strict reading, not the weak one — the weak reading would let a stray apostrophe in an unquoted scalar ([{name: Don't, uses: a/b@v7}]) wedge a scalar open and drop a real floating ref out of coverage. A false alarm is loud; a silent miss is not.Two tests that could not fail. The mask-equivalence test was
assertEqual(mask, [_in_block_scalar(…)])after_in_block_scalarbecame one line over the mask — it compared the mask with itself, so the rewrite the PR body called "pinned by an equivalence test" was pinned by nothing. The expected mask is now a literal. And the pin smoke step asserted only a non-zero exit, which a traceback satisfies; it now greps forby tag, not by commit SHA.Reporting.
_unpinnable's reason was discarded despite a docstring saying it "keeps the skip visible at the call site"; it is now printed (not pinnable <file>:<line>: <value> — <reason>) and counted, so swapping a pinned ref for a local path cannot quietly shrink the coverage number. Auses:with no@at all is no longer described as pinned "by tag" — different defect, different remedy, own message. And oneKNOWN_UNPINNEDentry covering two identical sites is counted once, matchingcheck_dir.Declined: the
# v7.0.1→# v7normalisation. The rule is one SHA, one spelling — not "prefer the major". Before this PR,upload-artifact@043fb46was 12×# v7/ 2×# v7.0.1anddownload-artifact@3e5f45bwas 14×# v8/ 2×# v8.0.1, whilecheckout@3d3c42eis 75×# v7.0.1andsetup-python@5fda3b9is 14×# v7.0.0. So the four edits move outliers onto their own SHA's majority, and the new pins take theirs for the same reason.# vNis also the spelling AGENTS.md documents.Acceptance
All seven criteria are met. The two refs are pinned; the assertion exists with a self-draining exemption list; it does not flag the heredoc fixture and the repo lint exits 0; tests cover a floating ref failing, a SHA-pinned ref passing, and a stale entry erroring; the four version-comment outliers are normalised with
create-github-app-tokenleft alone; the suite is green.Sweeps
.github/workflows/: 156uses:directives, 155 live (one is the heredoc fixture). 155/155 SHA-pinned, 0 floating, 0 exempt..github/workflows/: 27uses:directives acrossdocs/callers/*.md,README.md, two directory READMEs and one Python test fixture. All 27 are placeholders or truncated illustrations (@<full-commit-sha>,@<sha>,@8c4ff3e…); none teaches a floating tag, and none is executable. Nothing to fix there, and the new lint deliberately does not reach them.uses:spellings (uses: >-, value on the next line): 0 occurrences in the repo, so the new refusal changes nothing today.Provenance
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py': 382 passed, 0 failed (341 before this PR, 39 new, 2 rewritten);python3 .github/workflow-pins/check_workflow_pins.py: exit 0, 155 refs SHA-pinned, 0 exempt, 0 not pinnable, 0.82s (the pre-refactor baseline was 0.76s; the 6.7x regression the mask fixed is not back); all fourtest-workflow-pins.ymlCLI smoke steps extracted and reproduced locally against5156fb6: 4/4 as specified, including the exit-0 one and the new grep assertion;check_agents_md.py --root .: passed (the same 2 pre-existing warnings — 156-lineAGENTS.md, no CODEOWNERS); the other six Python suites (agents-md-integrity, cursor-review, groom 402, public-repo-hygiene 204, refresh-reviewers 53, linear-ticket): all OK; both pin SHAs and all four version comments verified against GitHub's tag refs, and the spelling census re-run againstorigin/main.test-workflow-pins.ymlheredoc fixtures are SHA-pinned against the plan's explicit instruction, and one existing unit test's fixture is locally substituted — both explained under "Deviation from the plan" above. The review round then went beyond the stated acceptance criteria in one place:docker://images are now CHECKED rather than exempted, which no criterion asked for, because the exemption as written carved out a category of the drift the check exists to ban.Residual
The
# vNversion comment is not linted, only the SHA. The convention is "full commit SHA with a trailing# v1comment", and the new check asserts only the first half. All 155 pinned refs satisfy the second half today, so there is nothing to fix — but nothing stops a future ref landing as a bare SHA with no comment, which is the form that makes a pin unreadable at review time. Worth its own change; it was outside the stated acceptance criteria and adding it would have widened this diff into unrelated files.Comment whitespace is inconsistent and unlinted. 38 of the 155 pinned refs write two spaces before the
#and 118 write one (e.g.bump-pr-derisk-callers.yml:147,test-linear-ticket.yml:41,45). Purely cosmetic, no runtime effect, and the plan named only the four version-text outliers, which are fixed. I left the whitespace alone rather than churning 38 lines across the workflow directory in a PR about pinning; normalising it is a one-linesedwhenever someone wants it.The refusal of
uses: >-denies a spelling GitHub Actions itself accepts. I checked rather than assumed: that YAML is valid and Actions would resolve it, so this is a policy refusal, not a claim the capability does not exist. It is stated as such in the error message, which names the one-line remedy (write the action inline). It affects 0 sites in the repo today. If a legitimate need for the folded spelling ever appears, the right fix is to teach the walker the continuation line, not to add an exemption — the debt list deliberately cannot silence this error.This repo still runs no
pinact/zizmoron itself. The new check is a hand-rolled 40-hex assertion, not the pin-validation that consumer CI runs, so it enforces the SHA shape but not everything those tools check. Bringing either into this repo's own CI is a separate change with its own dependency and runtime cost.Closes #172
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
?and line-terminator-agnostic**#311