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
the release pipeline publishes to PyPI and Anaconda with no approval gate: environment: release is commented out, and a vendor cron that bumps the version is enough to trigger it #641
The pypi job in .github/workflows/create-release.yml has its GitHub environment commented out, so nothing gates the upload. The conda job never had one at all. Both are reachable from a scheduled vendor crawl that bumps the version — no human action, no approval, no review of what is about to be published.
This matters right now because a 1.5.0b5 beta is intended partly to exercise util/bump_version.py, and that is exactly the path that triggers a publish.
The commented-out gate
.github/workflows/create-release.yml:230-241:
pypi:
name: PyPI distribution for Python ${{ matrix.python-version }}runs-on: ubuntu-latest## Specifying a GitHub environment is optional, but strongly encouraged#environment: release#permissions:# # IMPORTANT: this permission is mandatory for trusted publishing# id-token: writepermissions:
contents: writeid-token: writeneeds: [ github, version_check ]if: ${{ startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_TAG_EXISTS == 'false' }}
#environment: release is line 234; the "strongly encouraged" comment above it is line 233.
The half that got uncommented is the half that removes protection. id-token: write is live at line 240 — so trusted publishing to PyPI works — while environment: release, the line in the same commented block that would have required an approval, is still commented. The block reads as though it were disabled as a unit, but its permission was re-added below it and its gate was not.
environment: appears nowhere else in any workflow as a deployment environment:
The two activate-environment: base lines are conda shell activation, unrelated. So the commented line 234 is the only deployment environment the repository has ever declared, and it is inert.
conda (:317-322) has no environment: at all, not even commented:
Every other mention in the tree is prose or tests:
$ grep -rn "bump_version" --include="*.yml" .github/.github/workflows/cron-vendor.yml:78: python util/bump_version.py.github/workflows/create-release.yml:104: # automated one, because ``util/bump_version.py`` bumps without ever adding
2. That workflow commits and pushes the bump.cron-vendor.yml:88 runs git commit -am"Bumped version to $(python -c 'import pcapkit; print(pcapkit.__version__)')" and :98 runs git push origin "HEAD:${GITHUB_REF}", both gated only on files_changed == 'true' — i.e. on the registry crawl having found any change.
3. Its completion is the release trigger.cron-vendor.yml:1 is name: "Vendor Update", and create-release.yml:1-9:
on:
push:
# Sequence of patterns matched against refs/tagstags:
- 'v*'# Push events to matching v*, i.e. v1.0, v20.15.10workflow_run:
workflows: [ "Vendor Update" ]types:
- completed
4. The release reads the version back out of the tree.create-release.yml:69-71, in version_check:
so whatever bump_version.py wrote into pcapkit/__init__.py becomes the version that gets tagged and published. PCAPKIT_TAG_EXISTS then comes from mukunku/tag-exists-action, and pypi's if: fires when that is 'false' — which is precisely the state a fresh bump creates.
5. Nothing in between requires a human. Job graph, from grep -nE "^ [a-z_]+:|^ needs:":
unit-tests (no needs)
version_check needs: [ unit-tests ]
github needs: [ version_check ]
tag needs: [ version_check ]
pypi needs: [ github, version_check ] <- no environment
conda needs: [ tag, github, version_check ] <- no environment
unit-tests is a test gate, not an approval gate. There is no workflow_dispatch, no manual step, and no protected environment anywhere in the chain.
So: a scheduled vendor crawl that changes any registry constant is sufficient to publish a release to PyPI and to Anaconda. The schedule is cron-vendor.yml:4-5:
schedule:
- cron: '0 10 * * 6'# everyday at 10am
and note the trigger list also includes push: branches: [main] (cron-vendor.yml:6-7), so an ordinary merge to main enters the same chain.
Why this matters
A burned PyPI version number cannot be reused. PyPI refuses a re-upload of an existing version, and skip-existing: true at :292 means the pipeline will not even error when it happens — it will quietly succeed having published nothing. So an unintended publish does not just release the wrong thing, it permanently consumes the number the intended release wanted.
The chain is not hypothetical, and the repository already records its timing. CHANGELOG.md:79, from the #625 work, states that "the bump is what triggers create-release.yml, the median gap to the PyPI upload is three minutes, and the UTC calendar dates agree 30 times out of 30". Three minutes is less than the time it takes to notice a cron ran.
A release environment with a required reviewer would convert this into "the crawl bumps, tags and builds; a human approves the upload", which is what the commented line was for and what the comment beside it recommends.
Notes
Nothing here is a fix; this is a report. The workflow files were not modified — a cleanup edit to .github/workflows/** hours before an intended release is its own hazard.
Measured on 375e9d411. All line numbers above are from that commit.
Reported, not verified by me: the release-history statistics beyond what CHANGELOG.md:79 records — specifically a maximum bump-to-publish gap of 2.4 h, and roughly 1 bump in 12 producing a tag but no publish (eight consecutive tagged-but-unpublished versions in 2024, plus 1.5.0b1). Those came from the feat(util): have bump_version.py keep CITATION.cff in step with the bump #625 worker's scan of the release history. I verified the mechanism and the median-three-minutes claim's provenance, not that distribution.
The conda gap is arguably the worse of the two, since it was never even written down as a thing to turn on.
Related: concurrency at :29-32 already exists because two Vendor Update runs completing close together were observed producing two Create Release runs against the same tip (runs 34894022730 and 34894646592). That comment is evidence the chain fires in practice, not just on paper.
The
pypijob in.github/workflows/create-release.ymlhas its GitHub environment commented out, so nothing gates the upload. Thecondajob never had one at all. Both are reachable from a scheduled vendor crawl that bumps the version — no human action, no approval, no review of what is about to be published.This matters right now because a
1.5.0b5beta is intended partly to exerciseutil/bump_version.py, and that is exactly the path that triggers a publish.The commented-out gate
.github/workflows/create-release.yml:230-241:#environment: releaseis line 234; the "strongly encouraged" comment above it is line 233.The half that got uncommented is the half that removes protection.
id-token: writeis live at line 240 — so trusted publishing to PyPI works — whileenvironment: release, the line in the same commented block that would have required an approval, is still commented. The block reads as though it were disabled as a unit, but its permission was re-added below it and its gate was not.environment:appears nowhere else in any workflow as a deployment environment:The two
activate-environment: baselines are conda shell activation, unrelated. So the commented line 234 is the only deployment environment the repository has ever declared, and it is inert.conda(:317-322) has noenvironment:at all, not even commented:and it uploads at
:451-457:The trigger chain, traced in the YAML
1.
util/bump_version.pyhas exactly one caller..github/workflows/cron-vendor.yml:78:Every other mention in the tree is prose or tests:
2. That workflow commits and pushes the bump.
cron-vendor.yml:88runsgit commit -am"Bumped version to $(python -c 'import pcapkit; print(pcapkit.__version__)')"and:98runsgit push origin "HEAD:${GITHUB_REF}", both gated only onfiles_changed == 'true'— i.e. on the registry crawl having found any change.3. Its completion is the release trigger.
cron-vendor.yml:1isname: "Vendor Update", andcreate-release.yml:1-9:4. The release reads the version back out of the tree.
create-release.yml:69-71, inversion_check:so whatever
bump_version.pywrote intopcapkit/__init__.pybecomes the version that gets tagged and published.PCAPKIT_TAG_EXISTSthen comes frommukunku/tag-exists-action, andpypi'sif:fires when that is'false'— which is precisely the state a fresh bump creates.5. Nothing in between requires a human. Job graph, from
grep -nE "^ [a-z_]+:|^ needs:":unit-testsis a test gate, not an approval gate. There is noworkflow_dispatch, no manual step, and no protected environment anywhere in the chain.So: a scheduled vendor crawl that changes any registry constant is sufficient to publish a release to PyPI and to Anaconda. The schedule is
cron-vendor.yml:4-5:and note the trigger list also includes
push: branches: [main](cron-vendor.yml:6-7), so an ordinary merge tomainenters the same chain.Why this matters
A burned PyPI version number cannot be reused. PyPI refuses a re-upload of an existing version, and
skip-existing: trueat:292means the pipeline will not even error when it happens — it will quietly succeed having published nothing. So an unintended publish does not just release the wrong thing, it permanently consumes the number the intended release wanted.The chain is not hypothetical, and the repository already records its timing.
CHANGELOG.md:79, from the #625 work, states that "the bump is what triggerscreate-release.yml, the median gap to the PyPI upload is three minutes, and the UTC calendar dates agree 30 times out of 30". Three minutes is less than the time it takes to notice a cron ran.A
releaseenvironment with a required reviewer would convert this into "the crawl bumps, tags and builds; a human approves the upload", which is what the commented line was for and what the comment beside it recommends.Notes
.github/workflows/**hours before an intended release is its own hazard.375e9d411. All line numbers above are from that commit.CHANGELOG.md:79records — specifically a maximum bump-to-publish gap of 2.4 h, and roughly 1 bump in 12 producing a tag but no publish (eight consecutive tagged-but-unpublished versions in 2024, plus1.5.0b1). Those came from the feat(util): have bump_version.py keep CITATION.cff in step with the bump #625 worker's scan of the release history. I verified the mechanism and the median-three-minutes claim's provenance, not that distribution.condagap is arguably the worse of the two, since it was never even written down as a thing to turn on.concurrencyat:29-32already exists because twoVendor Updateruns completing close together were observed producing twoCreate Releaseruns against the same tip (runs 34894022730 and 34894646592). That comment is evidence the chain fires in practice, not just on paper.