The problem
.github/workflows/release.yml:
- The whole job has
contents: write (:17) while it runs uv sync and pytest, so any dependency executed during the build has a write token.
${{ inputs.tag || github.ref_name }} is expanded directly into shell at :49, :70 and :95. A manual dispatch with a crafted tag string is a script injection.
- The concurrency group
release-${{ github.ref }} does not serialize a manual dispatch against the tag-push run for the same tag, and both upload with --clobber.
- The published v0.1.0 release has no assets, and
main still reports 0.1.0 many commits past the tag, with no Unreleased section to hold them.
What done looks like
- A build job with
contents: read that uploads the dist as an artifact, and a separate upload job with contents: write that runs no project code.
- The tag is passed through
env: and quoted, and validated against ^v\d+\.\d+\.\d+$.
- The concurrency group is keyed on the resolved tag.
actions/attest-build-provenance on the built wheel and sdist.
- v0.1.0 gets its assets (or a note says why not), and CHANGELOG gets an Unreleased section.
The problem
.github/workflows/release.yml:contents: write(:17) while it runsuv syncand pytest, so any dependency executed during the build has a write token.${{ inputs.tag || github.ref_name }}is expanded directly into shell at:49,:70and:95. A manual dispatch with a crafted tag string is a script injection.release-${{ github.ref }}does not serialize a manual dispatch against the tag-push run for the same tag, and both upload with--clobber.mainstill reports 0.1.0 many commits past the tag, with no Unreleased section to hold them.What done looks like
contents: readthat uploads the dist as an artifact, and a separate upload job withcontents: writethat runs no project code.env:and quoted, and validated against^v\d+\.\d+\.\d+$.actions/attest-build-provenanceon the built wheel and sdist.