Skip to content

Commit 53fb5bd

Browse files
committed
feat(release): publish tagged artifacts to PyPI
1 parent 5d93ea3 commit 53fb5bd

5 files changed

Lines changed: 113 additions & 23 deletions

File tree

.github/workflows/release.yml

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ on:
77
workflow_dispatch:
88

99
jobs:
10+
release-version-check:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.12"
19+
20+
- name: Validate tag, project version, and PyPI version
21+
run: python scripts/check_release_version.py
22+
1023
typecheck:
1124
runs-on: ubuntu-latest
1225

@@ -30,7 +43,9 @@ jobs:
3043

3144
sdist:
3245
runs-on: ubuntu-latest
33-
needs: typecheck
46+
needs:
47+
- release-version-check
48+
- typecheck
3449

3550
steps:
3651
- uses: actions/checkout@v4
@@ -61,7 +76,9 @@ jobs:
6176

6277
wheels:
6378
runs-on: ${{ matrix.os }}
64-
needs: typecheck
79+
needs:
80+
- release-version-check
81+
- typecheck
6582
strategy:
6683
fail-fast: false
6784
matrix:
@@ -93,16 +110,17 @@ jobs:
93110
name: rapidobj-wheels-${{ matrix.os }}
94111
path: wheelhouse/*.whl
95112

96-
publish-testpypi:
97-
name: Publish to TestPyPI
113+
publish-pypi:
114+
name: Publish to PyPI
98115
runs-on: ubuntu-latest
99116
needs:
117+
- release-version-check
100118
- sdist
101119
- wheels
102120
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
103121
environment:
104-
name: testpypi
105-
url: https://test.pypi.org/p/rapidobj
122+
name: pypi
123+
url: https://pypi.org/p/rapidobj
106124
permissions:
107125
id-token: write
108126
contents: read
@@ -121,7 +139,5 @@ jobs:
121139
merge-multiple: true
122140
path: dist
123141

124-
- name: Publish package distributions to TestPyPI
142+
- name: Publish package distributions to PyPI
125143
uses: pypa/gh-action-pypi-publish@release/v1
126-
with:
127-
repository-url: https://test.pypi.org/legacy/

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,24 @@ stay efficient.
1212

1313
## Install
1414

15+
From PyPI:
16+
17+
```bash
18+
python -m pip install rapidobj
19+
```
20+
1521
From source:
1622

1723
```bash
1824
uv sync
1925
uv build --wheel --python 3.12 --no-sources
20-
python -m pip install dist/rapidobj-0.1.0-cp312-cp312-*.whl
26+
python -m pip install dist/rapidobj-0.1.3-cp312-cp312-*.whl
2127
```
2228

2329
For release builds, GitHub Actions is the authoritative wheel pipeline. Pull
2430
requests validate the package, and version tags build Linux and Windows wheels
25-
for CPython 3.12, 3.13, and 3.14. Tag builds also publish the validated
26-
artifacts to TestPyPI via Trusted Publishing.
31+
for CPython 3.12, 3.13, and 3.14. Tag builds publish the validated artifacts
32+
to PyPI via Trusted Publishing after a strict version check.
2733

2834
## Minimal Usage
2935

RELEASE.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
Use semantic version tags: `vX.Y.Z`.
66

7+
Production releases must satisfy all of the following:
8+
- the Git tag is `vX.Y.Z`
9+
- `[project].version` in `pyproject.toml` is `X.Y.Z`
10+
- `X.Y.Z` is not already published on PyPI
11+
712
## Local Validation
813

914
1. Clean old artifacts.
@@ -28,11 +33,12 @@ Use semantic version tags: `vX.Y.Z`.
2833
- `sdist` build and smoke test
2934
- Linux and Windows wheel builds for CPython `3.12`, `3.13`, and `3.14`
3035
2. Version tags (`vX.Y.Z`) run the release workflow:
36+
- verify that tag version, `pyproject.toml` version, and PyPI publishability match
3137
- build the `sdist`
3238
- build Linux and Windows wheel artifacts
3339
- run metadata checks and smoke tests
3440
- upload artifacts to GitHub Actions
35-
- publish those exact artifacts to TestPyPI via Trusted Publishing
41+
- publish those exact artifacts to PyPI via Trusted Publishing
3642

3743
## GitHub Source Release
3844

@@ -43,14 +49,10 @@ Use semantic version tags: `vX.Y.Z`.
4349
3. Wait for the release workflow to finish and download the generated artifacts if needed.
4450
4. Create GitHub release from the tag and include changelog notes.
4551

46-
## TestPyPI Publish
47-
48-
1. Configure a Trusted Publisher for the repository on TestPyPI.
49-
2. Push a version tag (`vX.Y.Z`).
50-
3. Wait for the `Release Artifacts` workflow to finish.
51-
4. Verify the package page and an install from TestPyPI.
52-
5352
## PyPI Publish
5453

55-
1. After TestPyPI validation, point the publish job at production PyPI.
56-
2. Reuse the same tag-triggered artifact publish flow with Trusted Publishing.
54+
1. Configure a Trusted Publisher for the repository on PyPI.
55+
2. Bump `[project].version` in `pyproject.toml`.
56+
3. Push a matching version tag (`vX.Y.Z`).
57+
4. Wait for the `Release Artifacts` workflow to finish.
58+
5. Verify the package page and an install from PyPI.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "rapidobj"
3-
version = "0.1.0"
3+
version = "0.1.3"
44
description = "Fast OBJ parser with Python bindings"
55
readme = "README.md"
66
license = { file = "LICENSE" }

scripts/check_release_version.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Validate tag, project version, and PyPI publishability for releases."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import os
7+
from pathlib import Path
8+
import sys
9+
import tomllib
10+
import urllib.error
11+
import urllib.request
12+
13+
14+
def fail(message: str) -> int:
15+
print(f"ERROR: {message}", file=sys.stderr)
16+
return 1
17+
18+
19+
def main() -> int:
20+
event_name = os.environ.get("GITHUB_EVENT_NAME", "")
21+
ref = os.environ.get("GITHUB_REF", "")
22+
23+
if not (event_name == "push" and ref.startswith("refs/tags/v")):
24+
print("Skipping release version check outside tag push context.")
25+
return 0
26+
27+
tag_version = ref.removeprefix("refs/tags/v")
28+
pyproject = Path("pyproject.toml")
29+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
30+
project = data["project"]
31+
package_name = project["name"]
32+
project_version = project["version"]
33+
34+
print(f"Package: {package_name}")
35+
print(f"Tag version: {tag_version}")
36+
print(f"pyproject.toml version: {project_version}")
37+
38+
if project_version != tag_version:
39+
return fail(
40+
"Tag version does not match pyproject.toml version "
41+
f"({tag_version} != {project_version})."
42+
)
43+
44+
url = f"https://pypi.org/pypi/{package_name}/json"
45+
try:
46+
with urllib.request.urlopen(url) as response:
47+
pypi_data = json.load(response)
48+
except urllib.error.HTTPError as exc:
49+
if exc.code == 404:
50+
print("Package does not exist on PyPI yet; version is publishable.")
51+
return 0
52+
raise
53+
54+
releases = pypi_data.get("releases", {})
55+
latest = pypi_data.get("info", {}).get("version", "<unknown>")
56+
print(f"Latest version on PyPI: {latest}")
57+
58+
if tag_version in releases and releases[tag_version]:
59+
return fail(f"Version {tag_version} is already published on PyPI.")
60+
61+
print(f"Version {tag_version} is not yet published on PyPI.")
62+
return 0
63+
64+
65+
if __name__ == "__main__":
66+
raise SystemExit(main())

0 commit comments

Comments
 (0)