Context
PR #820 added permissions: id-token: write to the test-wheels-build job in build-test.yml to work around a GitHub Actions parse-time validation issue.
The problem: build-test.yml is triggered by pull_request, which implicitly sets id-token: none. The reusable workflow lib-build-and-push.yml contains an upload_pypi job that declares id-token: write. GitHub validates permissions statically — before evaluating any if: conditions — so even though upload: false means upload_pypi never runs, the caller must still grant the permission the job declares.
@Lorak-mmk correctly pointed out in the PR review:
"This also gives unnecessary permissions to the whole workflow, no? This job could run perfectly well without write permissions, but now it has them. I wonder if there are other possible solutions, for example splitting the upload job into a separate file."
Root Cause
The upload_pypi job inside lib-build-and-push.yml is effectively dead code. None of the four callers ever pass upload: true:
| Caller |
upload: passed |
Has own publish job? |
build-test.yml |
false |
No |
build-pre-release.yml |
(default = false) |
No |
build-push.yml |
false |
Yes |
publish-manually.yml |
false |
Yes |
build-push.yml and publish-manually.yml already implement publishing as separate jobs in the caller workflow. This is a deliberate workaround for pypa/gh-action-pypi-publish#166: PyPI Trusted Publishing embeds the caller workflow path in the OIDC token, so publishing must happen in the caller workflow, not inside a reusable workflow.
Solution
- Remove the
upload_pypi job and upload input from lib-build-and-push.yml
- Rename
lib-build-and-push.yml → lib-build.yml (it no longer handles uploading)
- Update all caller workflows (
build-test.yml, build-push.yml, publish-manually.yml, build-pre-release.yml) to reference the new path
- Remove
permissions: id-token: write and with: upload: false from build-test.yml — no longer needed
- Update the TODO comments in
build-push.yml and publish-manually.yml — the separate publish job is now the intended design, not a temporary workaround
Result
build-test.yml no longer grants any elevated permissions (principle of least privilege restored)
- The reusable workflow contains only what it actually does: building wheels and the source distribution
- Publishing remains a caller-side responsibility, consistent with how PyPI Trusted Publishing works
Context
PR #820 added
permissions: id-token: writeto thetest-wheels-buildjob inbuild-test.ymlto work around a GitHub Actions parse-time validation issue.The problem:
build-test.ymlis triggered bypull_request, which implicitly setsid-token: none. The reusable workflowlib-build-and-push.ymlcontains anupload_pypijob that declaresid-token: write. GitHub validates permissions statically — before evaluating anyif:conditions — so even thoughupload: falsemeansupload_pypinever runs, the caller must still grant the permission the job declares.@Lorak-mmk correctly pointed out in the PR review:
Root Cause
The
upload_pypijob insidelib-build-and-push.ymlis effectively dead code. None of the four callers ever passupload: true:upload:passedbuild-test.ymlfalsebuild-pre-release.ymlfalse)build-push.ymlfalsepublish-manually.ymlfalsebuild-push.ymlandpublish-manually.ymlalready implement publishing as separate jobs in the caller workflow. This is a deliberate workaround for pypa/gh-action-pypi-publish#166: PyPI Trusted Publishing embeds the caller workflow path in the OIDC token, so publishing must happen in the caller workflow, not inside a reusable workflow.Solution
upload_pypijob anduploadinput fromlib-build-and-push.ymllib-build-and-push.yml→lib-build.yml(it no longer handles uploading)build-test.yml,build-push.yml,publish-manually.yml,build-pre-release.yml) to reference the new pathpermissions: id-token: writeandwith: upload: falsefrombuild-test.yml— no longer neededbuild-push.ymlandpublish-manually.yml— the separate publish job is now the intended design, not a temporary workaroundResult
build-test.ymlno longer grants any elevated permissions (principle of least privilege restored)