You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 860552b
Browse filesBrowse the repository at this point in the historyBrowse files
ci(security): harden checkout credentials, pin actions, fail-closed tag check
Three supply-chain items. The first two carry over from the sibling
project's `v2.2.2` audit (logged in the lockstep checklist but never
scoped to a rung); the third was raised by CodeRabbit reviewing #275 and
declined there because pinning only the new job would have left the repo
inconsistent while claiming to have addressed it.
## `persist-credentials: false` on 15 of 17 checkouts
`actions/checkout` writes the workflow `GITHUB_TOKEN` into `.git/config`,
where anything the job then executes from the checked-out tree -- build
scripts, proc macros, test binaries, `scripts/*.sh`, the MkDocs build --
can read it. On a pull request that tree is by definition unreviewed
code, and nearly every job here compiles or runs it. Highest exposure was
not a `ci.yml` job but `web.yml`'s `build`, whose workflow-level
`permissions:` grant `pages: write` + `id-token: write` while it runs
`trunk`, `cargo doc`, `pip install`, and `mkdocs build`.
Audited per site rather than applied blanket, and that distinction
mattered: **`release-auto.yml`'s `prepare` job genuinely needs the
credential.** It creates an annotated tag and `git push origin "$TAG"`,
which authenticates through exactly that token. The sibling's audit
concluded no job needed it -- that conclusion does NOT transfer here, and
a blanket sweep would have silently broken every future release. It is
now the only checkout in the repository that keeps the token, with the
reason recorded inline so the exception reads as a decision. Its exposure
is bounded by its trigger: `workflow_run` on a completed CI run of
`main`, never a pull request, so the tree it executes is already-reviewed
code.
`antigravity-review.yml` already had the setting and is unchanged.
## The release-tag check now fails closed
It was `git ls-remote --exit-code --tags origin "refs/tags/${tag}"`,
which collapses THREE outcomes into two -- tag present (0), tag absent
(2), and lookup failed (non-zero, various) -- reading any non-zero as
"absent". A transient network blip, an auth hiccup, or a rate limit
therefore sent an ALREADY-RELEASED version down the `should_release=true`
path, re-tagging a release `to-dos/ROADMAP.md`'s ceremony treats as
immutable.
Now `gh api git/matching-refs/tags/<tag>`, chosen over
`git/ref/tags/<tag>` because it answers "absent" with HTTP 200 and an
empty array, so a genuine miss can never be confused with an error and no
error-body parsing is needed. That endpoint matches by PREFIX, so the
exact ref is compared in `jq`. Verified necessary rather than assumed:
against this repository, `v1.2` prefix-matches SEVEN real tags while
exact-matching none, so a naive `length > 0` would report an unreleased
version as already released. Exercised all four cases against the live
API -- existing tag, absent tag, prefix-only match, lookup failure.
The step had no token of its own (it previously used only plain `git`),
so `GH_TOKEN` is added to it; `gh` errors out without one rather than
falling back to an ambient credential.
## All 34 action references pinned to commit SHAs
Across all 8 workflows plus the composite `rust-setup` action. An
upstream tag move could previously change what CI executes with no
repository review. Each pin carries its original tag as a trailing
comment so intent stays readable at a glance.
Also aligned one `actions/upload-artifact@v4` -- introduced with the fuzz
job in the previous commit -- to the `@v7` the rest of the repo already
used, before pinning it.
## Verification
`actionlint` reports the SAME three findings before and after (a
self-hosted runner label, two pre-existing shellcheck notes), confirmed
by linting the HEAD copies side by side -- no new lint surface. All nine
files parse as YAML. The tag-check logic was exercised against the live
GitHub API, including the prefix trap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments