From e086f76da7d1e20ed8c178c8f3a8fe9c6e634018 Mon Sep 17 00:00:00 2001 From: Xi Zhang <106144707+X-iZhang@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:31:17 +0100 Subject: [PATCH] ci: publish to PyPI via trusted publishing; build version images on release (#417) * ci: publish to PyPI via trusted publishing; build version images on release * ci: pin publish actions to commit SHAs; extend version guard to docker and manual dispatch * ci: disable setup-uv cache in the publish workflow --- .github/workflows/docker.yml | 18 +++++++++- .github/workflows/publish.yml | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7de8f67..54dbd7b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,7 +3,10 @@ name: Docker on: push: branches: ["main"] - tags: ["v*"] + # Version images build when a GitHub Release is published — the same event + # that triggers the PyPI upload (publish.yml), so the two channels stay in sync. + release: + types: [published] pull_request: paths: - "Dockerfile" @@ -32,6 +35,19 @@ jobs: steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + # Same guard as publish.yml — a release whose tag mismatches pyproject + # must not publish versioned images either. + - name: Guard — package version must match the release tag + if: github.event_name == 'release' + run: | + VERSION=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/^version = "(.*)"/\1/') + TAG="${GITHUB_REF_NAME#v}" + echo "pyproject version: $VERSION | release tag: $TAG" + if [ "$VERSION" != "$TAG" ]; then + echo "::error::pyproject version ($VERSION) does not match release tag ($TAG); refusing to publish images." + exit 1 + fi + - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7c08bcd --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,67 @@ +name: Publish to PyPI + +# Publishes EvoScientist to PyPI via OpenID Connect (trusted publishing) — no +# API token or password involved. Fires when a GitHub Release is published +# (i.e. after `gh release create vX.Y.Z`), builds the sdist + wheel, guards +# that the package version matches the release tag, then uploads with a +# short-lived OIDC token. +# +# One-time PyPI setup (Manage project -> Publishing -> Add a new publisher): +# Owner: EvoScientist +# Repository: EvoScientist +# Workflow name: publish.yml +# Environment name: pypi +on: + release: + types: [published] + # Manual re-run escape hatch — dispatch it from the release tag; the version + # guard rejects any non-tag ref. + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + name: Build and publish to PyPI + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: + name: pypi + url: https://pypi.org/project/EvoScientist/ + permissions: + id-token: write # required to mint the OIDC token PyPI verifies + steps: + # Actions in this job are pinned to full commit SHAs (like docker.yml): + # it holds id-token: write and PyPI publishing authority. + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + python-version: "3.11" + # No shared cache in the publishing job — build from clean sources only. + enable-cache: false + + - name: Build sdist + wheel + run: uv build + + - name: Guard — package version must match the release tag + run: | + if [ "${GITHUB_REF_TYPE}" != "tag" ]; then + echo "::error::This workflow must run from a version tag (got ${GITHUB_REF_TYPE} '${GITHUB_REF_NAME}'); dispatch it from the release tag." + exit 1 + fi + VERSION=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/^version = "(.*)"/\1/') + TAG="${GITHUB_REF_NAME#v}" + echo "pyproject version: $VERSION | release tag: $TAG" + if [ "$VERSION" != "$TAG" ]; then + echo "::error::pyproject version ($VERSION) does not match release tag ($TAG); refusing to publish." + exit 1 + fi + + - name: Twine metadata check + run: uvx twine check dist/* + + - name: Publish to PyPI (trusted publishing) + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2