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
Pin github actions to version hashes so that they are more robust against supply chain attacks. E.g., instead of astral-sh/setup-uv@v5, use astral-sh/setup-uv@0x123456
Use the latest versions available.
Do it in both this repo, and in the cicd.yaml in the template.
Spec
What: Replace every third-partyuses: reference in all four workflow files with a full
40-character commit SHA, followed by a # vX.Y.Z comment naming the version that SHA is.
Bump each action to its latest major at the same time (see the pin table). Four files, 17
third-party call sites; the one local reusable-workflow ref is deliberately left alone.
Why: A mutable ref — @v4, @v10, and especially @release/v1 — resolves at run time to
whatever the upstream tag or branch points at today. Anyone who can move that tag (upstream
maintainer, or an attacker who compromises the account) executes arbitrary code inside our CI
with our tokens. A commit SHA is immutable, so a compromised upstream cannot retroactively change
what our workflows run. This matters twice over here: once for this repo, and once for the
template, where every generated project inherits the refs verbatim.
The highest-value single fix is pypa/gh-action-pypi-publish@release/v1 — that is a branch,
not even a tag, so it changes under us on every upstream push, and that step holds the PyPI
trusted-publishing identity of every project generated from this template.
Pin table
Scope confirmed at triage: all four workflow files, not only cicd.yaml. publish.yaml and release.yaml carry the two highest-privilege actions (PyPI publish rights; contents: write tag
pushes), so excluding them would leave the ticket's own rationale unserved.
Do not pin release.yaml:28,uses: ./.github/workflows/cicd.yaml. That is a local
reusable workflow inside the generated repo; it is not a supply-chain surface and a SHA there is
meaningless (it would pin the generated project to a commit of its own that does not exist yet).
Two traps that will silently produce a broken pin
Annotated tags.pypa/gh-action-pypi-publish and python-semantic-release publish annotated tags, so gh api repos/OWNER/REPO/git/ref/tags/vX returns the tag object SHA,
not the commit. Pinning that SHA fails at run time with an unresolvable-action error. It must
be dereferenced (^{}). The two SHAs in the table above are already the dereferenced commits.
Do not invent or recall a SHA. Every hash must be resolved from the API at implementation
time. A hallucinated 40-hex string either fails the run or, far worse, silently pins something
nobody reviewed — which is the exact class of failure this ticket exists to prevent.
Re-resolve all five at build time (upstream may have released since triage; update the table and
say so in the PR body if a version moved):
forrin actions/checkout actions/setup-python astral-sh/setup-uv \
pypa/gh-action-pypi-publish python-semantic-release/python-semantic-release;do
tag=$(gh api repos/$r/releases/latest --jq .tag_name)
sha=$(gh api "repos/$r/git/ref/tags/$tag" --jq .object.sha)
type=$(gh api "repos/$r/git/ref/tags/$tag" --jq .object.type)# dereference annotated tags to the commit they point at
[ "$type"="tag" ] && sha=$(gh api "repos/$r/git/tags/$sha" --jq .object.sha)echo"$r@$sha # $tag"done
Major-version bumps: what actually changes
Triage decision: pin at the latest major, per the ticket text — not at the current major's
tip. That makes this a v4→v7 / v5→v7 / v5→v10 jump, so the breaking changes were checked
against upstream release notes rather than assumed. Findings:
checkout v5+ requires Actions runner ≥ v2.327.1 (node24). GitHub-hosted ubuntu-latest
satisfies this; self-hosted runners in generated projects may not.
checkout v7 blocks checking out fork PRs under pull_request_target / workflow_run.
Neither trigger appears in any of these four files — no impact.
setup-python v6 is node24 (same runner floor). v7 removed the pip-install input,
which is not used here.
setup-uv v6 removed the pyproject-file / uv-file inputs, v8 removed the old custom
version-manifest format. None of the nine setup-uv steps pass anywith: inputs, so both are
no-ops for us.
setup-uv v9 changed prune-cache to default false, and enable-cache defaults to auto
(caching on for hosted runners on push / pull_request). Consequence: Actions cache
usage will grow — here and in every generated project. This is the one real operational cost
of the bump; accepted and recorded, not mitigated.
setup-uv v10 makes autodisable the cache on release, tag-push, pull_request_target
and workflow_run events. release.yaml runs on workflow_dispatch and publish.yaml uses no
setup-uv, so nothing here changes behaviour.
If any of these bumps turns out to break CI, pin that one action at its current major's tip
SHA instead and note it in the PR body — hardening the ref is the goal; the version bump is the
part that may be traded away.
Interaction with #7 and #8 — rebase, not dependency
Both edit the template CI workflow and both declare #4 independent of them:
Refactor: rename cicd.yaml in template #8 renames template/.github/workflows/cicd.yaml → ci.yaml. Apply this spec to the
template CI workflow under whatever name it carries at implementation time; the file's
content and its nine call sites are unaffected by the rename.
Whichever lands first, the other rebases. There is deliberately no Depends-on: line: every
file this spec touches exists on origin/main today (verified at b55021b).
Acceptance criteria
Every third-party uses: in .github/workflows/ci.yaml, template/.github/workflows/{cicd,publish,release}.yaml (the template CI workflow under its
current name) references a full 40-hex commit SHA — 17 call sites, none left on a tag or
branch ref.
Each pinned ref carries a trailing # vX.Y.Z comment naming that SHA's version.
pypa/gh-action-pypi-publish no longer references the release/v1branch.
release.yaml's uses: ./.github/workflows/<template-ci>.yaml local reusable-workflow ref
is left unpinned and otherwise unchanged.
Every SHA was resolved from the GitHub API during implementation (not recalled), and each
one is a commit object — verified for the two annotated-tag actions with gh api repos/OWNER/REPO/git/commits/<sha> --jq .sha returning that same SHA.
The PR body states the five action@sha # version pairs actually used, and flags any that
differ from the table above because upstream released in the meantime.
git grep -nE 'uses:' -- .github template | grep -v '@[0-9a-f]\{40\}' returns exactly one
line: the local ./.github/workflows/… reusable-workflow ref. (On origin/main at b55021b this same command returns 18 lines — 17 third-party refs plus that local one.)
uv run pytest tests is green.
This repo's own CI run on the PR passes on the newly pinned checkout/setup-python/ setup-uv majors — i.e. the bump is proven, not just asserted.
Commit uses a ci: prefix and carries no AI attribution trailer.
Out of scope
Dependabot / Renovate config to keep the pins fresh. Pinned SHAs never update on their own,
so without an updater they freeze and miss upstream security fixes. Confirmed at triage as its
own follow-up ticket for both this repo and the template — not this PR.
Pinning tool versions outside GitHub Actions (pre-commit hook revs, pipx install python-semantic-release in release.yaml's release-notes step, base images).
Any change to what the workflows do — jobs, triggers, matrix, steps. Only uses: refs move,
plus the incidental behaviour that the major bumps bring with them.
# every third-party ref is a 40-hex SHA; only the local reusable workflow may remain
git grep -nE 'uses:' -- .github template | grep -v '@[0-9a-f]\{40\}'
uv run pytest tests # needs network; renders a project and runs its lint/test/pre-commit
Then confirm the PR's own CI run is green — that is the only real proof the major bumps hold.
Pin github actions to version hashes so that they are more robust against supply chain attacks. E.g., instead of
astral-sh/setup-uv@v5, useastral-sh/setup-uv@0x123456Use the latest versions available.
Do it in both this repo, and in the cicd.yaml in the template.
Spec
What: Replace every third-party
uses:reference in all four workflow files with a full40-character commit SHA, followed by a
# vX.Y.Zcomment naming the version that SHA is.Bump each action to its latest major at the same time (see the pin table). Four files, 17
third-party call sites; the one local reusable-workflow ref is deliberately left alone.
Why: A mutable ref —
@v4,@v10, and especially@release/v1— resolves at run time towhatever the upstream tag or branch points at today. Anyone who can move that tag (upstream
maintainer, or an attacker who compromises the account) executes arbitrary code inside our CI
with our tokens. A commit SHA is immutable, so a compromised upstream cannot retroactively change
what our workflows run. This matters twice over here: once for this repo, and once for the
template, where every generated project inherits the refs verbatim.
The highest-value single fix is
pypa/gh-action-pypi-publish@release/v1— that is a branch,not even a tag, so it changes under us on every upstream push, and that step holds the PyPI
trusted-publishing identity of every project generated from this template.
Pin table
Scope confirmed at triage: all four workflow files, not only
cicd.yaml.publish.yamlandrelease.yamlcarry the two highest-privilege actions (PyPI publish rights;contents: writetagpushes), so excluding them would leave the ticket's own rationale unserved.
actions/checkout@v43d3c42e5aac5ba805825da76410c181273ba90b1v7.0.1actions/setup-python@v55fda3b95a4ea91299a34e894583c3862153e4b97v7.0.0astral-sh/setup-uv@v520cfd1bf945f4377ade1205e4dbc17946fc9a30dv10.0.1pypa/gh-action-pypi-publish@release/v1dc37677b2e1c63e2034f94d8a5b11f265b73ba33v1.14.2python-semantic-release/python-semantic-release@v1039dd2052f2ce8282a5d932c31d58a2ca06d2550ev10.6.1Written form, at every call site:
The trailing comment is not decoration — it is the only thing that makes the diff reviewable and
tells a future updater what version it is looking at.
Call sites (17 third-party refs)
.github/workflows/ci.yaml— 3 (checkout, setup-python, setup-uv)template/.github/workflows/cicd.yaml— 9 (the same three, in each oflint,test,docs)template/.github/workflows/publish.yaml— 1 (pypi-publish)template/.github/workflows/release.yaml— 4 (checkout, semantic-release, setup-python, setup-uv)Do not pin
release.yaml:28,uses: ./.github/workflows/cicd.yaml. That is a localreusable workflow inside the generated repo; it is not a supply-chain surface and a SHA there is
meaningless (it would pin the generated project to a commit of its own that does not exist yet).
Two traps that will silently produce a broken pin
pypa/gh-action-pypi-publishandpython-semantic-releasepublishannotated tags, so
gh api repos/OWNER/REPO/git/ref/tags/vXreturns the tag object SHA,not the commit. Pinning that SHA fails at run time with an unresolvable-action error. It must
be dereferenced (
^{}). The two SHAs in the table above are already the dereferenced commits.time. A hallucinated 40-hex string either fails the run or, far worse, silently pins something
nobody reviewed — which is the exact class of failure this ticket exists to prevent.
Re-resolve all five at build time (upstream may have released since triage; update the table and
say so in the PR body if a version moved):
Major-version bumps: what actually changes
Triage decision: pin at the latest major, per the ticket text — not at the current major's
tip. That makes this a
v4→v7/v5→v7/v5→v10jump, so the breaking changes were checkedagainst upstream release notes rather than assumed. Findings:
v2.327.1(node24). GitHub-hostedubuntu-latestsatisfies this; self-hosted runners in generated projects may not.
pull_request_target/workflow_run.Neither trigger appears in any of these four files — no impact.
pip-installinput,which is not used here.
pyproject-file/uv-fileinputs, v8 removed the old customversion-manifest format. None of the nine setup-uv steps pass any
with:inputs, so both areno-ops for us.
prune-cacheto defaultfalse, andenable-cachedefaults toauto(caching on for hosted runners on
push/pull_request). Consequence: Actions cacheusage will grow — here and in every generated project. This is the one real operational cost
of the bump; accepted and recorded, not mitigated.
autodisable the cache onrelease, tag-push,pull_request_targetand
workflow_runevents.release.yamlruns onworkflow_dispatchandpublish.yamluses nosetup-uv, so nothing here changes behaviour.
If any of these bumps turns out to break CI, pin that one action at its current major's tip
SHA instead and note it in the PR body — hardening the ref is the goal; the version bump is the
part that may be traded away.
Interaction with #7 and #8 — rebase, not dependency
Both edit the template CI workflow and both declare #4 independent of them:
template/.github/workflows/cicd.yaml→ci.yaml. Apply this spec to thetemplate CI workflow under whatever name it carries at implementation time; the file's
content and its nine call sites are unaffected by the rename.
ci-gatejob with nouses:step, so it adds no pins.Whichever lands first, the other rebases. There is deliberately no
Depends-on:line: everyfile this spec touches exists on
origin/maintoday (verified atb55021b).Acceptance criteria
uses:in.github/workflows/ci.yaml,template/.github/workflows/{cicd,publish,release}.yaml(the template CI workflow under itscurrent name) references a full 40-hex commit SHA — 17 call sites, none left on a tag or
branch ref.
# vX.Y.Zcomment naming that SHA's version.pypa/gh-action-pypi-publishno longer references therelease/v1branch.release.yaml'suses: ./.github/workflows/<template-ci>.yamllocal reusable-workflow refis left unpinned and otherwise unchanged.
one is a commit object — verified for the two annotated-tag actions with
gh api repos/OWNER/REPO/git/commits/<sha> --jq .shareturning that same SHA.action@sha # versionpairs actually used, and flags any thatdiffer from the table above because upstream released in the meantime.
git grep -nE 'uses:' -- .github template | grep -v '@[0-9a-f]\{40\}'returns exactly oneline: the local
./.github/workflows/…reusable-workflow ref. (Onorigin/mainatb55021bthis same command returns 18 lines — 17 third-party refs plus that local one.)uv run pytest testsis green.checkout/setup-python/setup-uvmajors — i.e. the bump is proven, not just asserted.ci:prefix and carries no AI attribution trailer.Out of scope
so without an updater they freeze and miss upstream security fixes. Confirmed at triage as its
own follow-up ticket for both this repo and the template — not this PR.
pipx install python-semantic-releaseinrelease.yaml's release-notes step, base images).uses:refs move,plus the incidental behaviour that the major bumps bring with them.
cicd.yaml(Refactor: rename cicd.yaml in template #8) or adding theci-gatejob (Add CI gate for easy branch protection setup #7).Verification
Then confirm the PR's own CI run is green — that is the only real proof the major bumps hold.