Skip to content
Open
Show file tree
Hide file tree
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
90 changes: 45 additions & 45 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,72 +1,69 @@
name: Publish to PyPI

# plan_RELEASE W3 (gap G-C): a tag publishes the ARTIFACTS THE GATE TESTED.
#
# The gate is not re-implemented here — this workflow CALLS the one reusable
# `release-gate.yml` at the tag's SHA, so the tag is qualified by exactly the
# same jobs (including the six install-smoke cells) a pull request runs. That
# call builds the distribution once and uploads it as the run's `dist` artifact;
# `publish` then DOWNLOADS those same files, re-checks their SHA-256 hashes
# against the build manifest, and uploads them. It never runs `uv build`, never
# rebuilds from the checkout, and never fetches the version from PyPI.
#
# Because `publish` needs the gate job, a failed, skipped, or cancelled cell in
# ANY gate lane leaves `needs.gate` non-success and neither PyPI nor the GitHub
# release happens.

on:
push:
tags:
- "v*"

permissions:
contents: write
id-token: write
contents: read

jobs:
test:
name: Run tests before publish
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --extra test

- name: Run unit tests
run: uv run pytest -m "not integration" -v --tb=short
# The full release gate, at the tag SHA, with the tag passed in so the gate
# itself fails if the tag and the built artifact's metadata version disagree.
gate:
uses: ./.github/workflows/release-gate.yml
with:
ref: ${{ github.sha }}
release_tag: ${{ github.ref_name }}

publish:
name: Build and publish
name: Publish the gated artifacts
runs-on: ubuntu-latest
needs: test
needs: gate
environment: pypi
permissions:
contents: write # create the GitHub release
id-token: write # PyPI trusted publishing
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1

- name: Set up Python
run: uv python install 3.12
- name: Download the gated dist artifact (no rebuild)
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist

- name: Verify version matches tag
- name: Re-check hashes, membership, and the tag/version agreement
shell: bash
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
PKG_VERSION=$(python -c "
import re
with open('pyproject.toml') as f:
m = re.search(r'version\s*=\s*\"([^\"]+)\"', f.read())
print(m.group(1))
")
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag v${TAG_VERSION} does not match pyproject.toml version ${PKG_VERSION}"
exit 1
fi
echo "VERSION=${TAG_VERSION}" >> "$GITHUB_ENV"

- name: Build package
run: uv build
set -euo pipefail
python3 tools/package_verify.py verify \
--dist dist --manifest release-manifest.json \
--expect-version "${GITHUB_REF_NAME}"
echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
echo "These exact files are about to be published:"
sha256sum dist/*

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: dist

- name: Generate changelog
run: |
Expand Down Expand Up @@ -111,6 +108,9 @@ jobs:
name: v${{ env.VERSION }}
body_path: changelog.md
append_body: true
files: |
dist/*.whl
dist/*.tar.gz
body: |
## Installation

Expand Down
Loading
Loading