v3.2.0 #8
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Publish workflow. | |
| # Purpose: build and publish artifacts when a GitHub release is published. | |
| # This workflow does not compute versions or create tags. | |
| name: "Publish" | |
| on: | |
| release: | |
| types: | |
| - published | |
| concurrency: | |
| # Preserve release order on PyPI: publish runs execute one-by-one. | |
| group: publish-pypi | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| id-token: write | |
| env: | |
| PYTHON_VERSION: "3.11" | |
| POETRY_VERSION: "2.4.1" | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| TRUSTED_RELEASE_ACTORS: "vstack-release-bot[bot],eschaar" | |
| jobs: | |
| publish: | |
| name: Build and Publish to PyPI | |
| if: github.event.release.prerelease == false | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| env: | |
| PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| steps: | |
| - name: Validate release tag format | |
| # Fail fast before checkout if the tag is not a plain SemVer X.Y.Z. | |
| # Prevents non-versioned or pre-release tags from reaching the publish step. | |
| shell: bash | |
| run: | | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "ERROR: release tag '$TAG' is not in expected SemVer X.Y.Z format." | |
| echo "Only plain version tags (e.g. 1.2.3) are permitted to trigger publish." | |
| exit 1 | |
| fi | |
| echo "tag=$TAG format is valid." | |
| - name: Validate release actor | |
| # Only allow trusted actors to trigger a PyPI publish. | |
| # vstack-release-bot[bot] covers release-please GitHub App automation. | |
| # eschaar covers direct maintainer releases. | |
| shell: bash | |
| run: | | |
| ACTOR="${{ github.event.release.author.login }}" | |
| IFS=',' read -r -a ALLOWED <<< "$TRUSTED_RELEASE_ACTORS" | |
| TRUSTED=false | |
| for allowed_actor in "${ALLOWED[@]}"; do | |
| if [[ "$ACTOR" == "$allowed_actor" ]]; then | |
| TRUSTED=true | |
| break | |
| fi | |
| done | |
| if [[ "$TRUSTED" != "true" ]]; then | |
| echo "ERROR: release created by '$ACTOR' which is not in the trusted actor list." | |
| echo "Allowed: $TRUSTED_RELEASE_ACTORS" | |
| echo "Releases must be created by vstack-release-bot automation or a trusted maintainer." | |
| exit 1 | |
| fi | |
| echo "release_actor='$ACTOR' is trusted." | |
| - name: Checkout release tag | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: refs/tags/${{ github.event.release.tag_name }} | |
| fetch-depth: 0 | |
| - name: Install Poetry | |
| run: pipx install "poetry==${POETRY_VERSION}" | |
| - name: Install poetry-dynamic-versioning plugin | |
| run: pipx inject poetry "poetry-dynamic-versioning[plugin]>=1.10.0,<2.0.0" | |
| - name: Setup Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Validate release tag checkout | |
| run: | | |
| EXPECTED="${{ github.event.release.tag_name }}" | |
| ACTUAL_TAG="$(git tag --points-at HEAD | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)" | |
| echo "expected_tag=$EXPECTED" | |
| echo "head_tag=$ACTUAL_TAG" | |
| if [[ "$ACTUAL_TAG" != "$EXPECTED" ]]; then | |
| echo "ERROR: build checkout is not pinned to expected release tag." | |
| exit 1 | |
| fi | |
| - name: Build distributions | |
| run: poetry build | |
| - name: Smoke test built wheel | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install dist/*.whl | |
| vstack --help >/dev/null | |
| - name: Validate built artifact version | |
| run: | | |
| EXPECTED="${{ github.event.release.tag_name }}" | |
| shopt -s nullglob | |
| MATCHES=(dist/*"$EXPECTED"*.whl dist/*"$EXPECTED"*.tar.gz) | |
| echo "expected_version=$EXPECTED" | |
| ls -1 dist/ | |
| if [[ ${#MATCHES[@]} -eq 0 ]]; then | |
| echo "ERROR: no built artifacts contain expected version '$EXPECTED'." | |
| exit 1 | |
| fi | |
| - name: Publish to PyPI (trusted publishing) | |
| id: publish_trusted | |
| continue-on-error: true | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b | |
| - name: Publish to PyPI (API token fallback) | |
| if: steps.publish_trusted.outcome == 'failure' && env.PYPI_API_TOKEN != '' | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b | |
| with: | |
| user: __token__ | |
| password: ${{ env.PYPI_API_TOKEN }} | |
| - name: Fail when trusted publishing fails and no fallback token exists | |
| if: steps.publish_trusted.outcome == 'failure' && env.PYPI_API_TOKEN == '' | |
| shell: bash | |
| run: | | |
| echo "ERROR: trusted publishing failed and secret PYPI_API_TOKEN is not configured." | |
| echo "Either fix PyPI trusted publisher mapping or add PYPI_API_TOKEN as a fallback." | |
| exit 1 |