Python Package¶
This guide describes the complete process for releasing a new HARP version to PyPI.
Note
The release process is fully automated via GitHub Actions. When you push a version tag, the workflow automatically builds, tests, and publishes the package.
Prerequisites¶
Before creating a release, ensure:
You have push access to the GitHub repository
All changes are merged to the
mainor version branch (e.g.,0.9)All CI tests are passing
PyPI trusted publishing is configured (see PyPI Trusted Publishing Guide)
Quick Overview¶
The automated release workflow handles:
Version validation - Ensures
pyproject.tomlversion matches the git tagPackage building - Builds wheel in isolated sandbox environment with frontend bundled
Testing - Tests installation on Python 3.13, 3.14, and 3.14t (free-threaded)
Publishing - Publishes to TestPyPI, then production PyPI
GitHub Release - Creates release with changelog and wheel artifacts
Step-by-Step Release Process¶
1. Prepare the Changelog¶
Move unreleased changes to a version-specific changelog file:
# For version 0.9.0
mv docs/changelogs/unreleased.rst docs/changelogs/0.9.0.rst
Edit the file to add version header and release date:
0.9.0 (2025-01-15)
==================
Create a new empty unreleased.rst file for future changes.
See Changelog for complete changelog management workflow.
Commit the changelog updates:
git add docs/changelogs/
git commit -m "docs: prepare changelog for 0.9.0"
2. Set the Version Number¶
Define the version as an environment variable to avoid typos:
export VERSION=0.9.0
For pre-releases, use appropriate suffixes:
export VERSION=0.9.1-rc1 # Release candidate
export VERSION=1.0.0-beta1 # Beta release
export VERSION=1.0.0-alpha1 # Alpha release
3. Update pyproject.toml¶
Update the version in pyproject.toml and verify:
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml && rm pyproject.toml.bak
uv lock
grep "^version" pyproject.toml
4. Commit the Version Change¶
git add pyproject.toml uv.lock
git commit -m "chore: bump version to $VERSION"
5. Create an Annotated Git Tag¶
git tag -a $VERSION -m "Release $VERSION"
Important
Always use the -a flag to create an annotated tag, not a lightweight tag.
The release workflow requires annotated tags.
6. Push Changes and Tag¶
git push origin
git push origin $VERSION
Note
Push the tag after pushing the commit to ensure the tag points to the correct commit.
7. Monitor the Release Workflow¶
Once the tag is pushed, GitHub Actions automatically runs the release workflow:
1. Validates that pyproject.toml version matches tag
└─ Fails if versions don't match (see Troubleshooting)
2. Builds wheel in sandbox environment
├─ Installs dependencies
├─ Builds React frontend
├─ Bundles static assets
└─ Creates wheel with twine validation
3. Tests the built wheel
├─ Python 3.13 (standard)
├─ Python 3.14 (standard)
└─ Python 3.14t (free-threaded/no-GIL)
4. Publishes to TestPyPI
└─ For validation before production
5. Publishes to PyPI
└─ Production release
6. Creates GitHub Release
├─ Converts changelog (RST → Markdown)
├─ Attaches wheel artifacts
└─ Marks as pre-release if applicable
Watch the workflow progress:
# Using GitHub CLI
gh run list --limit 5
gh run watch # Watch the latest run
Or visit: https://github.com/msqd/harp/actions
The workflow typically takes 10-15 minutes to complete.
8. Verify the Release¶
Once the workflow completes successfully:
Check PyPI:
# View on PyPI
open https://pypi.org/project/harp-proxy/
Check GitHub Release:
# View releases
open https://github.com/msqd/harp/releases
Test Installation:
# Test directly from PyPI without installing (recommended)
uvx harp-proxy@$VERSION version
# Run commands to verify functionality
uvx harp-proxy@$VERSION --help
# Download the wheel file without installing
# Direct download from PyPI using JSON API (no pip required)
curl -L $(curl -s https://pypi.org/pypi/harp-proxy/$VERSION/json | \
python3 -c "import sys, json; print([u['url'] for u in json.load(sys.stdin)['urls'] if u['packagetype']=='bdist_wheel'][0])") \
-o harp_proxy-$VERSION-py3-none-any.whl
# Or with jq (if installed)
curl -L $(curl -s https://pypi.org/pypi/harp-proxy/$VERSION/json | \
jq -r '.urls[] | select(.packagetype=="bdist_wheel") | .url') \
-o harp_proxy-$VERSION-py3-none-any.whl
# For persistent installation in a project
uv pip install harp-proxy==$VERSION
Version Naming Conventions¶
Follow semantic versioning:
- Stable Releases
X.Y.Z(e.g.,0.9.0,1.0.0,2.1.3)Use for production-ready releases.
- Release Candidates
X.Y.Z-rcN(e.g.,0.9.1-rc1,0.9.1-rc2)Use for testing before final release. The workflow automatically marks these as pre-releases.
- Beta Releases
X.Y.Z-betaN(e.g.,1.0.0-beta1)Use for feature-complete but not fully tested releases.
- Alpha Releases
X.Y.Z-alphaN(e.g.,1.0.0-alpha1)Use for early testing releases with incomplete features.
Troubleshooting¶
Version Mismatch Error¶
If the workflow fails with:
❌ Version mismatch!
Expected (from git tag): 0.9.0
Actual (from pyproject.toml): 0.9-dev
Solution:
Delete the tag locally and remotely:
git tag -d $VERSION git push origin :refs/tags/$VERSION
Fix the version in
pyproject.tomlRepeat from step 3 (Update pyproject.toml)
Workflow Build Failures¶
If the build fails:
Check the workflow logs for specific errors
Fix the issue in your code
Delete the failed tag (see above)
Re-run the release process
Test Failures¶
If tests fail on specific Python versions:
Review test logs to identify the issue
Fix the code to support all Python versions
Re-release with a new tag
PyPI Publishing Failures¶
The workflow uses PyPI’s trusted publishing (OIDC), which requires no manual credentials.
If publishing fails:
Verify trusted publisher configuration on PyPI:
Ensure
harp-proxyhas a trusted publisher for:Owner:
msqdRepository:
harpWorkflow:
release.ymlEnvironment:
pypiandtestpypi
See the PyPI Trusted Publishing Guide for setup instructions
Emergency Rollback¶
If a release has critical issues:
Warning
Never delete a PyPI release. PyPI does not allow re-uploading the same version number.
Instead:
Release a new patch version with the fix:
export VERSION=0.9.1 # Increment version # Follow normal release process
Optionally yank the problematic version on PyPI:
Select the problematic version
Click “Options” → “Yank release”
Provide reason: “Critical bug, use version X.Y.Z instead”
Yanking prevents new installations but doesn’t break existing ones.
Update documentation to warn users about the problematic version
Manual Testing (Before Release)¶
To test the build process locally before creating a release:
Build the Wheel¶
make clean-dist wheel
This command:
Builds in an isolated sandbox environment
Compiles and bundles the React frontend
Creates the wheel package
Validates with
twine check
The wheel is created in dist/.
Test the Wheel¶
Test the built wheel in a fresh container:
bin/runc_wheel dist/*.whl
This starts a container with the wheel installed. Test it:
harp-proxy server --endpoint httpbin=4000:http://httpbin.org/
Manual Upload (Emergency Only)¶
If the automated workflow is completely broken and you need to release urgently:
twine upload dist/*
Warning
This requires PyPI credentials configured locally and should be avoided. Always prefer fixing the automated workflow instead.
Best Practices¶
Test thoroughly before releasing - Run the full test suite locally
Use release candidates for major versions - Create
X.Y.Z-rc1beforeX.Y.ZKeep the changelog updated - Add entries per PR/feature (see Changelog)
Document breaking changes clearly - Use “Important Changes” section
Release often - Small, frequent releases are better than large, infrequent ones
Monitor after release - Watch for issues reported after release
See Also¶
Changelog - Changelog management workflow
Chores - Pre-release housekeeping tasks
../ci - CI/CD pipeline documentation
PyPI Trusted Publishing Guide - PyPI setup reference