Skip to content

Commit 56076da

Browse files
committed
Switch to standard tag-triggered release pipeline
Replaces the two-workflow / two-trigger setup (tick "pre-release" => TestPyPI; tick "latest" => PyPI) with a single tag-triggered workflow that runs three sequenced jobs: build, publish-testpypi, publish-pypi. TestPyPI failure hard-blocks PyPI via `needs:`. The `pypi` environment gates the final upload behind manual approval. This matches TabPFN's release pipeline shape, which is the conventional "standard" pattern for Python releases (also used by numpy, scikit-learn, etc. in some form). Trade-off: less inspection between TestPyPI and PyPI, but tighter coupling and only one human click per release. Other adjustments: - Tag/version guard: a Python step in the build job verifies that the pushed tag matches `[project].version` in pyproject.toml. Catches manual `git tag` typos before the build runs. Becomes a no-op once hatch-vcs lands (the version derives from the tag, can't mismatch). - Auto-create GitHub Release: extracts the matching `## [VERSION]` section from CHANGELOG.md, uploads as artifact, then `gh release create` runs after PyPI publish. The maintainer never visits the Releases UI; the release page appears automatically with notes. - Build runs once. Both publish jobs download the same `dist/` artifact, guaranteeing identical bytes go to TestPyPI and PyPI. Action versions kept SHA-pinned per the convention in pull_request.yml. upload/download-artifact bumped to v7/v8 to match TabPFN's release pipeline. Drops publish-test.yml entirely.
1 parent b849957 commit 56076da

2 files changed

Lines changed: 161 additions & 76 deletions

File tree

Lines changed: 161 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1-
name: Publish to PyPI
1+
name: Release to PyPI
2+
3+
# Triggered by pushing a `vX.Y.Z` tag. The workflow runs three sequenced
4+
# jobs: build the wheel/sdist once, publish to TestPyPI and verify install,
5+
# then (after manual approval at the `pypi` environment) publish the same
6+
# bytes to real PyPI and create a GitHub Release with notes pulled from
7+
# CHANGELOG.md.
8+
#
9+
# Standard release shape: tag => TestPyPI gate => PyPI. No GitHub Releases
10+
# UI ticking. The maintainer's only manual steps are pushing the tag and
11+
# approving the `pypi` environment.
212

3-
# Triggered when a GitHub Release is marked as published (not a pre-release).
4-
# The `pypi` environment should require a manual reviewer approval, so this
5-
# workflow pauses before the actual upload.
613
on:
7-
release:
8-
types: [released]
14+
push:
15+
tags:
16+
- "v*"
917

1018
permissions:
11-
contents: read
12-
id-token: write # required by PyPI Trusted Publishing (no API token needed)
19+
contents: write # release creation
20+
id-token: write # PyPI Trusted Publishing
1321

1422
jobs:
15-
publish:
16-
name: Build and publish to PyPI
23+
build:
24+
name: Build sdist + wheel
1725
runs-on: ubuntu-latest
1826
timeout-minutes: 15
19-
environment:
20-
name: pypi
27+
outputs:
28+
version: ${{ steps.ver.outputs.version }}
2129
steps:
2230
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2331
with:
@@ -33,14 +41,155 @@ jobs:
3341
with:
3442
enable-cache: true
3543

44+
- name: Extract version from tag
45+
id: ver
46+
run: |
47+
VERSION="${GITHUB_REF_NAME#v}"
48+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
49+
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
50+
51+
- name: Verify tag matches pyproject.toml version
52+
env:
53+
VERSION: ${{ env.VERSION }}
54+
run: |
55+
python - <<'PY'
56+
import os, re
57+
from pathlib import Path
58+
59+
v_tag = os.environ["VERSION"]
60+
s = Path("pyproject.toml").read_text(encoding="utf-8")
61+
m = re.search(r'(?ms)^\[project\]\s*\n.*?^\s*version\s*=\s*"([^"]+)"', s)
62+
if not m:
63+
raise SystemExit("Could not find [project].version in pyproject.toml")
64+
v_file = m.group(1).strip()
65+
if v_tag != v_file:
66+
raise SystemExit(f"Version mismatch: tag={v_tag} pyproject={v_file}")
67+
print("OK: tag matches pyproject version:", v_tag)
68+
PY
69+
3670
- name: Build sdist + wheel
3771
run: uv build
3872

3973
- name: Check artifacts
4074
run: uvx twine check dist/*
4175

76+
- name: Extract release notes from CHANGELOG.md
77+
env:
78+
VERSION: ${{ env.VERSION }}
79+
run: |
80+
python - <<'PY'
81+
import os, re, pathlib
82+
83+
v = os.environ["VERSION"]
84+
text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
85+
86+
# Match: ## [X.Y.Z] - YYYY-MM-DD ... up to next ## [ header (or EOF)
87+
pat = rf"(?ms)^##\s+\[{re.escape(v)}\]\s+-\s+\d{{4}}-\d{{2}}-\d{{2}}\s*\n(.*?)(?=^##\s+\[|\Z)"
88+
m = re.search(pat, text)
89+
if not m:
90+
raise SystemExit(f"No CHANGELOG.md section for {v}. Expected: ## [{v}] - YYYY-MM-DD")
91+
92+
notes = m.group(1).strip() + "\n"
93+
pathlib.Path("release_notes.md").write_text(notes, encoding="utf-8")
94+
print("Wrote release_notes.md")
95+
PY
96+
97+
- name: Upload build artifacts
98+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
99+
with:
100+
name: dist
101+
path: dist/
102+
retention-days: 7
103+
104+
- name: Upload release notes
105+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
106+
with:
107+
name: release_notes
108+
path: release_notes.md
109+
retention-days: 7
110+
111+
publish-testpypi:
112+
name: Publish to TestPyPI + verify install
113+
needs: build
114+
runs-on: ubuntu-latest
115+
timeout-minutes: 15
116+
environment:
117+
name: testpypi
118+
permissions:
119+
contents: read
120+
id-token: write
121+
steps:
122+
- name: Download build artifacts
123+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
124+
with:
125+
name: dist
126+
path: dist/
127+
128+
- name: Publish to TestPyPI
129+
uses: pypa/gh-action-pypi-publish@6733eb7d741f0b11ec6a39b58540dab7590f9b7d # v1.14.0
130+
with:
131+
repository-url: https://test.pypi.org/legacy/
132+
print-hash: true
133+
skip-existing: true
134+
135+
- name: Install uv
136+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
137+
138+
- name: Wait for TestPyPI propagation
139+
run: sleep 30
140+
141+
- name: Verify install from TestPyPI
142+
env:
143+
VERSION: ${{ needs.build.outputs.version }}
144+
run: |
145+
TEST_DIR=$(mktemp -d)
146+
cd "$TEST_DIR"
147+
uv venv --python 3.12
148+
source .venv/bin/activate
149+
uv pip install \
150+
--index-url https://test.pypi.org/simple/ \
151+
--extra-index-url https://pypi.org/simple/ \
152+
--index-strategy unsafe-best-match \
153+
"tabpfn-extensions==${VERSION}"
154+
python -c "import tabpfn_extensions; print('imported tabpfn_extensions OK, version:', tabpfn_extensions.__version__)"
155+
156+
publish-pypi:
157+
name: Publish to PyPI + create GitHub Release
158+
needs: [build, publish-testpypi]
159+
runs-on: ubuntu-latest
160+
timeout-minutes: 15
161+
environment:
162+
name: pypi
163+
permissions:
164+
contents: write
165+
id-token: write
166+
steps:
167+
- name: Download build artifacts
168+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
169+
with:
170+
name: dist
171+
path: dist/
172+
42173
- name: Publish to PyPI
43174
uses: pypa/gh-action-pypi-publish@6733eb7d741f0b11ec6a39b58540dab7590f9b7d # v1.14.0
44175
with:
45176
print-hash: true
46177
skip-existing: true
178+
179+
- name: Checkout code
180+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
181+
182+
- name: Download release notes
183+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
184+
with:
185+
name: release_notes
186+
path: .
187+
188+
- name: Create GitHub Release
189+
env:
190+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
191+
VERSION: ${{ needs.build.outputs.version }}
192+
run: |
193+
gh release create "v${VERSION}" \
194+
--title "v${VERSION}" \
195+
--notes-file release_notes.md

.github/workflows/publish-test.yml

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)