Skip to content

Fix #4839: Implement atomic PyPI releases to prevent incomplete uploads#4864

Closed
motalib-code wants to merge 7 commits intopsf:mainfrom
motalib-code:fix/atomic-pypi-releases
Closed

Fix #4839: Implement atomic PyPI releases to prevent incomplete uploads#4864
motalib-code wants to merge 7 commits intopsf:mainfrom
motalib-code:fix/atomic-pypi-releases

Conversation

@motalib-code
Copy link

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

  • name: Upload sdist and wheel as artifacts
    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

# Quick check to see what we're about to upload
- name: List all artifacts
  run: ls -lah all-wheels/

# Upload everything in one go - this is the atomic part!
- name: Publish all wheels to PyPI
  env:
    TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
  run: |
    twine upload --verbose -u '__token__' all-wheels/*

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
Screenshot 2025-11-24 201356
Screenshot 2025-11-24 201302

motalib-code and others added 7 commits November 24, 2025 16:25
- 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
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release workflow: Atomic PyPI pushing?

2 participants