Skip to content

Latest commit

 

History

History
312 lines (237 loc) · 7.24 KB

File metadata and controls

312 lines (237 loc) · 7.24 KB

forge-ci-python-release

Complete Python package release workflow with PyPI publishing and GitHub releases.

Usage

jobs:
  release:
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}
      pypi_token: ${{ secrets.PYPI_TOKEN }}

What It Does

Complete release pipeline:

  1. Dependencies: Install dependencies using your chosen dependency manager
  2. Build: Build Python packages (wheel + sdist)
  3. Publish PyPI: Optionally publish to PyPI (using poetry publish, uv publish, or twine)
  4. Publish TestPyPI: Optionally publish to TestPyPI for testing
  5. GitHub Release: Create GitHub release with package artifacts

Inputs

Input Type Default Description
python-version string '3.12' Python version to use
working-directory string '.' Directory containing pyproject.toml
dependency-manager string 'poetry' Dependency manager (poetry, uv, venv)
poetry-version string '2.2.0' Poetry version (when using poetry)
publish-pypi boolean false Publish to PyPI
publish-testpypi boolean false Publish to TestPyPI
create-github-release boolean true Create GitHub release
snapshot boolean false Build without publishing/releasing
runs-on string 'ubuntu-latest' Runner type

Secrets

Secret Required Description
release_token No* GitHub token for creating releases
pypi_token No** PyPI API token for publishing
testpypi_token No*** TestPyPI API token for testing

*Required if create-github-release: true **Required if publish-pypi: true ***Required if publish-testpypi: true

Outputs

Output Description
version Package version extracted from build
artifacts JSON array of built artifacts

Examples

GitHub Release Only (Default)

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}

Creates GitHub release with wheel and sdist artifacts.

Publish to PyPI + GitHub Release

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    with:
      publish-pypi: true
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}
      pypi_token: ${{ secrets.PYPI_TOKEN }}

Publishes to PyPI and creates GitHub release.

Test on TestPyPI First

name: Release

on:
  push:
    tags:
      - 'v*-rc*'  # Release candidates

jobs:
  release:
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    with:
      publish-testpypi: true
      publish-pypi: false
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}
      testpypi_token: ${{ secrets.TEST_PYPI_TOKEN }}

Test releases on TestPyPI before publishing to production PyPI.

Snapshot Build (Testing)

name: Test Release Build

on:
  pull_request:
    paths:
      - 'pyproject.toml'
      - 'setup.py'

jobs:
  snapshot:
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    with:
      snapshot: true

Build packages without publishing/releasing (for testing).

Use uv Instead of Poetry

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    with:
      dependency-manager: 'uv'
      publish-pypi: true
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}
      pypi_token: ${{ secrets.PYPI_TOKEN }}

Uses uv build and uv publish instead of poetry.

Monorepo

name: Release

on:
  push:
    tags:
      - 'api-v*'
      - 'worker-v*'

jobs:
  api:
    if: startsWith(github.ref, 'refs/tags/api-v')
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    with:
      working-directory: './packages/api'
      publish-pypi: true
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}
      pypi_token: ${{ secrets.PYPI_TOKEN }}

  worker:
    if: startsWith(github.ref, 'refs/tags/worker-v')
    uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
    with:
      working-directory: './packages/worker'
      publish-pypi: true
    secrets:
      release_token: ${{ secrets.GITHUB_TOKEN }}
      pypi_token: ${{ secrets.PYPI_TOKEN }}

Build Tools by Dependency Manager

Dependency Manager Build Command Publish Command
poetry poetry build poetry publish
uv uv build uv publish
venv/pip python -m build twine upload

PyPI Token Setup

Get PyPI API Token

  1. Go to https://pypi.org/manage/account/token/
  2. Create new API token
  3. Copy the token (starts with pypi-)

Add to GitHub Secrets

  1. Go to repository Settings → Secrets → Actions
  2. Add new secret: PYPI_TOKEN
  3. Paste your PyPI token

TestPyPI Token (Optional)

  1. Go to https://test.pypi.org/manage/account/token/
  2. Create new API token
  3. Add to GitHub Secrets as TEST_PYPI_TOKEN

Version Detection

The workflow automatically detects the package version from the built wheel filename. Ensure your package version matches the git tag:

Poetry:

[tool.poetry]
version = "1.2.3"  # Should match git tag v1.2.3

setuptools:

setup(
    version="1.2.3",  # Should match git tag v1.2.3
)

uv/pyproject.toml:

[project]
version = "1.2.3"  # Should match git tag v1.2.3

Pre-release Detection

The workflow automatically marks releases as pre-release if the version contains:

  • alpha (e.g., 1.0.0-alpha.1)
  • beta (e.g., 1.0.0-beta.2)
  • rc (e.g., 1.0.0-rc.1)

Workflow Order

dependencies → build → publish (optional) → github release
  • Dependencies: Reuses forge-ci-python-tools-dependency-manager.yml
  • Build: Uses poetry/uv/build based on dependency manager
  • Publish: Uses poetry publish/uv publish/twine based on dependency manager
  • GitHub Release: Creates release with artifacts

Troubleshooting

"No such file or directory: 'dist/*.whl'"

Ensure your package builds correctly:

# Poetry
poetry build

# uv
uv build

# pip/venv
python -m build

"Invalid or non-existent authentication information"

Check your PyPI token:

  1. Ensure token is valid and not expired
  2. Token should start with pypi-
  3. Check token has upload permissions

"File already exists on PyPI"

You can't overwrite existing versions on PyPI. Bump your version:

version = "1.2.4"  # Increment version

GitHub Release Not Created

Ensure:

  1. release_token secret is provided
  2. create-github-release: true (default)
  3. Not in snapshot mode
  4. Running on a git tag

See Also