python-release.yml¶
Reusable workflow for automated Python package releases with testing, building, and publishing to PyPI.
Overview¶
Complete release pipeline:
- Test - Run full CI test suite
- Build - Build wheel and source distributions
- Publish - Upload to PyPI
- Release - Create GitHub release with artifacts
Usage¶
Basic Release on Tags¶
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
permissions:
contents: write
id-token: write
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
python-version: '3.11'
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
Test PyPI Release¶
jobs:
release:
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
python-version: '3.11'
repository-url: 'https://test.pypi.org/legacy/'
secrets:
pypi-token: ${{ secrets.TEST_PYPI_TOKEN }}
Dry Run¶
jobs:
release:
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
dry-run: true
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
Inputs¶
| Input | Type | Description | Default |
|---|---|---|---|
python-version |
string | Python version to use | '3.11' |
uv-version |
string | UV version to use | '0.7.8' |
test-directory |
string | Test directory | 'tests/' |
coverage-threshold |
number | Coverage threshold | 80 |
skip-tests |
boolean | Skip test job | false |
repository-url |
string | PyPI repository URL | 'https://upload.pypi.org/legacy/' |
skip-existing |
boolean | Skip if package exists | true |
create-github-release |
boolean | Create GitHub release | true |
prerelease |
boolean | Mark as prerelease | false |
dry-run |
boolean | Validate without publishing | false |
release-notes-file |
string | Path to release notes | '' |
Secrets¶
| Secret | Description | Required |
|---|---|---|
pypi-token |
PyPI API token | Yes (unless trusted publishing) |
github-token |
GitHub token | No (auto-provided) |
Outputs¶
| Output | Description |
|---|---|
release-version |
Released version |
pypi-url |
PyPI package URL |
github-release-url |
GitHub release URL |
Jobs¶
test¶
Runs complete test suite before release:
- Code quality checks
- Unit tests with coverage
- Security scanning
- Ensures package is ready for release
build¶
Builds package distributions:
- Creates wheel (
.whl) - Creates source distribution (
.tar.gz) - Validates package metadata
- Uploads artifacts
publish¶
Publishes to PyPI:
- Verifies metadata with twine
- Uploads to PyPI (or Test PyPI)
- Skips if package version exists (optional)
- Uses official PyPA publish action
release¶
Creates GitHub release:
- Creates Git tag (if not exists)
- Generates release notes
- Attaches build artifacts
- Marks as prerelease (optional)
Job Dependencies¶
All jobs run sequentially. If any job fails, subsequent jobs are skipped.
Examples¶
Complete Release Workflow¶
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
id-token: write
jobs:
release:
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
python-version: '3.11'
coverage-threshold: 85
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
Manual Release Trigger¶
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release'
required: true
type: string
jobs:
release:
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
python-version: '3.11'
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
Test Then Release¶
name: CI/CD
on:
push:
branches: [main]
tags: ['v*']
jobs:
ci:
if: "!startsWith(github.ref, 'refs/tags/v')"
uses: provide-io/ci-tooling/.github/workflows/[email protected]
release:
if: startsWith(github.ref, 'refs/tags/v')
uses: provide-io/ci-tooling/.github/workflows/[email protected]
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
Prerelease¶
on:
push:
tags:
- 'v*-alpha*'
- 'v*-beta*'
- 'v*-rc*'
jobs:
release:
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
prerelease: true
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
Skip Tests (Not Recommended)¶
jobs:
release:
uses: provide-io/ci-tooling/.github/workflows/[email protected]
with:
skip-tests: true # Use only if tests ran in previous job
secrets:
pypi-token: ${{ secrets.PYPI_TOKEN }}
PyPI Token Setup¶
Create PyPI Token¶
- Log in to PyPI
- Account Settings → API tokens
- "Add API token"
- Name: "GitHub Actions - {repo-name}"
- Scope: Project or account-wide
- Copy token (starts with
pypi-)
Add to GitHub¶
- Repository Settings → Secrets and variables → Actions
- "New repository secret"
- Name:
PYPI_TOKEN - Value: Paste PyPI token
- "Add secret"
Test PyPI (Optional)¶
Same steps on Test PyPI:
- Create token
- Add as TEST_PYPI_TOKEN
- Use with repository-url: 'https://test.pypi.org/legacy/'
Trusted Publishing (Alternative)¶
PyPI supports trusted publishing without tokens:
- Configure on PyPI:
- Project Settings → Publishing
- Add trusted publisher
- Owner: your-org
- Repository: your-repo
- Workflow: release.yml
-
Environment: release (optional)
-
Update workflow:
permissions: id-token: write # For trusted publishing contents: write jobs: release: uses: provide-io/ci-tooling/.github/workflows/[email protected] # No pypi-token needed!
Permissions¶
Required¶
With Trusted Publishing¶
Environment Protection¶
jobs:
release:
environment: production # Requires approval
uses: provide-io/ci-tooling/.github/workflows/[email protected]
Release Notes¶
Auto-Generated¶
Default behavior - generates notes with: - Version number - PyPI package link - Artifact list
Custom File¶
Workflow will read and use file content.
Inline Notes¶
Artifacts¶
python-packages¶
Contains:
- *.whl - Wheel distribution
- *.tar.gz - Source distribution
Retention: 90 days
Available for download from: - Workflow run page - GitHub release page - PyPI package page
Troubleshooting¶
Package Already Exists¶
If version already published:
Or bump version in VERSION file.
Tests Failing¶
Fix tests or skip (not recommended):
Metadata Validation Failed¶
Check pyproject.toml:
- Valid readme field
- Complete description
- Valid license identifier
Test locally:
GitHub Release Failed¶
Ensure:
- Tag exists: git push --tags
- Permissions: contents: write
- Valid token
PyPI Upload Failed¶
Check: - Valid token - Token has correct scope - Version not already published - Package name available (first release)
Version Bumping¶
Semantic Versioning¶
Follow semver.org:
# Patch (1.0.0 → 1.0.1)
echo "1.0.1" > VERSION
# Minor (1.0.1 → 1.1.0)
echo "1.1.0" > VERSION
# Major (1.1.0 → 2.0.0)
echo "2.0.0" > VERSION
Create Tag¶
Automated¶
Use bump2version:
bump2version patch # Patch version
bump2version minor # Minor version
bump2version major # Major version
Dry Run Workflow¶
Test releases before publishing:
-
Create dry-run workflow (
.github/workflows/test-release.yml):name: Test Release on: pull_request: branches: [main] jobs: test-release: uses: provide-io/ci-tooling/.github/workflows/[email protected] with: dry-run: true secrets: pypi-token: ${{ secrets.PYPI_TOKEN }} -
Verify in PR: Dry run executes on PRs
- Review: Check step summary for validation results
- Merge: Actual release on tag push
Release Checklist¶
Before creating release:
- Update
VERSIONfile - Update
CHANGELOG.md(if used) - Update documentation
- Run tests locally:
pytest - Build locally:
uv build - Validate:
twine check dist/* - Commit changes
- Create and push tag
Best Practices¶
Always Test Before Release¶
Don't skip tests. They catch issues before users do.
Use Semantic Versioning¶
Follow semver conventions: - Major: Breaking changes - Minor: New features - Patch: Bug fixes
Write Release Notes¶
Document what changed: - New features - Bug fixes - Breaking changes - Upgrade notes
Test on Test PyPI First¶
For major releases:
1. Release to Test PyPI
2. Test installation: pip install --index-url https://test.pypi.org/simple/ package-name
3. Verify functionality
4. Release to production PyPI
Security¶
- Never commit tokens: Always use GitHub Secrets
- Use project-scoped tokens: Limit token access
- Enable 2FA: Require 2FA for PyPI account
- Rotate tokens regularly: Every 90 days
- Consider trusted publishing: More secure than tokens
Performance¶
Typical workflow execution:
| Job | Time | Notes |
|---|---|---|
| test | 2-4min | Full CI test suite |
| build | 30-60s | Package building |
| publish | 30-60s | PyPI upload |
| release | 30-60s | GitHub release |
| Total | 4-7min | Complete pipeline |
Next Steps¶
- python-ci.yml - CI workflow reference
- Actions - Individual actions
- Quick Start - Getting started