Skip to content

Commit 518cffe

Browse files
Release v0.1.0 to PyPI and GitHub (#87)
* ci: add v0.1.0 trusted release pipeline * fix: fail closed release trigger
1 parent 8a1fadc commit 518cffe

6 files changed

Lines changed: 323 additions & 10 deletions

File tree

.github/workflows/release.yml

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags: ["v0.1.0"]
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
wheel:
13+
name: Build ${{ matrix.os }} cp${{ matrix.py }} wheel
14+
runs-on: ${{ matrix.runner }}
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
include:
19+
- { os: macos, runner: macos-14, py: "311", python: "3.11" }
20+
- { os: macos, runner: macos-14, py: "312", python: "3.12" }
21+
- { os: macos, runner: macos-14, py: "313", python: "3.13" }
22+
- { os: linux, runner: ubuntu-22.04, py: "311", python: "3.11" }
23+
- { os: linux, runner: ubuntu-22.04, py: "312", python: "3.12" }
24+
- { os: linux, runner: ubuntu-22.04, py: "313", python: "3.13" }
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: ${{ matrix.python }}
30+
- uses: astral-sh/setup-uv@v5
31+
- name: Build macOS wheel
32+
if: matrix.os == 'macos'
33+
run: uv build --wheel --python ${{ matrix.python }} --out-dir dist
34+
- name: Build manylinux wheel
35+
if: matrix.os == 'linux'
36+
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380
37+
with:
38+
command: build
39+
maturin-version: v1.11.5
40+
target: x86_64-unknown-linux-gnu
41+
manylinux: 2_28
42+
args: --release --locked --compatibility pypi --interpreter python${{ matrix.python }} --out dist
43+
- name: Inspect wheel
44+
run: python scripts/inspect_release_artifact.py dist/*.whl
45+
- uses: actions/upload-artifact@v4
46+
with:
47+
name: release-wheel-${{ matrix.os }}-cp${{ matrix.py }}
48+
path: dist/*.whl
49+
if-no-files-found: error
50+
51+
sdist:
52+
name: Build source distribution
53+
runs-on: ubuntu-22.04
54+
steps:
55+
- uses: actions/checkout@v4
56+
- uses: actions/setup-python@v5
57+
with:
58+
python-version: "3.11"
59+
- uses: astral-sh/setup-uv@v5
60+
- run: uv build --sdist --out-dir dist
61+
- run: python scripts/inspect_release_artifact.py dist/*.tar.gz
62+
- uses: actions/upload-artifact@v4
63+
with:
64+
name: release-sdist
65+
path: dist/*.tar.gz
66+
if-no-files-found: error
67+
68+
verify:
69+
name: Verify exact release set
70+
needs: [wheel, sdist]
71+
runs-on: ubuntu-22.04
72+
steps:
73+
- uses: actions/checkout@v4
74+
- uses: actions/setup-python@v5
75+
with:
76+
python-version: "3.11"
77+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
78+
with:
79+
pattern: release-*
80+
path: dist
81+
merge-multiple: true
82+
- run: python scripts/inspect_release_artifact.py dist/*
83+
- name: Generate release manifest
84+
run: >-
85+
python scripts/generate_release_manifest.py
86+
--dist dist
87+
--version 0.1.0
88+
--git-commit "$GITHUB_SHA"
89+
--model-sha256 119c0f19767b61446e04da1f8f01a001edf97a47a66965e7146db2483b4937a1
90+
--output release-manifest-v0.1.0.json
91+
- uses: actions/upload-artifact@v4
92+
with:
93+
name: verified-release
94+
path: |
95+
dist/*
96+
release-manifest-v0.1.0.json
97+
if-no-files-found: error
98+
99+
release-candidate-inference:
100+
name: Verify release candidate inference
101+
needs: verify
102+
runs-on: ubuntu-22.04
103+
timeout-minutes: 180
104+
steps:
105+
- uses: actions/checkout@v4
106+
- uses: actions/setup-python@v5
107+
with:
108+
python-version: "3.11"
109+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
110+
with:
111+
name: verified-release
112+
path: release
113+
- name: Install the verified manylinux wheel
114+
run: |
115+
pip install --only-binary :all: \
116+
release/dist/*cp311*manylinux_2_28_x86_64.whl
117+
- name: Verify all frozen predictions
118+
working-directory: ${{ runner.temp }}
119+
env:
120+
PYTHONPATH: ${{ github.workspace }}
121+
run: >-
122+
python -m scripts.certify_inference_backend
123+
--device cpu
124+
--dataset "$GITHUB_WORKSPACE/eval/voice_agent_itn/voice_agent_eval.jsonl"
125+
--frozen-digest "$GITHUB_WORKSPACE/eval/voice_agent_itn/FROZEN.sha256"
126+
--reference-records "$GITHUB_WORKSPACE/eval/voice_agent_itn/results/first-evaluation/premove-itn/records.jsonl"
127+
--wheel "$GITHUB_WORKSPACE"/release/dist/*cp311*manylinux_2_28_x86_64.whl
128+
--expected-wheel-platform manylinux_2_28_x86_64
129+
--source-commit "$GITHUB_SHA"
130+
--unavailable-device mps
131+
--output release-candidate-inference.json
132+
133+
draft-github-release:
134+
name: Stage GitHub release
135+
if: github.event_name == 'push' && github.ref == 'refs/tags/v0.1.0'
136+
needs: release-candidate-inference
137+
runs-on: ubuntu-22.04
138+
permissions:
139+
contents: write
140+
steps:
141+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
142+
with:
143+
name: verified-release
144+
path: release
145+
- name: Create draft with verified artifacts
146+
env:
147+
GH_TOKEN: ${{ github.token }}
148+
run: >-
149+
gh release create "$GITHUB_REF_NAME" release/dist/*
150+
release/release-manifest-v0.1.0.json
151+
--repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --draft
152+
153+
publish-pypi:
154+
name: Publish to PyPI
155+
if: github.event_name == 'push' && github.ref == 'refs/tags/v0.1.0'
156+
needs: draft-github-release
157+
runs-on: ubuntu-22.04
158+
environment:
159+
name: pypi
160+
url: https://pypi.org/p/premove-itn
161+
permissions:
162+
id-token: write
163+
steps:
164+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
165+
with:
166+
name: verified-release
167+
path: release
168+
- name: Keep only distributions in upload directory
169+
run: |
170+
mkdir dist
171+
mv release/dist/* dist/
172+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
173+
with:
174+
print-hash: true
175+
176+
public-install:
177+
name: Public install on ${{ matrix.os }}
178+
if: github.event_name == 'push' && github.ref == 'refs/tags/v0.1.0'
179+
needs: publish-pypi
180+
runs-on: ${{ matrix.runner }}
181+
timeout-minutes: 180
182+
strategy:
183+
matrix:
184+
include:
185+
- { os: Linux, runner: ubuntu-22.04 }
186+
- { os: macOS, runner: macos-14 }
187+
steps:
188+
- uses: actions/setup-python@v5
189+
with:
190+
python-version: "3.11"
191+
- name: Install only the public binary wheel
192+
run: |
193+
pip install --no-cache-dir --only-binary :all: \
194+
premove-itn==0.1.0
195+
- name: Verify public CLI and package
196+
run: |
197+
premove-itn --version
198+
premove-itn --help >/dev/null
199+
python -c "import premove_itn; print(premove_itn.__file__)"
200+
- name: Verify public Linux inference
201+
if: matrix.os == 'Linux'
202+
run: python -c "from premove_itn import PremoveITN; assert PremoveITN.from_pretrained(device='cpu').normalize('call me at four thirty') == 'call me at 04:30'"
203+
204+
github-release:
205+
name: Publish GitHub release
206+
if: github.event_name == 'push' && github.ref == 'refs/tags/v0.1.0'
207+
needs: public-install
208+
runs-on: ubuntu-22.04
209+
permissions:
210+
contents: write
211+
steps:
212+
- name: Publish the staged release
213+
env:
214+
GH_TOKEN: ${{ github.token }}
215+
run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@ The package is prepared for:
2626
pip install premove-itn
2727
```
2828

29-
The PyPI v0.1.0 release is not published yet. It will be published after
30-
automated release gates and platform validation are complete. Contributors can
31-
currently build and install the release wheel locally:
29+
Install the exact v0.1.0 release from PyPI:
3230

3331
```bash
34-
uv build
35-
pip install dist/premove_itn-*.whl
32+
pip install premove-itn==0.1.0
3633
```
3734

3835
The model weights are downloaded separately from the frozen

docs/model-card.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ print(itn.normalize("call me at four thirty"))
4949
# call me at 04:30
5050
```
5151

52-
The `premove-itn` PyPI release is not published yet. Until the public package
53-
release, contributors can build and install the release wheel from the GitHub
54-
repository. Create one `PremoveITN` instance and reuse it; model initialization
55-
is expensive compared with warm normalization.
52+
Install the package with `pip install premove-itn==0.1.0`. Create one
53+
`PremoveITN` instance and reuse it; model initialization is expensive compared
54+
with warm normalization.
5655

5756
`device="auto"` selects CUDA when available, then Apple MPS, then CPU. The
5857
Release wheels are validated on macOS 14+ arm64 and `manylinux_2_28` x86_64

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ keywords = [
2525
"voice agents",
2626
]
2727
classifiers = [
28-
"Development Status :: 2 - Pre-Alpha",
28+
"Development Status :: 3 - Alpha",
2929
"License :: OSI Approved :: MIT License",
3030
"Programming Language :: Python :: 3",
3131
"Programming Language :: Python :: 3.11",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Verify the public artifact set and write its release manifest."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import hashlib
7+
import json
8+
from pathlib import Path
9+
10+
PYTHON_VERSIONS = ("311", "312", "313")
11+
12+
13+
def expected_names(version: str) -> set[str]:
14+
prefix = f"premove_itn-{version}"
15+
return {
16+
*(f"{prefix}-cp{py}-cp{py}-macosx_11_0_arm64.whl" for py in PYTHON_VERSIONS),
17+
*(
18+
f"{prefix}-cp{py}-cp{py}-manylinux_2_28_x86_64.whl"
19+
for py in PYTHON_VERSIONS
20+
),
21+
f"{prefix}.tar.gz",
22+
}
23+
24+
25+
def build_manifest(
26+
dist: Path, *, version: str, git_commit: str, model_sha256: str
27+
) -> dict[str, object]:
28+
files = {path.name: path for path in dist.iterdir() if path.is_file()}
29+
expected = expected_names(version)
30+
if set(files) != expected:
31+
raise RuntimeError(
32+
f"release artifact set mismatch: missing={sorted(expected - set(files))}, "
33+
f"unexpected={sorted(set(files) - expected)}"
34+
)
35+
artifacts = {
36+
name: hashlib.sha256(files[name].read_bytes()).hexdigest()
37+
for name in sorted(files)
38+
}
39+
return {
40+
"schema_version": 1,
41+
"version": version,
42+
"git_commit": git_commit,
43+
"model_repository": "premove-ai/premove-itn",
44+
"model_revision": "80bda5e2e1fe9542aa628597090242df57c1a157",
45+
"model_sha256": model_sha256,
46+
"artifacts": artifacts,
47+
}
48+
49+
50+
def main() -> None:
51+
parser = argparse.ArgumentParser()
52+
parser.add_argument("--dist", type=Path, required=True)
53+
parser.add_argument("--version", required=True)
54+
parser.add_argument("--git-commit", required=True)
55+
parser.add_argument("--model-sha256", required=True)
56+
parser.add_argument("--output", type=Path, required=True)
57+
arguments = parser.parse_args()
58+
manifest = build_manifest(
59+
arguments.dist,
60+
version=arguments.version,
61+
git_commit=arguments.git_commit,
62+
model_sha256=arguments.model_sha256,
63+
)
64+
arguments.output.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
65+
66+
67+
if __name__ == "__main__":
68+
main()

tests/test_release_manifest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from scripts.generate_release_manifest import build_manifest, expected_names
6+
7+
8+
def test_expected_names_define_seven_public_artifacts() -> None:
9+
names = expected_names("0.1.0")
10+
assert len(names) == 7
11+
assert "premove_itn-0.1.0-cp311-cp311-manylinux_2_28_x86_64.whl" in names
12+
assert "premove_itn-0.1.0.tar.gz" in names
13+
14+
15+
def test_build_manifest_hashes_exact_artifact_set(tmp_path: Path) -> None:
16+
for name in expected_names("0.1.0"):
17+
(tmp_path / name).write_bytes(name.encode())
18+
manifest = build_manifest(
19+
tmp_path, version="0.1.0", git_commit="abc123", model_sha256="model123"
20+
)
21+
assert manifest["git_commit"] == "abc123"
22+
assert manifest["model_sha256"] == "model123"
23+
assert len(manifest["artifacts"]) == 7
24+
25+
26+
def test_build_manifest_rejects_incomplete_set(tmp_path: Path) -> None:
27+
(tmp_path / "premove_itn-0.1.0.tar.gz").write_bytes(b"sdist")
28+
with pytest.raises(RuntimeError, match="artifact set mismatch"):
29+
build_manifest(
30+
tmp_path,
31+
version="0.1.0",
32+
git_commit="abc123",
33+
model_sha256="model123",
34+
)

0 commit comments

Comments
 (0)