chore(ci): pin every workflow action to a full commit SHA - #20
Merged
Conversation
release.yml and ci.yml were the outliers in this org: codeql.yml, dependency-review.yml and secret-scan.yml in this same repo were already SHA-pinned, as are openadapt-ops and openadapt-tray. The one that mattered was `pypa/gh-action-pypi-publish@release/v1` on the PyPI publish job. `release/v1` is a *branch*, not a tag, so whoever can move that branch decides what code runs with this repo's PyPI Trusted Publishing OIDC identity (`id-token: write`, `environment: pypi`) at a moment nobody is watching. The remaining eleven `uses:` lines floated on major tags, which are mutable for the same reason. Pinning only -- no action was upgraded. Each SHA was resolved from the upstream repo's own tag refs (`git ls-remote --tags`, all lightweight, so each ref is already the commit) and re-confirmed as a commit object via the GitHub API, then checked to be exactly what the floating ref resolves to today: actions/checkout@v7 -> 3d3c42e5 (v7.0.1; v7 == v7.0.1) actions/setup-python@v7 -> 5fda3b95 (v7.0.0; v7 == v7.0.0) actions/setup-node@v7 -> 820762786 (v7.0.0; v7 == v7.0.0) actions/upload-artifact@v7 -> 043fb46d (v7.0.1; v7 == v7.0.1) actions/download-artifact@v8 -> 3e5f45b2 (v8.0.1; v8 == v8.0.1) pypa/gh-action-pypi-publish -> ba38be9e (v1.14.1) For the publish action, `release/v1` currently points at the same commit as the v1.14.1 tag, so pinning to the tag's commit is byte-identical to what runs today -- it just stops moving underneath us. `test_release_actions_are_pinned_to_commits` is ported from openadapt-tray's test of the same name so this cannot regress; it lives in tests/test_distribution.py, which the release workflow already runs as its own version-consistency gate before any publish step. A companion test covers the other workflows and requires the trailing `# vX.Y.Z` comment so the human-readable version is never lost to the SHA. Both fail against the pre-change workflows (12 floating refs) and pass after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
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.
What was floating
ci.ymlandrelease.ymlwere the only two workflows in this repo still on mutable refs.codeql.yml,dependency-review.ymlandsecret-scan.ymlwere already fully SHA-pinned, as areopenadapt-opsandopenadapt-tray. Twelveuses:lines in total:actions/checkout@v7actions/setup-python@v7actions/checkout(×3)@v7actions/setup-python@v7actions/setup-node@v7actions/upload-artifact(×2)@v7actions/download-artifact@v8pypa/gh-action-pypi-publish(×2)@release/v1The last one is the reason this is not a style nit.
release/v1is a branch, not a tag. It sits on thepypi-publishjob, which runs withid-token: writeandenvironment: pypi— i.e. this repo's PyPI Trusted Publishing identity. Whoever can move that branch chooses what code runs with those credentials, on a tag push, when nobody is watching. Major tags like@v7are mutable for the same reason, just less pointed.How each SHA was resolved
Pinning only — no action was upgraded. Every SHA came from the upstream repo's own refs, not from another repo's workflow and not from a search result:
git ls-remote --tags <upstream>for each action. None of theactions/*tags produced a^{}peel line, so they are lightweight tags whose ref is the commit.gh api repos/<owner>/<repo>/commits/<sha>.actions/checkout3d3c42e5aac5ba805825da76410c181273ba90b1v7==v7.0.1✅actions/setup-python5fda3b95a4ea91299a34e894583c3862153e4b97v7==v7.0.0✅actions/setup-node820762786026740c76f36085b0efc47a31fe5020v7==v7.0.0✅actions/upload-artifact043fb46d1a93c77aae656e7c1c64a875d1fc6a0av7==v7.0.1✅actions/download-artifact3e5f45b2cfb9172054b4087a40e8e0b5a5461e7cv8==v8.0.1✅pypa/gh-action-pypi-publishba38be9e461d3875417946c167d0b5f3d385a247release/v1HEAD == v1.14.1 ✅No floating ref resolved to anything older than a pin used elsewhere in the org, so nothing needed noting and nothing was silently upgraded.
checkout@v7.0.1andsetup-python@v7.0.0independently matched the SHAs already in this repo'scodeql.ymland inopenadapt-ops.The PyPI publish action specifically
pypa/gh-action-pypi-publish's latest v1.x tag is v1.14.1 (annotated tag object2834a314…, dereferencing to commitba38be9e461d3875417946c167d0b5f3d385a247). Therelease/v1branch head is currently that same commit — so this pin changes nothing about what executes, it only stops the ref moving underneath us.openadapt-trayalready pins this exact SHA.Regression guard
test_release_actions_are_pinned_to_commitsis ported fromopenadapt-tray's test of the same name. It lives intests/test_distribution.pyrather than a new file because the release workflow already runs that file specifically as its pre-publish version-consistency gate — so the guard now sits on the publish path itself, not only in CI. A companiontest_every_workflow_action_is_pinned_to_a_commitcovers the other four workflows and additionally requires the trailing# vX.Y.Zcomment, so the human-readable version can't be lost to the SHA.Both were verified to actually bite: run against the pre-change workflows they fail and name all 12 floating refs; stripping a version comment fails the second one.
Scope
Pinning only. No trigger, job, step, permission, condition or version was changed — the diff is
uses:lines plus the two tests.python -m pytest -q→ 74 passed;ruff check src tests scripts→ clean; all five workflows still parse as YAML.Note on the publish path
Green checks here do not exercise publishing, and I'm not claiming they do.
release.yml's path filter includes.github/workflows/release.yml, so this PR runs thevalidatejob (explicit dry run, no publish) — that covers the pinnedcheckout/setup-python/setup-node/upload-artifact. Thepypi-publishjob is gated onpushof av*tag or a published Release, neither of which this PR does, so the pinneddownload-artifactandgh-action-pypi-publishare not executed. The correctness argument for those is the resolution table above: each pinned SHA is exactly the commit its floating ref points at today, so the publish job runs identical action code.openadapt-agenton PyPI (latest 2.0.1, 2 releases, last upload 2026-07-23) must be unchanged after this merges, and will be checked.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM