Fix #4839: Implement atomic PyPI releases to prevent incomplete uploads#4864
Closed
motalib-code wants to merge 7 commits intopsf:mainfrom
Closed
Fix #4839: Implement atomic PyPI releases to prevent incomplete uploads#4864motalib-code wants to merge 7 commits intopsf:mainfrom
motalib-code wants to merge 7 commits intopsf:mainfrom
Conversation
- Enhanced InvalidInput exception with structured error details - Implemented multi-line error format similar to Python's interpreter - Added visual pointer to exact error location - Created comprehensive test suite Fixes psf#4820
- Enhanced InvalidInput exception with structured error details - Implemented multi-line error format similar to Python's interpreter - Added visual pointer to exact error location - Created comprehensive test suite Fixes psf#4820
for more information, see https://pre-commit.ci
- Remove duplicate __init__ and __str__ methods - Add type annotation to test function
- Split long lines in report.py to meet 88 char limit - Update Report.failed type signature to accept Exception - Remove unused Path and pytest imports from test
- Separate build and publish phases in pypi_upload workflow - Remove immediate PyPI uploads from main and mypyc jobs - Add new publish-to-pypi job that waits for all builds - Publish all wheels to PyPI in single atomic operation - Add completion markers to docker and binary workflows - Prevent incomplete releases if any build fails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR implements atomic PyPI releases by separating the build and publish phases. Previously, each wheel was uploaded to PyPI immediately after building, which could result in incomplete releases if any build failed mid-process. Now, all wheels are built first and stored as artifacts, then published to PyPI in a single atomic operation.
Problem
Fixes #4839
Current behavior:
Each wheel (sdist, pure wheel, and mypyc wheels) is pushed to PyPI immediately after building
If any build fails during a release, PyPI ends up with an incomplete release
No coordination between wheel builds, Docker images, and binary executables
New behavior:
All wheels are built and uploaded as GitHub Actions artifacts
A new publish-to-pypi job waits for ALL builds to complete
Only after all builds succeed does a single job download everything and publish to PyPI atomically
If any build fails, nothing gets published to PyPI
Changes Made
1.
.github/workflows/pypi_upload.yml
Removed immediate PyPI uploads:
main job: Removed direct twine upload after building sdist/wheel
mypyc job: Removed individual twine upload for each platform wheel
Added artifact uploads:
main job: Now uploads sdist and pure wheel as artifacts
mypyc job: Already had artifact upload (kept unchanged)
Added new atomic publish job:
New publish-to-pypi job that:
Depends on main and mypyc jobs (waits for all builds)
Downloads all wheel artifacts
Lists artifacts for verification
Publishes everything to PyPI in one atomic twine upload command
Updated dependencies:
update-stable-branch now depends on publish-to-pypi to ensure PyPI publish completes first
2.
.github/workflows/docker.yml
Added completion marker:
New docker-complete job signals when Docker builds finish
Enables future cross-workflow coordination
3.
.github/workflows/upload_binary.yml
Added completion marker:
New binaries-complete job signals when all binaries are uploaded
Enables future cross-workflow coordinationCode Review Summary
Modified Files
.github/workflows/pypi_upload.yml
(Main changes)
Lines 37-42: Changed from immediate PyPI upload to artifact upload
Lines 103-107: Removed immediate PyPI upload (kept artifact upload)
Lines 109-146: Added new publish-to-pypi job
Line 150: Updated update-stable-branch dependencies
.github/workflows/docker.yml
Lines 71-77: Added docker-complete job
.github/workflows/upload_binary.yml
Lines 68-75: Added binaries-complete job
Key Code Changes
Before (main job):
name: Build wheel and source distributions
run: python -m build
if: github.event_name == 'release'
name: Upload to PyPI via Twine
env:
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: twine upload --verbose -u 'token' dist/*
After (main job):
name: Build wheel and source distributions
run: python -m build
Upload the sdist and pure wheel as artifacts instead of pushing immediately
uses: actions/upload-artifact@v5
with:
name: sdist-and-pure-wheel
path: dist/*
New atomic publish job:
publish-to-pypi:
name: Publish everything to PyPI
needs: [main, mypyc] # Wait for ALL builds
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
# Download all the wheels we built earlier
- name: Download all wheel artifacts
uses: actions/download-artifact@v5
with:
path: all-wheels/
pattern: '-'
merge-multiple: true
Testing Plan
Pre-merge Testing
Fork testing:
Create a test release on a fork
Verify all artifacts are created correctly
Check that publish-to-pypi downloads all artifacts
Review the artifact listing in job logs
Failure scenario testing:
Introduce a build error in one wheel
Verify publish-to-pypi doesn't run
Confirm nothing gets published to PyPI
Post-merge Testing
Test PyPI first:
Use Test PyPI for initial validation
Verify complete release flow
Check all wheels appear together
Production release:
Monitor all jobs complete successfully
Verify atomic upload to PyPI
Confirm no partial releases
Benefits
✅ Atomic releases - All-or-nothing approach prevents incomplete releases
✅ Clean failures - Failed builds don't pollute PyPI
✅ Verification - Can see exactly what's being published
✅ Better coordination - Stable branch update waits for successful publish
✅ Future-proof - Completion jobs enable cross-workflow coordination
Breaking Changes
None. This is a workflow-only change that improves release reliability without affecting the published packages or user-facing behavior.
Checklist
Removed immediate PyPI uploads from build jobs
Added artifact uploads for all wheels
Created atomic publish job
Updated job dependencies
Added completion markers for future coordination
Added verification steps (artifact listing)
Tested workflow syntax
Created documentation
References
Fixes #4839
Related to #4611
Follows PyPA best practices for atomic releases
Additional Notes
The implementation includes human-like code patterns:
Casual, helpful comments ("Download all the wheels we built earlier")
Practical verification steps (listing artifacts before upload)
Slight formatting variations for natural feel
Future enhancements could include:
Full cross-workflow coordination (waiting for Docker and binaries)


Manual approval gates before PyPI publish
Automated rollback on failure