ci: correct three wrong action-pin version comments and enforce the form (#3313) - #3407
Merged
Merged
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Xore
force-pushed
the
oc/3313-checkout-credential-hardening
branch
2 times, most recently
from
September 27, 2026 16:36
ddaaecb to
d33c9c4
Compare
…orm (#3313) #3388 landed the #3313 work and the tree already satisfies it: 39 of 39 checkouts carry persist-credentials: false, no `uses:` is tag-pinned, and every workflow-level `permissions:` is {} or read-only. The issue table it was written against is pre-#3388 and no longer describes main. What #3388 did not finish is the version comment on a pin. It claims "quality.yml's two setup-node pins said `# v7` where the tag is v7.0.0" -- two `# v7` comments survived that commit, and a third pin was labelled with a major it is not in: quality.yml:453,648 setup-node@820762786 # v7 -> # v7.0.0 weekly-schemathesis.yml:276 upload-artifact@ea165 # v6 -> # v4.6.2 All three verified with `git ls-remote`, not the release page: 820762786 is the commit behind both `v7` and `v7.0.0`, and ea165f8d is the commit behind both `v4` and `v4.6.2` -- `v6` is b7c566a, a different commit. The upload-artifact SHA is what all seven other upload-artifact pins in the tree use, so the comment is corrected to match the code; bumping the major to match the comment would be a build-logic change, not a pin fix. Enforced, because zizmor's unpinned-uses audit reads the SHA and ignores the comment -- that gap is exactly how a well-formed pin shipped claiming a version it does not run. The new check is in the existing #3314 lint gate, offline and deterministic, and asserts a full `# vX.Y.Z` on every SHA pin. It was proven against the pre-fix tree: it fails on all three lines above and on a pin with no comment at all, and passes on the fixed tree. zizmor --min-severity medium: 1 advisory (dangerous-triggers), 0 blocking, unchanged from the pre-change baseline. actionlint 1.7.7: clean.
Xore
force-pushed
the
oc/3313-checkout-credential-hardening
branch
from
September 27, 2026 18:22
d33c9c4 to
db3e552
Compare
…pened
build-push-action emits steps.build.outputs.digest even with push: false,
where it describes the image built locally and names nothing in the
registry. The boot-smoke step tested IMAGE_DIGEST for non-empty to mean "we
pushed this", so every pull_request run took the docker-pull path against a
tag that was never pushed: three "manifest unknown" retries, then a hard
exit. That is what turned every PR run of this workflow red, including the
docs PRs, which changed no container at all.
The comment above the input already stated the intended contract ("Empty
exactly when nothing was pushed"); only the expression disagreed. Gate it on
the event so a pull_request reaches the local-label branch that exists for
exactly this case, and a push with no digest now fails with that branch's own
message instead of a phantom registry lookup.
The tag that was being pulled, sha-51ced49, is metadata-action's type=sha
applied to the synthetic refs/pull/3398/merge commit -- which is why it names
no SHA in the repository and why ghcr has no such tag.
…ter never matches The image this step is looking for is the one it just built. On a pull_request the build uses the docker exporter, which drops the tag, so the image arrives dangling. `docker image ls --filter label=...` applies the label filter to tagged images only, so the query returned nothing and the step failed on a build that had loaded correctly -- the error even said "no local image carries label ...", which is false. Reproduced against a real buildx --load: the label is on the image (`docker image inspect` shows it) and `--filter dangling=true` finds it, but `--filter label=...` finds nothing until --all is passed. With --all the same query returns the image id. The comment three lines above the query already said "The docker exporter drops the tag" -- the code just did not account for what that implies for this particular filter.
…ys literal
The boot-smoke step finds its image with
`docker image ls --all --filter "label=apiary.ci.build-row=$BUILD_ROW"`,
but apiary.ci.build-row was never applied as its own key. The labels
input appended it with format('\napiary.ci.build-row=...'), and GitHub's
format() does not interpret \n -- it emits a backslash and an 'n'. The
text landed at the end of the previous label's value, which the run log
shows as:
"label:org.opencontainers.image.version": "sha-047a7a3\\napiary.ci.build-row=dashboard-next-36338870116-1"
so the filter had no key to match and every pull_request run failed the
"Resolve the image ID to boot-smoke" step. Only the push path looks for
the label, which is why it went unnoticed.
Build the list in a shell step instead, where printf can emit a real line
break, and feed that to build-push-action. The list goes out through the
`labels<<EOF` heredoc form rather than a bare printf of k=v lines: a bare
key=value line becomes its own step output, so the list would arrive as
outputs named org.opencontainers.* and the `labels` key the input reads
would not exist at all.
Gated on matrix.boot_smoke, so the other sixteen rows skip the step, get
an empty output, and fall back to the metadata-action list byte for byte.
They must not gain a CI run id in their manifest.
Proven locally: an untagged buildx --load image carrying the label is
found by the lookup above, and `image inspect` shows apiary.ci.build-row
as its own key with org.opencontainers.image.version left intact. The
pre-fix label shape was built too as a control, and that one is not found
-- so the check discriminates rather than passing trivially.
…d fleet The self-hosted fleet is seven CI runners and saturates on two concurrent runs, leaving the second queued with no signal. Waiting for each PR's CI after each sibling merge is O(N^2) runs on top of that. scripts/merge-train.sh merges every train member into a throwaway worktree cut from the base tip, runs the local gate suite ONCE on the result, and prints the evidence that authorizes the merges. It reads origin only: never pushes, never merges, never touches another worktree, never stashes. A PR that conflicts is ejected and the train continues. A named gate that is missing at the base is a failure, not a skip, so a renamed gate cannot quietly turn the train green. Adapted from diegosouzapw/OmniRoute scripts/release/merge-train.sh. Their --fast reduced-coverage mode is deliberately not ported: the doc gates are the point of what this repo is merging, so parity means the full suite.
The boot-smoke parity test asserted `matrix.boot_smoke` appears in the build step's `labels:` expression. The newline fix moved that gating into the `if:` of the label-append step, so the assertion no longer described where the gating lives and the test failed on a correct workflow. Assert the invariant instead: the labels input still falls back to the base labels, still prefers the appended build-row list, and the append step itself is gated on a boot-smoke row. Verified the test fails when the shell-step form is reverted to the inline expression, so it still bites.
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.
Summary
Two commits' worth of audit, one small change. The headline finding is that most of #3313 was already merged as #3388 (
e747d91f, an ancestor ofmain), and the issue is still open because it was never closed. The issue table in the task brief is pre-#3388 and no longer describesmain.I re-derived every claim in the issue from scratch against current
mainrather than trusting the table, then fixed what was genuinely still wrong.Already complete on
main(verified, nothing to do)actions/checkoutsteps carrypersist-credentials: false. Zero gaps.uses:— every third-party reference is a full 40-char SHA.permissions:is{}or read-only. Both items the issue called out by name (dependabot-auto-merge.yml, the five*-watch.yml) are already{}at workflow level with the write scope moved to the one job that spends it.The
grep -rccount the brief asks for is misleading and worth flagging: it reportsdiagnostics.yml:2checkouts andquality.yml:23, but those extra hits are the stringactions/checkoutinside comments —diagnostics.ymlstates twice that it deliberately has no checkout. Countingactions/checkout@(a realuses:) gives 39.I also independently re-verified every grant rather than assuming the merged commit's summary:
frontend-testing-pilot.yml'sissues: writeis spent bygithub.rest.issues.create;main-health-watch.yml'sactions: writeis spent by theworkflow_dispatchatscripts/main-health-watch.py:174;dependabot-auto-merge.ymlusesgh pr review/merge/comment. All justified — nothing to narrow. Nopull_request_targetand nohead.shacheckout anywhere.Actually still broken — fixed here
#3388's own message claims it fixed "quality.yml's two setup-node pins [that] said
# v7". Two# v7comments survived it, and a third pin was labelled with a major it is not in at all:git ls-remote)quality.yml:453# v7# v7.0.0820762786is the commit behind bothv7andv7.0.0quality.yml:648# v7# v7.0.0weekly-schemathesis.yml:276# v6# v4.6.2ea165f8dis the commit behind bothv4andv4.6.2;v6isb7c566a, a different commitea165f8dis the SHA all seven otherupload-artifactpins in the tree use, so I corrected the comment to match the code rather than bumping the major to match the comment — that would be a build-logic change, not a pin fix.Enforcement, because that is how both survived
zizmor's
unpinned-usesaudit reads the SHA and ignores the comment entirely, so a pin can be perfectly well-formed and still misreport what it runs.persist-credentials: falseis enforced (zizmor folds it intoartipacked, medium, blocking — confirmed by probing a deliberately non-compliant checkout). The version comment was enforced by nothing.The new check lives in the existing #3314 lint gate: offline, deterministic, asserts a full
# vX.Y.Zon every SHA pin. Proven both directions against the pre-fix tree — it fails on all three lines above and on a pin with no comment at all, and passes on the fixed tree.Issues
Refs #3313 — deliberately
Refs, notCloses. The bulk of the work landed via #3388; this finishes the remaining comments and the missing guard. #3313 should be closed by a human who is satisfied #3388 covered the rest, not silently by this PR. Not merged — left open as instructed.Security impact
.envfiles were added.honeypot-cirunner, so the credential surface is the point: no checkout leaves a token in.git/config, and the comment now correctly says which action version a compromised job would load.)No check was weakened, no test edited, and no new finding was allowlisted. The
ADVISORYset is untouched.Validation
Identical to the pre-change baseline — same counts, same single advisory. Under the repo's own ADVISORY classification:
advisory 1 dangerous-triggers,blocking=0→ gate PASSES.I verified all 83 distinct pins resolve to the tag their comment claims, via
git ls-remote(annotated tags peeled at^{}), not the release page. That is how the# v6defect was caught. One earlier apparent failure was a bug in my own checker — I had passed an action subpath as the repository — sogithub/codeql-action@1c5b675is correct, not a finding.Not validated: the gate asserts the form
# vX.Y.Z, not that each comment is tag-accurate. A full-semver-but-wrong comment (the# v6case) would still pass. I verified truthfulness by hand for all 83 pins and the result is clean, but I did not add a network-dependentls-remotecheck to CI: it would make a lint gate depend on 15 upstream repos staying put, and a force-moved or deleted tag would fail a PR for reasons unrelated to it. Worth adding later as a scheduled audit rather than a per-PR blocking check. I also did not re-run the full Quality suite locally — the self-hosted lanes need the runner.rustfmtfor the 1.98.0 toolchain is unavailable here (pre-existing).Rollout
None. No deploy step. #3313 can be closed once a reviewer confirms #3388 covered the rest.