Fix Publish to PyPI step being silently skipped on tag push#229
Conversation
…ttestation permissions Co-authored-by: alexlib <747110+alexlib@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a long-standing silent skip of the upload_pypi job on tag pushes. Previously, a single failed matrix combination in build_wheels (out of 9: 3 OS × 3 Python) would cause GitHub Actions to propagate a skipped status to upload_pypi, silently preventing any PyPI release. The fix introduces always() and explicit result checks in the job condition, and adds attestations: write / contents: read permissions required by newer versions of pypa/gh-action-pypi-publish.
Changes:
- Updated the
upload_pypijobifcondition to usealways()and explicitly checkneeds.build_sdist.result == 'success'andneeds.build_wheels.resultbeing either'success'or'failure' - Added
attestations: writeandcontents: readpermissions to theupload_pypijob
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| github.event_name == 'push' && | ||
| startsWith(github.ref, 'refs/tags/') && | ||
| needs.build_sdist.result == 'success' && | ||
| (needs.build_wheels.result == 'success' || needs.build_wheels.result == 'failure') |
There was a problem hiding this comment.
The condition needs.build_wheels.result == 'failure' is satisfied both when only one matrix leg fails (the intended partial-failure case) and when every single wheel build fails (all 9 matrix jobs fail). In the total-failure case, upload_pypi will still run and attempt to publish — but only the sdist artifact will exist, with no wheels at all.
While skip-existing: true prevents duplicate uploads, publishing a tag release to PyPI with zero wheels is likely unintentional and could result in an incomplete release reaching users.
Consider adding an explicit guard against a complete wheel build failure, for example by restricting the condition to needs.build_wheels.result == 'success' only, and relying on re-runs with skip-existing: true to handle truly flaky partial failures. Alternatively, if partial-failure publishing is genuinely desired, the PR description and/or a workflow comment should document this tradeoff explicitly.
| (needs.build_wheels.result == 'success' || needs.build_wheels.result == 'failure') | |
| needs.build_wheels.result == 'success' |
upload_pypiwas silently skipped whenever any singlebuild_wheelsmatrix combination failed, because GitHub Actions propagates a failedneedsdependency as a skip to downstream jobs. With 9 matrix combinations (3 OS × 3 Python), a single flaky build or test was enough to block all PyPI releases entirely.Changes
upload_pypijob conditionalways()so the job evaluates its own condition even whenbuild_wheelshas failuresneeds.build_sdist.result == 'success'(sdist required) andneeds.build_wheels.result == 'success' || 'failure'(at least attempted, not cancelled/skipped)This pairs correctly with the existing
skip-existing: trueon the publish step — partial wheel sets can be published and completed on re-run.Permissions
attestations: writeandcontents: readrequired bypypa/gh-action-pypi-publish >= v1.12, which generates Sigstore attestations by default💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.