Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ name: Build, validate & Release
# Usage:
# - For PRs: this workflow runs automatically to validate the package builds and installs correctly on multiple Python versions. No artifacts are published for PRs.
# - For releases: when you push a tag like v1.2.3, this workflow runs the full matrix validation, then builds the release artifacts, and finally publishes to PyPI if all checks pass.
# - For manual re-runs: use "Run workflow" and provide a tag-like value such as v2.0.0rc1.

on:
workflow_dispatch:
inputs:
release_tag:
description: 'Tag to build/publish (e.g. v1.2.3 or v2.0.0rc1)'
required: true
type: string
# Release pipeline: run only when pushing a version-like tag (e.g. v1.2.3)
# Test pipeline: run tests on main/master pushes & pull requests AND tags.
push:
Expand Down Expand Up @@ -36,9 +42,24 @@ jobs:
python-version: ["3.10", "3.11", "3.12"]

steps:
# Fetch repository sources so we can build/test
- name: Validate manual tag input
if: ${{ github.event_name == 'workflow_dispatch' }}
shell: bash
run: |
case "${{ inputs.release_tag }}" in
v*.*.*)
echo "Manual release tag accepted: ${{ inputs.release_tag }}"
;;
*)
echo "release_tag must look like v1.2.3 (or similar, e.g. v2.0.0rc1)"
exit 1
;;
esac

- name: Checkout sources
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release_tag) || github.ref }}

- name: Set up Python
uses: actions/setup-python@v6
Expand Down Expand Up @@ -86,12 +107,28 @@ jobs:
runs-on: ubuntu-latest
needs: test_matrix
# Safety gate: only run for version tags, never for PRs/branches
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}

steps:
- name: Validate manual tag input
if: ${{ github.event_name == 'workflow_dispatch' }}
shell: bash
run: |
case "${{ inputs.release_tag }}" in
v*.*.*)
echo "Manual release tag accepted: ${{ inputs.release_tag }}"
;;
*)
echo "release_tag must look like v1.2.3 (or similar, e.g. v2.0.0rc1)"
exit 1
;;
esac
# Fetch sources for the tagged revision
- name: Checkout sources
uses: actions/checkout@v6
with:
# For a manual run, we want to check out the commit at the provided tag
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release_tag) || github.ref }}

# Use a single, modern Python for the canonical release build
- name: Set up Python (release build)
Expand Down Expand Up @@ -124,7 +161,7 @@ jobs:
runs-on: ubuntu-latest
needs: build_release
# Safety gate: only run for version tags
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}

steps:
# Retrieve the exact distributions produced in build_release
Expand All @@ -138,16 +175,29 @@ jobs:
- name: Set up Python (publish)
uses: actions/setup-python@v6
with:
python-version: "3.x"
python-version: "3.12"

# Install twine for uploading
- name: Install Twine
run: python -m pip install -U twine

# Check that the PyPI API token is present before attempting upload (fails fast if not set)
- name: Check PyPI credential presence
shell: bash
env:
TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
run: |
if [ -z "$TWINE_PASSWORD" ]; then
echo "TWINE_PASSWORD is empty"
exit 1
else
echo "TWINE_PASSWORD is present"
fi

# Upload to PyPI using an API token stored in repo secrets.
# --skip-existing avoids failing if you re-run a workflow for the same version.
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
run: python -m twine upload --non-interactive --verbose --skip-existing dist/*
run: python -m twine upload --non-interactive --skip-existing dist/*
Loading