Skip to content

Commit 5e3e422

Browse files
committed
WS3
1 parent 9b8fbaa commit 5e3e422

File tree

3 files changed

+272
-6
lines changed

3 files changed

+272
-6
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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+
run: python3 scripts/check_recorder_version.py
68+
- name: Extract package version
69+
id: version
70+
run: |
71+
VERSION=$(python3 - <<'PY'
72+
from pathlib import Path
73+
import tomllib
74+
data = tomllib.loads(Path("codetracer-python-recorder/pyproject.toml").read_text())
75+
print(data["project"]["version"])
76+
PY
77+
)
78+
echo "package_version=${VERSION}" >> "$GITHUB_OUTPUT"
79+
- name: Assert tag matches version
80+
if: startsWith(github.ref, 'refs/tags/')
81+
run: |
82+
TAG="${GITHUB_REF##*/}"
83+
EXPECTED="recorder-v${{ steps.version.outputs.package_version }}"
84+
if [ "$TAG" != "$EXPECTED" ]; then
85+
echo "::error::Tag '$TAG' does not match package version '$EXPECTED'"
86+
exit 1
87+
fi
88+
89+
- name: Build artefacts (Linux targets)
90+
if: startsWith(matrix.name, 'linux-')
91+
uses: messense/maturin-action@v1
92+
with:
93+
command: build
94+
args: ${{ matrix.publish-args }}
95+
manylinux: ${{ matrix.manylinux }}
96+
97+
- name: Install Python 3.12 (macOS)
98+
if: startsWith(matrix.name, 'macos')
99+
id: mac_py312
100+
uses: actions/setup-python@v5
101+
with:
102+
python-version: '3.12'
103+
104+
- name: Install Python 3.13 (macOS)
105+
if: startsWith(matrix.name, 'macos')
106+
id: mac_py313
107+
uses: actions/setup-python@v5
108+
with:
109+
python-version: '3.13'
110+
111+
- name: Install Python 3.12 (Windows)
112+
if: matrix.name == 'windows-amd64'
113+
id: win_py312
114+
uses: actions/setup-python@v5
115+
with:
116+
python-version: '3.12'
117+
architecture: 'x64'
118+
119+
- name: Install Python 3.13 (Windows)
120+
if: matrix.name == 'windows-amd64'
121+
id: win_py313
122+
uses: actions/setup-python@v5
123+
with:
124+
python-version: '3.13'
125+
architecture: 'x64'
126+
127+
- name: Install maturin (macOS)
128+
if: startsWith(matrix.name, 'macos')
129+
run: python3 -m pip install --upgrade pip maturin>=1.5,<2
130+
131+
- name: Install maturin (Windows)
132+
if: matrix.name == 'windows-amd64'
133+
run: python -m pip install --upgrade pip maturin>=1.5,<2
134+
135+
- name: Build artefacts (macOS universal2)
136+
if: startsWith(matrix.name, 'macos')
137+
env:
138+
PYTHON312: ${{ steps.mac_py312.outputs.python-path }}/bin/python3
139+
PYTHON313: ${{ steps.mac_py313.outputs.python-path }}/bin/python3
140+
working-directory: ${{ env.CRATE_DIR }}
141+
run: maturin build ${{ matrix.publish-args }} --interpreter "$PYTHON312" "$PYTHON313"
142+
143+
- name: Build cp312 wheel (Windows)
144+
if: matrix.name == 'windows-amd64'
145+
env:
146+
PYTHON: ${{ steps.win_py312.outputs.python-path }}\python.exe
147+
working-directory: ${{ env.CRATE_DIR }}
148+
run: maturin build --release --interpreter "$env:PYTHON"
149+
150+
- name: Build cp313 wheel (Windows)
151+
if: matrix.name == 'windows-amd64'
152+
working-directory: ${{ env.CRATE_DIR }}
153+
env:
154+
PYTHON: ${{ steps.win_py313.outputs.python-path }}\python.exe
155+
working-directory: ${{ env.CRATE_DIR }}
156+
run: maturin build --release --interpreter "$env:PYTHON"
157+
158+
- name: Upload artefacts
159+
uses: actions/upload-artifact@v4
160+
with:
161+
name: dist-${{ matrix.name }}
162+
path: |
163+
codetracer-python-recorder/target/wheels/*
164+
if-no-files-found: error
165+
166+
publish-testpypi:
167+
name: Publish to TestPyPI
168+
needs: build
169+
runs-on: ubuntu-latest
170+
environment:
171+
name: testpypi
172+
steps:
173+
- uses: actions/checkout@v4
174+
- uses: actions/setup-python@v5
175+
with:
176+
python-version: '3.12'
177+
- name: Download artefacts
178+
uses: actions/download-artifact@v4
179+
with:
180+
pattern: dist-*
181+
path: dist
182+
merge-multiple: true
183+
- name: Inspect artefacts
184+
run: ls -R dist
185+
- name: Install maturin
186+
run: python3 -m pip install --upgrade pip maturin>=1.5,<2
187+
- name: Python smoke install (Linux wheel)
188+
run: |
189+
python3 scripts/check_recorder_version.py
190+
FILE=$(python3 scripts/select_recorder_artifact.py --wheel-dir dist --mode wheel --platform linux)
191+
python3 -m venv .smoke
192+
. .smoke/bin/activate
193+
pip install --upgrade pip
194+
pip install "$FILE"
195+
python -m codetracer_python_recorder --help
196+
- name: Publish to TestPyPI
197+
uses: pypa/gh-action-pypi-publish@release/v1
198+
with:
199+
repository-url: https://test.pypi.org/legacy/
200+
packages-dir: dist
201+
202+
publish-pypi:
203+
name: Publish to PyPI
204+
needs: publish-testpypi
205+
if: startsWith(github.ref, 'refs/tags/')
206+
runs-on: ubuntu-latest
207+
environment:
208+
name: pypi-production
209+
url: https://pypi.org/project/codetracer-python-recorder/
210+
steps:
211+
- uses: actions/checkout@v4
212+
- uses: actions/setup-python@v5
213+
with:
214+
python-version: '3.12'
215+
- name: Download artefacts
216+
uses: actions/download-artifact@v4
217+
with:
218+
pattern: dist-*
219+
path: dist
220+
merge-multiple: true
221+
- name: Publish to PyPI
222+
uses: pypa/gh-action-pypi-publish@release/v1
223+
with:
224+
packages-dir: dist

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
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+
- Configure PyPI/TestPyPI Trusted Publishing entries and environment protection rules to complete the release pipeline hand-off.
22+
- Author release documentation for maintainers covering workflow_dispatch usage and environment approvals.

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)