Skip to content

v3.1.1

v3.1.1 #7

Workflow file for this run

# 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.3.4"
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@v6
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.0.0,<2.0.0"
- name: Setup Python
uses: actions/setup-python@v6
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@release/v1
- name: Publish to PyPI (API token fallback)
if: steps.publish_trusted.outcome == 'failure' && env.PYPI_API_TOKEN != ''
uses: pypa/gh-action-pypi-publish@release/v1
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