diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5962c04..0d7b229 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,19 @@ jobs: - name: Run unit tests run: python3 -m unittest discover -s tests -t tests -v + # The release workflow only builds after a merge to the default branch, by + # which point the tag and the GitHub release already exist — a build that + # fails there burns a version number. Build here instead, on the same + # interpreter the tests above ran under, so a broken pyproject.toml is + # stopped at the pull request. + # + # The version is deliberately not rewritten: what this checks is that the + # tree as committed builds. Choosing the number belongs to the tag. + - name: Build the package + run: | + python3 -m pip install --upgrade build + python3 -m build + - name: Setup Python 3.14 uses: actions/setup-python@v5 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4264251..5d83a7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,10 @@ -# Copy this file into a downstream repository as `.github/workflows/release.yml`. -# `scripts/sync.sh` does that automatically when the file does not exist yet. +# Release pipeline: version, tag, GitHub release, then the built package. # -# This is the source-only shape: compute the version, create the tag, create the -# GitHub release. GitHub attaches the source tarball and zip to every release by -# itself, so nothing else is required. -# -# Add the `publish` job from examples/publish.yml when the repository publishes to -# a registry, and the `package` job from examples/package.yml when it ships build -# artifacts. Both are independent siblings of `release`: add or delete them freely. +# Adapted from nexform-tech/repo-template: the `release` job is the synced stub +# and stays as written, while `publish` and `package` come from +# `examples/publish.yml` and `examples/package.yml`. All three are siblings and +# both new jobs are gated on `release` producing a tag, so either can be deleted +# without touching the others. name: Release on: @@ -30,3 +27,91 @@ jobs: release: # Pin to a moving major tag (`@v1`), or to a commit SHA for maximum safety. uses: nexform-tech/repo-template/.github/workflows/release.yml@v1 + + # Publish to PyPI through Trusted Publishing: GitHub mints an OIDC token that + # PyPI exchanges for a one-shot upload credential, so this repository stores no + # API token at all. PyPI must know about the repository first — a pending + # publisher for owner `nexform-tech`, repository `litegrip-python`, workflow + # `release.yml` and no environment. Without it the upload is refused and this + # run goes red. + publish: + needs: release + # Nothing released (no feat/fix/perf in the merge) means nothing to publish. + if: needs.release.outputs.new_tag != '' + runs-on: ubuntu-22.04 + permissions: + contents: read + id-token: write # the OIDC token PyPI exchanges for an upload credential + env: + VERSION: ${{ needs.release.outputs.new_tag }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + # The tag is the only source of truth: semantic-release never commits the + # version back (AGENTS.md §3), so pyproject.toml is rewritten here, in the + # workspace, and nowhere else. `litegrip.__version__` needs no equivalent + # step — it reads the installed distribution metadata (see + # `_detect_version` in src/litegrip/__init__.py), so it follows this number + # by construction instead of being a second copy of it. + - name: Set the release version + run: | + set -euo pipefail + V="${VERSION#v}" + sed -i "s/^version = .*/version = \"${V}\"/" pyproject.toml + # A sed that matched nothing would build 0.0.0 — the placeholder — so + # assert the rewrite rather than trusting it. + grep -qx "version = \"${V}\"" pyproject.toml + + - name: Build sdist and wheel + run: | + python -m pip install --upgrade build + python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # Attach the same artifacts to the GitHub release, so the files that reached + # PyPI are also downloadable from this repository without going through PyPI. + # + # The version rewrite and the build repeat what `publish` does. That is + # deliberate: the two jobs are independent siblings, and coupling them so the + # build happens once would mean deleting one breaks the other. + package: + needs: release + if: needs.release.outputs.new_tag != '' + runs-on: ubuntu-22.04 + permissions: + contents: write # attach artifacts to the GitHub release + env: + VERSION: ${{ needs.release.outputs.new_tag }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set the release version + run: | + set -euo pipefail + V="${VERSION#v}" + sed -i "s/^version = .*/version = \"${V}\"/" pyproject.toml + grep -qx "version = \"${V}\"" pyproject.toml + + - name: Build artifacts + run: | + python -m pip install --upgrade build + python -m build + + - name: Attach artifacts to the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "$VERSION" dist/* --clobber