diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b0e1170..a1c83cb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -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: @@ -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 @@ -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) @@ -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 @@ -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/*