Skip to content

Commit 121a9c8

Browse files
committed
WS3
1 parent 9b8fbaa commit 121a9c8

File tree

3 files changed

+269
-6
lines changed

3 files changed

+269
-6
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
name: Recorder Release
2+
3+
on:
4+
push:
5+
tags:
6+
- recorder-v*
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
id-token: write
12+
13+
concurrency:
14+
group: recorder-release-${{ github.ref }}
15+
cancel-in-progress: false
16+
17+
env:
18+
CRATE_DIR: codetracer-python-recorder
19+
20+
jobs:
21+
verify:
22+
name: Verify Tests (Linux)
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: cachix/install-nix-action@v27
27+
with:
28+
nix_path: nixpkgs=channel:nixos-25.05
29+
extra_nix_config: |
30+
experimental-features = nix-command flakes
31+
- name: Prepare dev environment (Python 3.13)
32+
run: nix develop --command bash -lc 'just venv 3.13 dev'
33+
- name: Run test suite
34+
run: nix develop --command bash -lc 'just test'
35+
- name: Check recorder version parity
36+
run: nix develop --command bash -lc 'python3 scripts/check_recorder_version.py'
37+
38+
build:
39+
name: Build Artefacts (${{ matrix.name }})
40+
needs: verify
41+
runs-on: ${{ matrix.os }}
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
include:
46+
- name: linux-x86_64
47+
os: ubuntu-latest
48+
build-sdist: true
49+
publish-args: --release --sdist --interpreter python3.12 python3.13
50+
manylinux: 2014
51+
- name: linux-aarch64
52+
os: ubuntu-latest
53+
build-sdist: false
54+
publish-args: --release --interpreter python3.12 python3.13 --target aarch64-unknown-linux-gnu
55+
manylinux: 2014
56+
- name: macos-universal2
57+
os: macos-13
58+
build-sdist: false
59+
publish-args: --release --universal2
60+
- name: windows-amd64
61+
os: windows-latest
62+
build-sdist: false
63+
publish-args: --release
64+
steps:
65+
- uses: actions/checkout@v4
66+
- name: Check recorder version parity
67+
shell: bash
68+
run: |
69+
PYTHON=$(command -v python3 || command -v python)
70+
"$PYTHON" scripts/check_recorder_version.py
71+
- name: Extract package version
72+
id: version
73+
shell: bash
74+
run: |
75+
PYTHON=$(command -v python3 || command -v python)
76+
VERSION=$("$PYTHON" -c "import importlib, pathlib; tomllib = importlib.import_module('tomllib'); data = tomllib.loads(pathlib.Path('codetracer-python-recorder/pyproject.toml').read_text()); print(data['project']['version'])")
77+
echo "package_version=${VERSION}" >> "$GITHUB_OUTPUT"
78+
- name: Assert tag matches version
79+
if: startsWith(github.ref, 'refs/tags/')
80+
run: |
81+
TAG="${GITHUB_REF##*/}"
82+
EXPECTED="recorder-v${{ steps.version.outputs.package_version }}"
83+
if [ "$TAG" != "$EXPECTED" ]; then
84+
echo "::error::Tag '$TAG' does not match package version '$EXPECTED'"
85+
exit 1
86+
fi
87+
88+
- name: Build artefacts (Linux targets)
89+
if: startsWith(matrix.name, 'linux-')
90+
uses: messense/maturin-action@v1
91+
with:
92+
command: build
93+
args: ${{ matrix.publish-args }}
94+
manylinux: ${{ matrix.manylinux }}
95+
96+
- name: Install Python 3.12 (macOS)
97+
if: startsWith(matrix.name, 'macos')
98+
id: mac_py312
99+
uses: actions/setup-python@v5
100+
with:
101+
python-version: '3.12'
102+
103+
- name: Install Python 3.13 (macOS)
104+
if: startsWith(matrix.name, 'macos')
105+
id: mac_py313
106+
uses: actions/setup-python@v5
107+
with:
108+
python-version: '3.13'
109+
110+
- name: Install Python 3.12 (Windows)
111+
if: matrix.name == 'windows-amd64'
112+
id: win_py312
113+
uses: actions/setup-python@v5
114+
with:
115+
python-version: '3.12'
116+
architecture: 'x64'
117+
118+
- name: Install Python 3.13 (Windows)
119+
if: matrix.name == 'windows-amd64'
120+
id: win_py313
121+
uses: actions/setup-python@v5
122+
with:
123+
python-version: '3.13'
124+
architecture: 'x64'
125+
126+
- name: Install maturin (macOS)
127+
if: startsWith(matrix.name, 'macos')
128+
run: python3 -m pip install --upgrade pip maturin>=1.5,<2
129+
130+
- name: Install maturin (Windows)
131+
if: matrix.name == 'windows-amd64'
132+
run: python -m pip install --upgrade pip maturin>=1.5,<2
133+
134+
- name: Build artefacts (macOS universal2)
135+
if: startsWith(matrix.name, 'macos')
136+
env:
137+
PYTHON312: ${{ steps.mac_py312.outputs.python-path }}
138+
PYTHON313: ${{ steps.mac_py313.outputs.python-path }}
139+
working-directory: ${{ env.CRATE_DIR }}
140+
run: maturin build ${{ matrix.publish-args }} --interpreter "$PYTHON312" "$PYTHON313"
141+
142+
- name: Build cp312 wheel (Windows)
143+
if: matrix.name == 'windows-amd64'
144+
env:
145+
PYTHON: ${{ steps.win_py312.outputs.python-path }}
146+
working-directory: ${{ env.CRATE_DIR }}
147+
run: maturin build --release --interpreter "$env:PYTHON"
148+
149+
- name: Build cp313 wheel (Windows)
150+
if: matrix.name == 'windows-amd64'
151+
env:
152+
PYTHON: ${{ steps.win_py313.outputs.python-path }}
153+
working-directory: ${{ env.CRATE_DIR }}
154+
run: maturin build --release --interpreter "$env:PYTHON"
155+
156+
- name: Upload artefacts
157+
uses: actions/upload-artifact@v4
158+
with:
159+
name: dist-${{ matrix.name }}
160+
path: |
161+
codetracer-python-recorder/target/wheels/*
162+
if-no-files-found: error
163+
164+
publish-testpypi:
165+
name: Publish to TestPyPI
166+
needs: build
167+
runs-on: ubuntu-latest
168+
environment:
169+
name: testpypi
170+
steps:
171+
- uses: actions/checkout@v4
172+
- uses: actions/setup-python@v5
173+
with:
174+
python-version: '3.12'
175+
- name: Download artefacts
176+
uses: actions/download-artifact@v4
177+
with:
178+
pattern: dist-*
179+
path: dist
180+
merge-multiple: true
181+
- name: Inspect artefacts
182+
run: ls -R dist
183+
- name: Install maturin
184+
run: python3 -m pip install --upgrade pip maturin>=1.5,<2
185+
- name: Python smoke install (Linux wheel)
186+
run: |
187+
python3 scripts/check_recorder_version.py
188+
FILE=$(python3 scripts/select_recorder_artifact.py --wheel-dir dist --mode wheel --platform linux)
189+
python3 -m venv .smoke
190+
. .smoke/bin/activate
191+
pip install --upgrade pip
192+
pip install "$FILE"
193+
python -m codetracer_python_recorder --help
194+
- name: Publish to TestPyPI
195+
uses: pypa/gh-action-pypi-publish@release/v1
196+
with:
197+
repository-url: https://test.pypi.org/legacy/
198+
packages-dir: dist
199+
200+
publish-pypi:
201+
name: Publish to PyPI
202+
needs: publish-testpypi
203+
if: startsWith(github.ref, 'refs/tags/')
204+
runs-on: ubuntu-latest
205+
environment:
206+
name: pypi-production
207+
url: https://pypi.org/project/codetracer-python-recorder/
208+
steps:
209+
- uses: actions/checkout@v4
210+
- uses: actions/setup-python@v5
211+
with:
212+
python-version: '3.12'
213+
- name: Download artefacts
214+
uses: actions/download-artifact@v4
215+
with:
216+
pattern: dist-*
217+
path: dist
218+
merge-multiple: true
219+
- name: Publish to PyPI
220+
uses: pypa/gh-action-pypi-publish@release/v1
221+
with:
222+
packages-dir: dist

design-docs/codetracer-python-recorder-pypi-release-implementation-plan.status.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
- ✅ Expanded the Python API tests to cover end-to-end tracing via `start`/`stop` (`test_start_emits_trace_files`).
1313
- ✅ Verified `just py-test` passes with the new coverage.
1414

15+
## Workstream 3 – Cross-platform build & publish automation
16+
- ✅ Added `.github/workflows/recorder-release.yml` with a platform matrix (manylinux x86_64/aarch64, macOS universal2, Windows amd64) and a Linux verification gate reusing our existing test suite.
17+
- ✅ Integrated artefact collection plus a TestPyPI smoke install that exercises the CLI before invoking Trusted Publishing-friendly uploads.
18+
- ✅ Added a guarded PyPI promotion job that reuses the staged artefacts and requires environment approval prior to publishing.
19+
1520
## Next Tasks
16-
- Kick off Workstream 3: design the cross-platform release workflow (`recorder-release.yml`) and wire in TestPyPI publishing.
17-
- Add release-pipeline steps to invoke the new smoke install target once the workflow skeleton exists.
21+
- First tagged release should monitor the new workflow end-to-end and capture any follow-up improvements in the release tracker issue.

scripts/select_recorder_artifact.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import argparse
77
import sys
88
from pathlib import Path
9+
from typing import Literal
910

1011

1112
def parse_args() -> argparse.Namespace:
@@ -22,15 +23,51 @@ def parse_args() -> argparse.Namespace:
2223
default="wheel",
2324
help="Prefer a wheel for the current interpreter or fallback to the source distribution.",
2425
)
26+
parser.add_argument(
27+
"--platform",
28+
choices=("auto", "linux", "macos", "windows", "any"),
29+
default="auto",
30+
help="Restrict wheel selection to the given platform; defaults to the current platform.",
31+
)
2532
return parser.parse_args()
2633

2734

28-
def choose_artifact(wheel_dir: Path, mode: str) -> Path:
29-
candidates = []
35+
def _normalize_platform(value: str) -> Literal["linux", "macos", "windows", "any"]:
36+
if value == "auto":
37+
if sys.platform.startswith("linux"):
38+
return "linux"
39+
if sys.platform == "darwin":
40+
return "macos"
41+
if sys.platform.startswith("win"):
42+
return "windows"
43+
return "any"
44+
return value # type: ignore[return-value]
45+
46+
47+
def _is_wheel_compatible(path: Path, platform: str) -> bool:
48+
name = path.name
49+
if platform == "any":
50+
return True
51+
if platform == "linux":
52+
return "manylinux" in name or "linux" in name
53+
if platform == "macos":
54+
return "macosx" in name or "universal2" in name
55+
if platform == "windows":
56+
return "win" in name
57+
return False
58+
59+
60+
def choose_artifact(wheel_dir: Path, mode: str, platform: str) -> Path:
61+
platform = _normalize_platform(platform)
62+
candidates: list[Path] = []
3063
if mode == "wheel":
3164
abi = f"cp{sys.version_info.major}{sys.version_info.minor}"
3265
pattern = f"codetracer_python_recorder-*-{abi}-{abi}-*.whl"
33-
candidates = sorted(wheel_dir.glob(pattern))
66+
candidates = [
67+
path
68+
for path in sorted(wheel_dir.glob(pattern))
69+
if _is_wheel_compatible(path, platform)
70+
]
3471
if not candidates:
3572
candidates = sorted(wheel_dir.glob("codetracer_python_recorder-*.tar.gz"))
3673
if not candidates:
@@ -40,7 +77,7 @@ def choose_artifact(wheel_dir: Path, mode: str) -> Path:
4077

4178
def main() -> int:
4279
args = parse_args()
43-
artifact = choose_artifact(args.wheel_dir, args.mode)
80+
artifact = choose_artifact(args.wheel_dir, args.mode, args.platform)
4481
sys.stdout.write(str(artifact))
4582
return 0
4683

0 commit comments

Comments
 (0)