Skip to content

Commit 0fbade1

Browse files
committed
Polish PyPI release automation
1 parent 16f9f79 commit 0fbade1

4 files changed

Lines changed: 113 additions & 2 deletions

File tree

.github/workflows/release.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ on:
66
- "v*"
77
workflow_dispatch:
88

9+
concurrency:
10+
group: release-${{ github.ref }}
11+
cancel-in-progress: false
12+
913
permissions:
1014
contents: write
1115
id-token: write
@@ -32,7 +36,7 @@ jobs:
3236
with:
3337
maturin-version: v1.13.1
3438
target: ${{ matrix.target }}
35-
manylinux: "2_28"
39+
manylinux: "2014"
3640
args: --release --locked --compatibility pypi --out dist --features openblas-static -i python${{ matrix.python-version }}
3741

3842
- name: Upload wheel artifact
@@ -141,6 +145,16 @@ jobs:
141145
find artifacts -type f \( -name '*.whl' -o -name '*.tar.gz' \) -exec cp {} dist/ \;
142146
ls -lh dist
143147
148+
- name: Set up Python
149+
uses: actions/setup-python@v5
150+
with:
151+
python-version: "3.13"
152+
153+
- name: Validate distribution metadata
154+
run: |
155+
python -m pip install --upgrade pip twine
156+
python -m twine check --strict dist/*
157+
144158
- name: Publish to PyPI
145159
uses: pypa/gh-action-pypi-publish@release/v1
146160
with:

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,25 @@ Relevant files:
690690

691691
The release workflow builds wheels with `openblas-static` enabled so binary installs are as self-contained as practical.
692692

693+
### Releasing to PyPI
694+
695+
The PyPI project name is `pqk`.
696+
697+
Once the one-time PyPI Trusted Publisher setup is done for:
698+
699+
- owner: `BaseModelAI`
700+
- repository: `pkq`
701+
- workflow: `.github/workflows/release.yml`
702+
- environment: `pypi`
703+
704+
the normal release path is:
705+
706+
```bash
707+
python scripts/release.py 1.0.1 --commit --tag --push
708+
```
709+
710+
That updates the version in the release metadata, creates the release commit, creates tag `v1.0.1`, and pushes both to `origin`. The tag push triggers the GitHub release workflow, which builds the wheels and publishes them to PyPI.
711+
693712
## Original project and related work
694713

695714
### Original implementation

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ description = "Modern Rust implementation of PQk-means for large-scale clusterin
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = { file = "LICENSE" }
12-
authors = [{ name = "Codex" }]
12+
authors = [{ name = "Jacek Dąbrowski", email = "ponythewhite@gmail.com" }]
13+
maintainers = [{ name = "BaseModelAI" }]
1314
keywords = ["clustering", "product-quantization", "pqkmeans", "vector-search", "rust", "parquet"]
1415
classifiers = [
1516
"Development Status :: 5 - Production/Stable",
@@ -32,6 +33,12 @@ dependencies = [
3233
"pyarrow>=15.0",
3334
]
3435

36+
[project.urls]
37+
Homepage = "https://github.com/BaseModelAI/pkq"
38+
Repository = "https://github.com/BaseModelAI/pkq"
39+
Issues = "https://github.com/BaseModelAI/pkq/issues"
40+
Releases = "https://github.com/BaseModelAI/pkq/releases"
41+
3542
[project.optional-dependencies]
3643
dev = [
3744
"pytest>=8.0",

scripts/release.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import re
6+
import subprocess
7+
from pathlib import Path
8+
9+
10+
REPO_ROOT = Path(__file__).resolve().parents[1]
11+
VERSION_PATTERN = re.compile(r"\b\d+\.\d+\.\d+\b")
12+
13+
TARGETS = [
14+
REPO_ROOT / "Cargo.toml",
15+
REPO_ROOT / "pyproject.toml",
16+
REPO_ROOT / "scripts" / "render_sexy_hero.py",
17+
]
18+
19+
20+
def parse_args() -> argparse.Namespace:
21+
parser = argparse.ArgumentParser(
22+
description="Bump pqk version metadata, optionally commit, tag, and push a release."
23+
)
24+
parser.add_argument("version", help="SemVer version number, for example 1.0.1")
25+
parser.add_argument("--commit", action="store_true", help="Create a git commit for the version bump")
26+
parser.add_argument("--tag", action="store_true", help="Create an annotated git tag v<version>")
27+
parser.add_argument("--push", action="store_true", help="Push main and tags to the selected remote")
28+
parser.add_argument("--remote", default="origin", help="Git remote to push to when --push is set")
29+
return parser.parse_args()
30+
31+
32+
def validate_version(version: str) -> None:
33+
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
34+
raise SystemExit(f"invalid version '{version}', expected SemVer like 1.0.1")
35+
36+
37+
def run(*args: str) -> None:
38+
subprocess.run(args, cwd=REPO_ROOT, check=True)
39+
40+
41+
def replace_version(path: Path, version: str) -> None:
42+
original = path.read_text()
43+
updated, count = VERSION_PATTERN.subn(version, original, count=1)
44+
if count != 1:
45+
raise SystemExit(f"could not update version in {path}")
46+
path.write_text(updated)
47+
48+
49+
def main() -> None:
50+
args = parse_args()
51+
validate_version(args.version)
52+
53+
for path in TARGETS:
54+
replace_version(path, args.version)
55+
56+
if args.commit:
57+
run("git", "add", *(str(path.relative_to(REPO_ROOT)) for path in TARGETS))
58+
run("git", "commit", "-m", f"Release v{args.version}")
59+
60+
if args.tag:
61+
run("git", "tag", "-a", f"v{args.version}", "-m", f"Release v{args.version}")
62+
63+
if args.push:
64+
run("git", "push", args.remote, "main")
65+
run("git", "push", args.remote, "--tags")
66+
67+
print(f"updated version metadata to {args.version}")
68+
69+
70+
if __name__ == "__main__":
71+
main()

0 commit comments

Comments
 (0)