From f993ceeb911ef5704f36329894c53499b033c555 Mon Sep 17 00:00:00 2001 From: Laurence Young Date: Thu, 15 Oct 2026 18:17:34 +0800 Subject: [PATCH 1/2] ci: publish the package to PyPI and attach it to the release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release workflow was the source-only stub: it tagged and created the GitHub release but shipped no package. Add the two jobs the template provides for a repository that publishes. `publish` uploads the sdist and the wheel to PyPI through Trusted Publishing (OIDC), so this repository stores no API token at all. `package` attaches the same two files to the GitHub release. Both jobs rewrite the version from the tag inside the workspace, because semantic-release never commits it back (AGENTS.md §3). Two files spell that version out: pyproject.toml and `litearm.__version__`, which the README tells users to verify an install with. Rewriting only the first would ship a wheel whose metadata says one number while `litearm.__version__` still says 2.1.0. Both are asserted after the rewrite, because a sed that matched nothing would otherwise build whatever the file already said. --- .github/workflows/release.yml | 107 +++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4264251..c9ad964 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,95 @@ 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 `litearm-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 the two places that spell it out are + # rewritten here, in the workspace, and nowhere else. `__version__` is one + # of them because the README tells users to verify an install by reading it; + # leaving it alone would ship a wheel whose metadata says 1.2.0 while + # `litearm.__version__` still says 2.1.0. + - name: Set the release version + run: | + set -euo pipefail + V="${VERSION#v}" + sed -i "s/^version = .*/version = \"${V}\"/" pyproject.toml + sed -i "s/^__version__ = .*/__version__ = \"${V}\"/" src/litearm/__init__.py + # A sed that matched nothing would build the version already in the + # file, so assert the rewrite rather than trusting it. + grep -qx "version = \"${V}\"" pyproject.toml + grep -qx "__version__ = \"${V}\"" src/litearm/__init__.py + + - 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 + sed -i "s/^__version__ = .*/__version__ = \"${V}\"/" src/litearm/__init__.py + grep -qx "version = \"${V}\"" pyproject.toml + grep -qx "__version__ = \"${V}\"" src/litearm/__init__.py + + - 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 From 9cd34348769870c94b950c19d680198826d6b0b9 Mon Sep 17 00:00:00 2001 From: Laurence Young Date: Thu, 15 Oct 2026 18:17:44 +0800 Subject: [PATCH 2/2] ci: build the package on every pull request The release workflow only builds after a merge to the default branch, by which point semantic-release has already created the tag and the GitHub release. A build that fails there leaves a version that exists as a tag and nowhere else, and that number cannot be reused. Build in the pull request gate as well, so a broken pyproject.toml is stopped before the merge. The version is deliberately left alone here: what this checks is that the tree as committed builds. --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe27468..a07b3bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,3 +44,13 @@ jobs: run: | pip install -e ".[dev]" pytest + + # release workflow 只在合进默认分支之后才打包, 那时 tag 和 GitHub Release + # 都已经生成 —— 构建到那一步才炸, 等于白烧一个版本号。在这里先构建一次, + # 让坏掉的 pyproject.toml 挡在 PR 上。 + # 这里**不**改写版本号: 要验的就是当前树能不能构建, 版本由 tag 决定是 + # release workflow 的事。 + - name: Build the package + run: | + pip install --upgrade build + python -m build