From c002ec0f0a8e6246f65ddee2afeda4e822b76431 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 7 Aug 2026 12:49:38 +0000 Subject: [PATCH 1/3] fix: stop the release from trying to publish its dependencies Every release since v2.11.0 has failed, ten in a row, ending with: 403 Invalid API Token: project-scoped token is not valid for project: 'PyYAML' The build runs `pip wheel -w dist .`, which resolves and writes a wheel for the project *and every dependency*, so dist/ holds commit_check and pyyaml. Reproduced locally: $ pip wheel -w dist . commit_check-2.13.4-py3-none-any.whl pyyaml-6.0.3-cp311-...-manylinux_2_28_x86_64.whl That was harmless while the upload named what it wanted. #453 migrated from twine to pypa/gh-action-pypi-publish and the scoping was lost in the move: - run: twine upload dist/commit_check* + uses: pypa/gh-action-pypi-publish@... The action has no file selection -- it uploads the whole directory -- so PyPI is asked to accept PyYAML under a token scoped to commit-check, and correctly refuses. Nobody noticed because the release still works. Uploads happen per file and commit_check sorts before pyyaml, so our wheel lands, then the job goes red on someone else's. The package is on PyPI; only the workflow looked broken, which is a state that is easy to keep ignoring. `--no-deps` builds our wheel alone, which is all this workflow ever meant to publish: $ pip wheel --no-deps -w dist . commit_check-2.13.4-py3-none-any.whl Also adds a guard after the build. The publish step cannot select files, so "dist/ contains only our artifacts" is a precondition it depends on and cannot verify; the guard checks it where the problem is fixable and names it, instead of surfacing as a 403 about a project we do not own. Checked against both states: it fails on the old dist/ and passes on the new one. Note for later, not changed here: this workflow has only ever published wheels -- `pip wheel` produces no sdist, and PyPI has none for any version. Switching the build to `python -m build` would add one, but that changes what a release contains and belongs in its own change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- .github/workflows/publish-package.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index f869e808..834e9179 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -28,10 +28,22 @@ jobs: # Install dependencies python -m pip install --only-binary :all: --upgrade pip python -m pip install --only-binary :all: ".[ci]" - # Build wheel - python -m pip wheel -w dist . + # Build wheel. --no-deps matters: without it pip also resolves and + # writes every dependency's wheel into dist/, and the publish step + # below uploads the whole directory, so PyPI is asked to accept + # someone else's package under our project-scoped token. + python -m pip wheel --no-deps -w dist . # Check distribution twine check dist/commit_check* + # The publish step has no way to select files, so dist/ being ours + # alone is a precondition it cannot check for itself. Fail here, with + # a message that names the problem, rather than at the upload with a + # 403 about a project we do not own. + foreign=$(find dist -mindepth 1 -not -name 'commit_check-*' -printf '%f\n') + if [ -n "$foreign" ]; then + echo "::error::dist/ must contain only commit-check artifacts, found: $foreign" + exit 1 + fi - name: Create attestations uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 From b7e44ee53fd4ac89e67eb710b2c7544dbd2696a3 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 7 Aug 2026 12:53:53 +0000 Subject: [PATCH 2/3] fix: let the guard name our wheels, not merely not-someone-else's Review pointed out the guard tested only the prefix, so anything called commit_check-* passed it -- an sdist, a stray .txt, a leftover directory. Confirmed against a dist/ holding all four shapes: the old expression flagged just pyyaml and waved the other three through. The tighter form says what the step actually depends on. dist/ should hold our wheels and nothing else, so a release that starts producing something else should stop and be looked at, rather than be uploaded because it happened to carry the right prefix. That includes an sdist, deliberately. Adding one is a change to what a release contains, and it should be made on purpose -- the guard failing with the file named is the prompt to update this line, not an obstacle to route around. Error message now says "wheels" rather than "artifacts", matching what is being checked. Verified by extracting the guard from the workflow YAML and running it verbatim against the two real dist/ directories from the earlier reproduction: exit 1 naming pyyaml on the pre-fix build, exit 0 on the --no-deps build. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- .github/workflows/publish-package.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index 834e9179..ddbd4fe8 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -35,13 +35,18 @@ jobs: python -m pip wheel --no-deps -w dist . # Check distribution twine check dist/commit_check* - # The publish step has no way to select files, so dist/ being ours - # alone is a precondition it cannot check for itself. Fail here, with - # a message that names the problem, rather than at the upload with a - # 403 about a project we do not own. - foreign=$(find dist -mindepth 1 -not -name 'commit_check-*' -printf '%f\n') + # The publish step has no way to select files, so dist/ holding + # exactly what we mean to upload is a precondition it cannot check + # for itself. Fail here, with a message that names the problem, + # rather than at the upload with a 403 about a project we do not own. + # + # This names our own wheels specifically, not merely "not someone + # else's": a release that starts producing anything else -- an sdist, + # a stray file, a leftover directory -- should stop here and be + # looked at, not be uploaded because it happened to carry our prefix. + foreign=$(find dist -mindepth 1 \( ! -type f -o ! -name 'commit_check-*.whl' \) -printf '%f\n') if [ -n "$foreign" ]; then - echo "::error::dist/ must contain only commit-check artifacts, found: $foreign" + echo "::error::dist/ must contain only commit-check wheels, found: $foreign" exit 1 fi From a89753f1bc592600aa5588bb499167a45ebd0119 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 7 Aug 2026 13:29:50 +0000 Subject: [PATCH 3/3] fix: drop the dist guard, keep the one-line fix Maintainer review: --no-deps is the fix, and the guard was two thirds of the diff for something that only fires if someone later removes the flag. Agreed, and removed. The argument for keeping it was that the 403 is a weak signal -- uploads run file by file and commit_check-* sorts before pyyaml-*, so our wheel lands on PyPI and only then does the job go red. The release works and the red reads as noise, which is how ten of them were ignored. But that does not justify eleven lines guarding a flag that now carries a comment saying why it exists; removing it would be a deliberate act, and the guard added a fresh way for a release to fail (find -printf is GNU-only, and any false positive blocks publishing outright). What ships is one changed word and the explanation for it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- .github/workflows/publish-package.yml | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index ddbd4fe8..bc8b9f65 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -28,27 +28,13 @@ jobs: # Install dependencies python -m pip install --only-binary :all: --upgrade pip python -m pip install --only-binary :all: ".[ci]" - # Build wheel. --no-deps matters: without it pip also resolves and - # writes every dependency's wheel into dist/, and the publish step - # below uploads the whole directory, so PyPI is asked to accept - # someone else's package under our project-scoped token. + # Build wheel. --no-deps is load-bearing: without it pip also writes + # every dependency's wheel into dist/, and the publish step uploads + # the whole directory (it has no file-selection input), so PyPI is + # asked to accept someone else's package under our scoped token. python -m pip wheel --no-deps -w dist . # Check distribution twine check dist/commit_check* - # The publish step has no way to select files, so dist/ holding - # exactly what we mean to upload is a precondition it cannot check - # for itself. Fail here, with a message that names the problem, - # rather than at the upload with a 403 about a project we do not own. - # - # This names our own wheels specifically, not merely "not someone - # else's": a release that starts producing anything else -- an sdist, - # a stray file, a leftover directory -- should stop here and be - # looked at, not be uploaded because it happened to carry our prefix. - foreign=$(find dist -mindepth 1 \( ! -type f -o ! -name 'commit_check-*.whl' \) -printf '%f\n') - if [ -n "$foreign" ]; then - echo "::error::dist/ must contain only commit-check wheels, found: $foreign" - exit 1 - fi - name: Create attestations uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1