Skip to content

PyPI Publishing Setup

This monorepo publishes 8 separate packages to PyPI:

Individual packages: 1. canvod-readers - GNSS data readers 2. canvod-auxiliary - Auxiliary data handling 3. canvod-grids - Grid operations 4. canvod-store - Storage backends 5. canvod-utils - Utility functions 6. canvod-viz - Visualization tools 7. canvod-vod - VOD calculation

Umbrella package: 8. canvodpy - Depends on all 7 above

User experience:

pip install canvodpy  # Gets all 8 packages automatically

  •   Phase 1 — TestPyPI (Done)


    Account created · First manual upload · OIDC trusted publishers configured. Beta/alpha releases publish here automatically.

  •   Phase 2 — Real PyPI (Active)


    Production account · Environment protection · Stable releases only. pip install canvodpy serves users from here.


Quick Commands

# Build all 8 packages locally
just build-all

# Manual publish to TestPyPI (requires credentials)
just publish-testpypi

# Manual publish to PyPI (requires credentials)
just publish-pypi

# Automated publish (beta) - triggers workflow
git tag v0.1.0-beta.1 && git push --tags

# Automated publish (production) - triggers workflow
git tag v0.1.0 && git push --tags

Phase 1: TestPyPI Setup (Completed)

Step 1: Create TestPyPI Account (Done)

Already done! Account created at https://test.pypi.org

Step 2: Manual First Publish (Done)

Already completed with twine upload --repository testpypi dist/*

All 8 packages published: - https://test.pypi.org/project/canvod-readers/ - https://test.pypi.org/project/canvod-auxiliary/ - https://test.pypi.org/project/canvod-grids/ - https://test.pypi.org/project/canvod-store/ - https://test.pypi.org/project/canvod-utils/ - https://test.pypi.org/project/canvod-viz/ - https://test.pypi.org/project/canvod-vod/ - https://test.pypi.org/project/canvodpy/

Step 3: Set Up OIDC Automation (Done)

3.1: Create GitHub Environment

  1. Go to: https://github.com/nfb2021/canvodpy/settings/environments
  2. Click "New environment"
  3. Name: testpypi
  4. Save

3.2: Register Trusted Publishers (FOR EACH OF 8 PACKAGES)

For each package, go to its publishing settings: - https://test.pypi.org/manage/project/canvod-readers/settings/publishing/ - https://test.pypi.org/manage/project/canvod-auxiliary/settings/publishing/ - https://test.pypi.org/manage/project/canvod-grids/settings/publishing/ - https://test.pypi.org/manage/project/canvod-store/settings/publishing/ - https://test.pypi.org/manage/project/canvod-utils/settings/publishing/ - https://test.pypi.org/manage/project/canvod-viz/settings/publishing/ - https://test.pypi.org/manage/project/canvod-vod/settings/publishing/ - https://test.pypi.org/manage/project/canvodpy/settings/publishing/

On each page: 1. Click "Add a new publisher" 2. Select "GitHub" 3. Fill in: - Owner: nfb2021 - Repository: canvodpy - Workflow: publish_testpypi.yml - Environment: testpypi 4. Click "Add"

3.3: Test OIDC Publishing

git tag v0.1.0-beta.1 -m "Test OIDC"
git push --tags
cd /path/to/canvodpy
uv build

Upload manually (first time only):

uvx twine upload --repository testpypi dist/*

When prompted:

username: your_testpypi_username
password: your_testpypi_password

Success looks like:

Uploading canvodpy-0.1.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Verify it's there: - Go to: https://test.pypi.org/project/canvodpy/

Step 3: Configure OIDC on TestPyPI

  1. Go to project settings:
  2. https://test.pypi.org/manage/project/canvodpy/settings/publishing/

  3. Scroll to "Publishing"

  4. Click "Add a new publisher"

  5. Fill in the form:

    PyPI project name:     canvodpy
    Owner:                 nfb2021
    Repository name:       canvodpy
    Workflow name:         publish_testpypi.yml
    Environment name:      test-release
    

  6. Click "Add"

  7. You should see:

    ✓ Trusted publisher added
    
    Publisher:
    - Repository: nfb2021/canvodpy
    - Workflow: publish_testpypi.yml
    - Environment: test-release
    

Step 4: Create GitHub Environment

  1. Go to your repo:
  2. https://github.com/nfb2021/canvodpy

  3. Settings → Environments

  4. New environment:

  5. Name: test-release
  6. Click "Configure environment"

  7. Add protection rules (optional):

  8. Required reviewers: Add yourself
  9. Wait timer: 0 minutes (it's just testing)

  10. Save

Step 5: Create TestPyPI Workflow

Create: .github/workflows/publish_testpypi.yml

name: Publish to TestPyPI

on:
  release:
    types: [published]

  # Manual trigger for testing
  workflow_dispatch:

permissions:
  id-token: write  # REQUIRED for OIDC
  contents: read

jobs:
  publish-testpypi:
    name: Upload to TestPyPI
    runs-on: ubuntu-latest
    environment: test-release

    # Only run if tag contains 'beta' or 'alpha' (for testing)
    if: contains(github.ref, 'beta') || contains(github.ref, 'alpha')

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Install uv
        uses: astral-sh/setup-uv@v7
        with:
          version: "0.9.27"

      - name: Set up Python
        run: uv python install 3.14

      - name: Build package
        run: uv build

      - name: Publish to TestPyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          repository-url: https://test.pypi.org/legacy/
          print-hash: true

Step 6: Test the Workflow

Create a beta release:

just release 0.1.0-beta.1
git push origin main
git push origin --tags

Publish the draft release: 1. Go to: https://github.com/nfb2021/canvodpy/releases 2. Find draft for v0.1.0-beta.1 3. Click "Publish release"

Watch the workflow: 1. Go to: https://github.com/nfb2021/canvodpy/actions 2. See "Publish to TestPyPI" running 3. Wait ~2-3 minutes 4. Should say "✓ Complete"

Verify upload: - https://test.pypi.org/project/canvodpy/ - Should see version 0.1.0b1

Test installation:

# Create test environment
uv venv test-env
source test-env/bin/activate  # or test-env\Scripts\activate on Windows

# Install from TestPyPI
pip install --index-url https://test.pypi.org/simple/ canvodpy

# Test it works
python -c "import canvodpy; print(canvodpy.__version__)"
# Should print: 0.1.0b1

# Clean up
deactivate
rm -rf test-env

If it works, you are ready for real PyPI.


Phase 2: Real PyPI Setup

Step 7: Create Real PyPI Account

  1. Go to: https://pypi.org/account/register/
  2. Fill in details (can use same email as TestPyPI)
  3. Verify email
  4. Enable 2FA (PyPI requires it for trusted publishers!)

Step 8: Reserve Package Name on PyPI

Package names are permanent

Once a package is uploaded to PyPI, the name is reserved forever. You cannot delete it or reuse its version numbers. Double-check everything before uploading.

# Build fresh
cd /path/to/canvodpy
rm -rf dist/
uv build

# Upload to REAL PyPI
uvx twine upload dist/*

When prompted:

username: your_pypi_username
password: your_pypi_password

Verify: - https://pypi.org/project/canvodpy/

Step 9: Configure OIDC on Real PyPI

Exactly same as TestPyPI:

  1. Go to:
  2. https://pypi.org/manage/project/canvodpy/settings/publishing/

  3. Add publisher:

    PyPI project name:     canvodpy
    Owner:                 nfb2021
    Repository name:       canvodpy
    Workflow name:         publish_pypi.yml
    Environment name:      release
    

  4. Click "Add"

Step 10: Create Production GitHub Environment

Enable 2FA first

PyPI requires 2FA to add trusted publishers. Enable it at pypi.org/manage/account/ before proceeding.

  1. Settings → Environments → New
  2. Name: release
  3. Protection rules (IMPORTANT!):
  4. Required reviewers: Add all maintainers
  5. Wait timer: 5 minutes (think before publishing)
  6. Save

Step 11: Create Production Workflow

Create: .github/workflows/publish_pypi.yml

name: Publish to PyPI

on:
  release:
    types: [published]

permissions:
  id-token: write
  contents: read

jobs:
  publish-pypi:
    name: Upload to PyPI
    runs-on: ubuntu-latest
    environment: release

    # Skip beta/alpha releases for production PyPI
    if: "!contains(github.ref, 'beta') && !contains(github.ref, 'alpha')"

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Install uv
        uses: astral-sh/setup-uv@v7
        with:
          version: "0.9.27"

      - name: Set up Python
        run: uv python install 3.14

      - name: Build package
        run: uv build

      - name: Check package
        run: |
          uvx twine check dist/*
          ls -lh dist/

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          print-hash: true

Step 12: First Production Release

When ready for v0.1.0:

# Create release
just release 0.1.0
git push origin main
git push origin --tags

Publish: 1. Go to releases 2. Find draft v0.1.0 3. Review carefully! 4. Click "Publish release" 5. Approve the workflow when prompted 6. Wait 5 minutes (your wait timer) 7. Workflow runs 8. Package published to PyPI!

Verify:

pip install canvodpy
python -c "import canvodpy; print(canvodpy.__version__)"
# Should print: 0.1.0

Your package is now on PyPI.


Summary

just release 0.2.0-beta.1
git push --tags
# → publishes to TestPyPI automatically
just release 0.2.0
git push --tags
# → publishes to real PyPI (after environment approval)
pip install canvodpy          # latest stable
pip install canvodpy==0.2.0   # specific version

Best Practices

Test on TestPyPI first

Use -beta.X or -alpha.X version suffixes to publish to TestPyPI. Verify pip install --index-url https://test.pypi.org/simple/ canvodpy works before creating a stable release.

Version numbers are permanent

PyPI does not allow deleting packages or reusing version numbers. Bump the version and re-release if you need to fix a bad upload.

Environment protection

The release environment has required reviewers and a 5-minute wait timer — this prevents accidental production releases.


Troubleshooting

Publisher not configured OIDC error
  1. Verify environment name in workflow matches PyPI config exactly
  2. Verify workflow filename matches exactly
  3. Verify repository owner/name are correct
  4. Confirm 2FA is enabled on your PyPI account
Package already exists

Cannot upload the same version twice. Bump and retry:

just bump patch   # 0.1.0 → 0.1.1

Build failed

Test locally first:

uv build
ls dist/   # should contain .whl and .tar.gz
uvx twine check dist/*

Environment approval stuck

Check the release environment's protection rules at Settings → Environments → release. Approve pending deployments manually.


Next Steps

  • Test with beta release on TestPyPI
  • First production release v0.1.0
  • Set up Zenodo for DOI (Zenodo Setup)