pylibgrit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pylibgrit-0.1.0/.github/scripts/check_release_inventory.py +94 -0
- pylibgrit-0.1.0/.github/scripts/test_check_release_inventory.py +84 -0
- pylibgrit-0.1.0/.github/workflows/ci.yml +193 -0
- pylibgrit-0.1.0/.github/workflows/release.yml +289 -0
- pylibgrit-0.1.0/.gitignore +11 -0
- pylibgrit-0.1.0/Cargo.lock +1320 -0
- pylibgrit-0.1.0/Cargo.toml +21 -0
- pylibgrit-0.1.0/LICENSE +21 -0
- pylibgrit-0.1.0/PKG-INFO +284 -0
- pylibgrit-0.1.0/README.md +266 -0
- pylibgrit-0.1.0/docs/superpowers/api-matrix.md +334 -0
- pylibgrit-0.1.0/docs/superpowers/plans/2026-06-13-pygrit-bindings.md +2023 -0
- pylibgrit-0.1.0/docs/superpowers/plans/2026-06-14-pygrit-6wheel-coverage.md +564 -0
- pylibgrit-0.1.0/docs/superpowers/plans/2026-06-14-pygrit-release-readiness.md +813 -0
- pylibgrit-0.1.0/docs/superpowers/specs/2026-06-13-pygrit-bindings-design.md +299 -0
- pylibgrit-0.1.0/docs/superpowers/specs/2026-06-14-pygrit-6wheel-coverage-design.md +173 -0
- pylibgrit-0.1.0/docs/superpowers/specs/2026-06-14-pygrit-release-readiness-design.md +277 -0
- pylibgrit-0.1.0/docs/superpowers/specs/2026-06-15-grit-lib-write-surface-spike.md +112 -0
- pylibgrit-0.1.0/pyproject.toml +43 -0
- pylibgrit-0.1.0/python/pylibgrit/__init__.py +64 -0
- pylibgrit-0.1.0/python/pylibgrit/__init__.pyi +242 -0
- pylibgrit-0.1.0/python/pylibgrit/py.typed +0 -0
- pylibgrit-0.1.0/rust-toolchain.toml +3 -0
- pylibgrit-0.1.0/src/config.rs +57 -0
- pylibgrit-0.1.0/src/diff.rs +246 -0
- pylibgrit-0.1.0/src/error.rs +107 -0
- pylibgrit-0.1.0/src/lib.rs +49 -0
- pylibgrit-0.1.0/src/objects.rs +567 -0
- pylibgrit-0.1.0/src/odb.rs +44 -0
- pylibgrit-0.1.0/src/refs.rs +141 -0
- pylibgrit-0.1.0/src/repository.rs +543 -0
- pylibgrit-0.1.0/src/revwalk.rs +62 -0
- pylibgrit-0.1.0/tests/__init__.py +0 -0
- pylibgrit-0.1.0/tests/conftest.py +56 -0
- pylibgrit-0.1.0/tests/gitlib.py +54 -0
- pylibgrit-0.1.0/tests/test_concurrency.py +38 -0
- pylibgrit-0.1.0/tests/test_config.py +32 -0
- pylibgrit-0.1.0/tests/test_diff.py +305 -0
- pylibgrit-0.1.0/tests/test_errors.py +8 -0
- pylibgrit-0.1.0/tests/test_ffi_lifetime.py +81 -0
- pylibgrit-0.1.0/tests/test_objectid.py +35 -0
- pylibgrit-0.1.0/tests/test_objectkind.py +21 -0
- pylibgrit-0.1.0/tests/test_objects.py +446 -0
- pylibgrit-0.1.0/tests/test_odb.py +41 -0
- pylibgrit-0.1.0/tests/test_refs.py +53 -0
- pylibgrit-0.1.0/tests/test_repository.py +76 -0
- pylibgrit-0.1.0/tests/test_resolve.py +52 -0
- pylibgrit-0.1.0/tests/test_revwalk.py +169 -0
- pylibgrit-0.1.0/tests/test_smoke.py +6 -0
- pylibgrit-0.1.0/uv.lock +327 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate the release artifact set before publishing to PyPI.
|
|
3
|
+
|
|
4
|
+
Asserts that a ``dist/`` directory contains exactly the artifacts the release
|
|
5
|
+
workflow is expected to produce: five CPython ``abi3`` wheels (one per target
|
|
6
|
+
platform) and one sdist, every file carrying the same project version. Any
|
|
7
|
+
deviation -- a missing or extra artifact, a version disagreement, a non-abi3
|
|
8
|
+
wheel -- is a hard error, so a malformed upload set can never reach PyPI.
|
|
9
|
+
|
|
10
|
+
Because each target platform yields a uniquely named wheel, "exactly 5 wheels"
|
|
11
|
+
already implies five distinct platforms; there is no separate platform-tag
|
|
12
|
+
check (the exact tag strings shift with runner images and would be brittle).
|
|
13
|
+
|
|
14
|
+
Usage: ``python check_release_inventory.py <dist-dir>``
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
DIST_NAME = "pylibgrit"
|
|
23
|
+
# AIDEV-NOTE: EXPECTED_WHEELS must equal the number of build legs in the release
|
|
24
|
+
# workflow's `build` matrix (.github/workflows/release.yml). Bump both together when
|
|
25
|
+
# adding or removing a wheel target.
|
|
26
|
+
EXPECTED_WHEELS = 5
|
|
27
|
+
SDIST_SUFFIX = ".tar.gz"
|
|
28
|
+
WHEEL_SUFFIX = ".whl"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_inventory(dist_dir: Path) -> list[str]:
|
|
32
|
+
"""Return a list of human-readable problems with ``dist_dir`` (empty == OK)."""
|
|
33
|
+
errors: list[str] = []
|
|
34
|
+
versions: set[str] = set()
|
|
35
|
+
|
|
36
|
+
sdists = sorted(dist_dir.glob(f"*{SDIST_SUFFIX}"))
|
|
37
|
+
if len(sdists) != 1:
|
|
38
|
+
names = [p.name for p in sdists]
|
|
39
|
+
errors.append(
|
|
40
|
+
f"expected exactly 1 sdist (*{SDIST_SUFFIX}), found {len(sdists)}: {names}"
|
|
41
|
+
)
|
|
42
|
+
for sdist in sdists:
|
|
43
|
+
stem = sdist.name[: -len(SDIST_SUFFIX)]
|
|
44
|
+
parts = stem.split("-")
|
|
45
|
+
if len(parts) != 2 or parts[0] != DIST_NAME:
|
|
46
|
+
errors.append(f"unexpected sdist filename: {sdist.name}")
|
|
47
|
+
continue
|
|
48
|
+
versions.add(parts[1])
|
|
49
|
+
|
|
50
|
+
wheels = sorted(dist_dir.glob(f"*{WHEEL_SUFFIX}"))
|
|
51
|
+
if len(wheels) != EXPECTED_WHEELS:
|
|
52
|
+
names = [p.name for p in wheels]
|
|
53
|
+
errors.append(
|
|
54
|
+
f"expected exactly {EXPECTED_WHEELS} wheels, found {len(wheels)}: {names}"
|
|
55
|
+
)
|
|
56
|
+
for wheel in wheels:
|
|
57
|
+
stem = wheel.name[: -len(WHEEL_SUFFIX)]
|
|
58
|
+
parts = stem.split("-")
|
|
59
|
+
# Expected: name-version-pythontag-abitag-platformtag (no build tag).
|
|
60
|
+
if len(parts) != 5 or parts[0] != DIST_NAME:
|
|
61
|
+
errors.append(f"unexpected wheel filename: {wheel.name}")
|
|
62
|
+
continue
|
|
63
|
+
_, version, python_tag, abi_tag, _platform = parts
|
|
64
|
+
versions.add(version)
|
|
65
|
+
# AIDEV-NOTE: cp311 is the project's minimum Python (abi3 floor); update this
|
|
66
|
+
# tag here when bumping requires-python in pyproject.toml / Cargo.toml abi3-py*.
|
|
67
|
+
if python_tag != "cp311" or abi_tag != "abi3":
|
|
68
|
+
errors.append(f"wheel is not cp311-abi3: {wheel.name}")
|
|
69
|
+
|
|
70
|
+
if len(versions) > 1:
|
|
71
|
+
errors.append(f"artifacts disagree on version: {sorted(versions)}")
|
|
72
|
+
|
|
73
|
+
return errors
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: list[str]) -> int:
|
|
77
|
+
if len(argv) != 2:
|
|
78
|
+
print(f"usage: {argv[0]} <dist-dir>", file=sys.stderr)
|
|
79
|
+
return 2
|
|
80
|
+
dist_dir = Path(argv[1])
|
|
81
|
+
if not dist_dir.is_dir():
|
|
82
|
+
print(f"not a directory: {dist_dir}", file=sys.stderr)
|
|
83
|
+
return 2
|
|
84
|
+
errors = check_inventory(dist_dir)
|
|
85
|
+
if errors:
|
|
86
|
+
for err in errors:
|
|
87
|
+
print(f"::error::release inventory: {err}", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
print(f"release inventory OK: {EXPECTED_WHEELS} wheels + 1 sdist, single version")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
raise SystemExit(main(sys.argv))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for the release-inventory checker.
|
|
2
|
+
|
|
3
|
+
Release tooling — intentionally outside ``tests/`` so the binding suite (run by
|
|
4
|
+
CI's ``pytest tests/``) does not collect it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
13
|
+
|
|
14
|
+
from check_release_inventory import check_inventory # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _touch(directory: Path, name: str) -> None:
|
|
18
|
+
(directory / name).write_bytes(b"")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _good_dist(directory: Path, version: str = "0.1.0") -> None:
|
|
22
|
+
_touch(directory, f"pylibgrit-{version}.tar.gz")
|
|
23
|
+
_touch(
|
|
24
|
+
directory,
|
|
25
|
+
f"pylibgrit-{version}-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
|
26
|
+
)
|
|
27
|
+
_touch(
|
|
28
|
+
directory,
|
|
29
|
+
f"pylibgrit-{version}-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
|
|
30
|
+
)
|
|
31
|
+
_touch(directory, f"pylibgrit-{version}-cp311-abi3-musllinux_1_2_x86_64.whl")
|
|
32
|
+
_touch(directory, f"pylibgrit-{version}-cp311-abi3-musllinux_1_2_aarch64.whl")
|
|
33
|
+
_touch(directory, f"pylibgrit-{version}-cp311-abi3-macosx_11_0_arm64.whl")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_valid_inventory_passes(tmp_path: Path) -> None:
|
|
37
|
+
_good_dist(tmp_path)
|
|
38
|
+
assert check_inventory(tmp_path) == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_missing_sdist_fails(tmp_path: Path) -> None:
|
|
42
|
+
_good_dist(tmp_path)
|
|
43
|
+
(tmp_path / "pylibgrit-0.1.0.tar.gz").unlink()
|
|
44
|
+
errors = check_inventory(tmp_path)
|
|
45
|
+
assert any("sdist" in e for e in errors), errors
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_extra_sdist_fails(tmp_path: Path) -> None:
|
|
49
|
+
_good_dist(tmp_path)
|
|
50
|
+
_touch(tmp_path, "pylibgrit-0.1.0.zip.tar.gz")
|
|
51
|
+
errors = check_inventory(tmp_path)
|
|
52
|
+
assert any("sdist" in e for e in errors), errors
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_too_few_wheels_fails(tmp_path: Path) -> None:
|
|
56
|
+
_good_dist(tmp_path)
|
|
57
|
+
(tmp_path / "pylibgrit-0.1.0-cp311-abi3-macosx_11_0_arm64.whl").unlink()
|
|
58
|
+
errors = check_inventory(tmp_path)
|
|
59
|
+
assert any("5 wheels" in e for e in errors), errors
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_too_many_wheels_fails(tmp_path: Path) -> None:
|
|
63
|
+
_good_dist(tmp_path)
|
|
64
|
+
# A 6th valid abi3 wheel (distinct platform, same version) — only the count check
|
|
65
|
+
# should fire, proving the gate rejects over-count as well as under-count.
|
|
66
|
+
_touch(tmp_path, "pylibgrit-0.1.0-cp311-abi3-macosx_14_0_arm64.whl")
|
|
67
|
+
errors = check_inventory(tmp_path)
|
|
68
|
+
assert any("5 wheels" in e for e in errors), errors
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_non_abi3_wheel_fails(tmp_path: Path) -> None:
|
|
72
|
+
_good_dist(tmp_path)
|
|
73
|
+
(tmp_path / "pylibgrit-0.1.0-cp311-abi3-macosx_11_0_arm64.whl").unlink()
|
|
74
|
+
_touch(tmp_path, "pylibgrit-0.1.0-cp311-cp311-macosx_11_0_arm64.whl")
|
|
75
|
+
errors = check_inventory(tmp_path)
|
|
76
|
+
assert any("abi3" in e for e in errors), errors
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_version_mismatch_fails(tmp_path: Path) -> None:
|
|
80
|
+
_good_dist(tmp_path)
|
|
81
|
+
(tmp_path / "pylibgrit-0.1.0-cp311-abi3-macosx_11_0_arm64.whl").unlink()
|
|
82
|
+
_touch(tmp_path, "pylibgrit-0.2.0-cp311-abi3-macosx_11_0_arm64.whl")
|
|
83
|
+
errors = check_inventory(tmp_path)
|
|
84
|
+
assert any("version" in e for e in errors), errors
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# CI for pylibgrit (PyO3 + maturin abi3 bindings to grit-lib).
|
|
2
|
+
#
|
|
3
|
+
# Three jobs:
|
|
4
|
+
# lint — cargo fmt/clippy + ruff format/check (fast gate).
|
|
5
|
+
# test — build the extension and run pytest + mypy + stubtest on a
|
|
6
|
+
# Python matrix (3.11 = the abi3 floor, 3.13 = current).
|
|
7
|
+
# build-wheels — build release abi3 wheels per target, verify the abi3 tag,
|
|
8
|
+
# install the wheel into a clean venv and run the FULL suite +
|
|
9
|
+
# stubtest against the INSTALLED package, build+install+smoke the
|
|
10
|
+
# sdist (fail hard), and upload the artifacts.
|
|
11
|
+
#
|
|
12
|
+
# Conventions / choices (see also rust-toolchain.toml, pyproject.toml):
|
|
13
|
+
# * Rust toolchain is pinned to 1.94.1 to match rust-toolchain.toml. We pin
|
|
14
|
+
# dtolnay/rust-toolchain@1.94.1 directly: the `toolchain:` input is only
|
|
15
|
+
# honored by the @master ref, NOT by @stable, so the version goes in the ref.
|
|
16
|
+
# * All cargo/maturin builds use --locked (committed Cargo.lock) for
|
|
17
|
+
# reproducibility, matching the local `--locked` workflow.
|
|
18
|
+
# * setup-uv is pinned to a full version (@v8.2.0): astral-sh/setup-uv
|
|
19
|
+
# publishes only full vX.Y.Z tags, NOT a floating major tag, so `@v8` does
|
|
20
|
+
# not resolve. Bump this pin when updating uv. Dev deps live in
|
|
21
|
+
# [dependency-groups] (PEP 735), installed via `uv sync --group dev`.
|
|
22
|
+
# * build-wheels also installs setup-uv: the sdist step shells out to
|
|
23
|
+
# `uvx maturin sdist`, so uv must be on PATH (harmless on legs that skip it).
|
|
24
|
+
# * macOS (macos-14 arm64) is best-effort: grit-lib is
|
|
25
|
+
# Unix-oriented and the deps are pure-Rust, so it is expected to build, but
|
|
26
|
+
# fail-fast is disabled so a macOS hiccup does not mask Linux results.
|
|
27
|
+
# * Wheel coverage mirrors most of release.yml's grid: linux glibc x86_64/aarch64,
|
|
28
|
+
# linux musl x86_64 (Alpine import-smoke only), macOS arm64. The emulated
|
|
29
|
+
# musl-aarch64 leg is release-only (kept off per-PR CI to avoid a second slow QEMU
|
|
30
|
+
# build). macOS Intel (x86_64) is not built anywhere — see release.yml.
|
|
31
|
+
# * aarch64 Linux wheels cross-build inside maturin-action's manylinux
|
|
32
|
+
# container (manylinux: auto) via emulation — no extra QEMU step is needed
|
|
33
|
+
# for the build, but that leg does not run the installed-wheel test suite or
|
|
34
|
+
# the sdist compile (both would require emulating the foreign interpreter /
|
|
35
|
+
# toolchain); the native legs (linux x86_64, macОS) cover that.
|
|
36
|
+
# * The native legs install the built wheel into a CLEAN venv and run the real
|
|
37
|
+
# pytest suite + stubtest against the INSTALLED package — exercising the
|
|
38
|
+
# shipped wheel, not the source tree. The source `python/pylibgrit` is kept off
|
|
39
|
+
# sys.path by running from a temp dir that contains only tests/ + conftest +
|
|
40
|
+
# the pyproject pytest config (pyproject's pythonpath=["."] then points at a
|
|
41
|
+
# dir with no source pylibgrit, so the installed wheel is the only importable
|
|
42
|
+
# copy). These steps fail the job on any test failure.
|
|
43
|
+
# * The sdist is built, installed into a clean venv (which compiles from source
|
|
44
|
+
# via the pinned Rust toolchain), and import-smoked — no `|| true`, so a
|
|
45
|
+
# broken sdist fails the job.
|
|
46
|
+
|
|
47
|
+
name: CI
|
|
48
|
+
|
|
49
|
+
on:
|
|
50
|
+
push:
|
|
51
|
+
branches: [main, master]
|
|
52
|
+
pull_request:
|
|
53
|
+
|
|
54
|
+
jobs:
|
|
55
|
+
lint:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v6
|
|
59
|
+
- uses: dtolnay/rust-toolchain@1.94.1
|
|
60
|
+
with:
|
|
61
|
+
components: rustfmt, clippy
|
|
62
|
+
- uses: Swatinem/rust-cache@v2
|
|
63
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
64
|
+
- run: uv sync --group dev
|
|
65
|
+
- run: cargo fmt --check
|
|
66
|
+
- run: cargo clippy --all-targets --locked -- -D warnings
|
|
67
|
+
- run: uv run ruff format --check .
|
|
68
|
+
- run: uv run ruff check .
|
|
69
|
+
|
|
70
|
+
test:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
strategy:
|
|
73
|
+
fail-fast: false
|
|
74
|
+
matrix:
|
|
75
|
+
python-version: ["3.11", "3.13"]
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v6
|
|
78
|
+
- uses: dtolnay/rust-toolchain@1.94.1
|
|
79
|
+
with:
|
|
80
|
+
components: rustfmt, clippy
|
|
81
|
+
- uses: Swatinem/rust-cache@v2
|
|
82
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
83
|
+
with:
|
|
84
|
+
python-version: ${{ matrix.python-version }}
|
|
85
|
+
- run: uv sync --group dev
|
|
86
|
+
- run: uv run maturin develop --uv --locked
|
|
87
|
+
- run: uv run pytest tests/ -v
|
|
88
|
+
- run: uv run mypy python tests
|
|
89
|
+
- run: uv run python -m mypy.stubtest pylibgrit
|
|
90
|
+
|
|
91
|
+
build-wheels:
|
|
92
|
+
name: build-wheels (${{ matrix.os }} ${{ matrix.target }} ${{ matrix.manylinux }})
|
|
93
|
+
runs-on: ${{ matrix.os }}
|
|
94
|
+
strategy:
|
|
95
|
+
fail-fast: false
|
|
96
|
+
matrix:
|
|
97
|
+
include:
|
|
98
|
+
- os: ubuntu-latest
|
|
99
|
+
target: x86_64
|
|
100
|
+
manylinux: auto
|
|
101
|
+
smoke: true
|
|
102
|
+
- os: ubuntu-latest
|
|
103
|
+
target: aarch64
|
|
104
|
+
manylinux: auto
|
|
105
|
+
smoke: false # cross-built under emulation; foreign interpreter not run here
|
|
106
|
+
- os: ubuntu-latest
|
|
107
|
+
target: x86_64
|
|
108
|
+
manylinux: musllinux_1_2
|
|
109
|
+
smoke: false
|
|
110
|
+
musl: true # import-smoke only in an Alpine container (native docker)
|
|
111
|
+
- os: macos-14
|
|
112
|
+
target: aarch64
|
|
113
|
+
manylinux: auto
|
|
114
|
+
smoke: true
|
|
115
|
+
steps:
|
|
116
|
+
- uses: actions/checkout@v6
|
|
117
|
+
- uses: dtolnay/rust-toolchain@1.94.1
|
|
118
|
+
with:
|
|
119
|
+
# Pre-install the rust-toolchain.toml components so a later host `cargo`/
|
|
120
|
+
# `maturin` call can't trigger a flaky lazy rustup re-add of clippy.
|
|
121
|
+
components: rustfmt, clippy
|
|
122
|
+
# The sdist step (x86_64 only) calls `uvx maturin sdist`; uv must be on
|
|
123
|
+
# PATH. Harmless on the legs that skip that step.
|
|
124
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
125
|
+
- uses: PyO3/maturin-action@v1
|
|
126
|
+
with:
|
|
127
|
+
target: ${{ matrix.target }}
|
|
128
|
+
args: --release --locked --out dist
|
|
129
|
+
# glibc legs use `auto` (newer glibc, fine for dev — release.yml pins 2014);
|
|
130
|
+
# the musl leg builds musllinux_1_2; macOS ignores this (Linux-only).
|
|
131
|
+
manylinux: ${{ matrix.manylinux }}
|
|
132
|
+
- name: list wheels
|
|
133
|
+
run: ls -la dist
|
|
134
|
+
- name: verify abi3 tag
|
|
135
|
+
shell: bash
|
|
136
|
+
run: ls dist/*.whl | grep -q abi3 && echo "abi3 OK"
|
|
137
|
+
- name: test the INSTALLED wheel (clean venv, full suite + stubtest)
|
|
138
|
+
if: matrix.smoke
|
|
139
|
+
shell: bash
|
|
140
|
+
run: |
|
|
141
|
+
set -euxo pipefail
|
|
142
|
+
# Clean venv: only the built wheel + test deps. git is preinstalled on
|
|
143
|
+
# GitHub runners (the suite shells out to it via tests/conftest.py).
|
|
144
|
+
python3 -m venv /tmp/wheeltest
|
|
145
|
+
/tmp/wheeltest/bin/python -m pip install --upgrade pip
|
|
146
|
+
/tmp/wheeltest/bin/pip install dist/*.whl pytest mypy
|
|
147
|
+
# Stage ONLY tests/ + the pyproject pytest config in an empty dir, so the
|
|
148
|
+
# source `python/pylibgrit` is NOT importable. pyproject's pythonpath=["."]
|
|
149
|
+
# then resolves to this dir (no source pylibgrit), making the installed wheel
|
|
150
|
+
# the only `import pylibgrit`.
|
|
151
|
+
workdir="$(mktemp -d)"
|
|
152
|
+
cp -r tests "$workdir/"
|
|
153
|
+
cp pyproject.toml "$workdir/"
|
|
154
|
+
cd "$workdir"
|
|
155
|
+
# Prove the import comes from the installed wheel, not a source tree.
|
|
156
|
+
/tmp/wheeltest/bin/python -c "import pylibgrit, os; p = os.path.dirname(pylibgrit.__file__); print('pylibgrit from:', p); assert 'site-packages' in p, p"
|
|
157
|
+
/tmp/wheeltest/bin/python -m pytest tests/ -q
|
|
158
|
+
/tmp/wheeltest/bin/python -m mypy.stubtest pylibgrit
|
|
159
|
+
- name: import-smoke the musl wheel (Alpine container)
|
|
160
|
+
if: matrix.musl
|
|
161
|
+
shell: bash
|
|
162
|
+
run: |
|
|
163
|
+
set -euxo pipefail
|
|
164
|
+
# The full pytest+stubtest suite runs on glibc x86_64 + both macOS legs; this
|
|
165
|
+
# leg just proves the wheel loads and links against musl libc. Native docker
|
|
166
|
+
# (x86_64 host) — no QEMU.
|
|
167
|
+
shopt -s nullglob
|
|
168
|
+
wheel=(dist/*-cp311-abi3-*.whl)
|
|
169
|
+
if [[ ${#wheel[@]} -ne 1 ]]; then
|
|
170
|
+
echo "::error::expected exactly one *-cp311-abi3-*.whl, found ${#wheel[@]}: ${wheel[*]:-<none>}"
|
|
171
|
+
ls -la dist || true
|
|
172
|
+
exit 1
|
|
173
|
+
fi
|
|
174
|
+
docker run --rm -v "$PWD/dist:/dist:ro" python:3.11-alpine \
|
|
175
|
+
sh -c "pip install /dist/${wheel[0]##*/} && python -c 'import pylibgrit; pylibgrit.Repository'"
|
|
176
|
+
- name: build + install + smoke the sdist (fail hard)
|
|
177
|
+
# glibc x86_64 leg only — the musl x86_64 leg also matches os+target.
|
|
178
|
+
if: matrix.os == 'ubuntu-latest' && matrix.target == 'x86_64' && matrix.manylinux == 'auto'
|
|
179
|
+
shell: bash
|
|
180
|
+
run: |
|
|
181
|
+
set -euxo pipefail
|
|
182
|
+
# Build the sdist into dist/ (no `|| true` — a failure must fail the job).
|
|
183
|
+
uvx maturin sdist --out dist
|
|
184
|
+
# Install the sdist into a clean venv: this COMPILES from source, using
|
|
185
|
+
# the Rust toolchain set up for this job.
|
|
186
|
+
python3 -m venv /tmp/sdisttest
|
|
187
|
+
/tmp/sdisttest/bin/python -m pip install --upgrade pip
|
|
188
|
+
/tmp/sdisttest/bin/pip install dist/pylibgrit-*.tar.gz
|
|
189
|
+
/tmp/sdisttest/bin/python -c "import pylibgrit; pylibgrit.Repository"
|
|
190
|
+
- uses: actions/upload-artifact@v7
|
|
191
|
+
with:
|
|
192
|
+
name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }}
|
|
193
|
+
path: dist
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Trusted-publishing release pipeline for pylibgrit (PyO3 + maturin abi3 bindings).
|
|
2
|
+
#
|
|
3
|
+
# Separate from ci.yml so the OIDC `id-token: write` permission never lives in the
|
|
4
|
+
# everyday push/PR workflow. Flow:
|
|
5
|
+
# version-guard — (release only) tag must be vX.Y.Z and equal the crate version.
|
|
6
|
+
# build — 5 targets (linux glibc x86_64/aarch64, linux musl x86_64/aarch64,
|
|
7
|
+
# macOS arm64): build the abi3 wheel, assert exactly one cp311-abi3
|
|
8
|
+
# wheel, import-smoke it (native on glibc-x86_64 + macOS; container on
|
|
9
|
+
# the glibc-aarch64 + musl-x86_64/aarch64 legs, arm64 via QEMU),
|
|
10
|
+
# upload. (No macOS Intel: GitHub's macos-13 runners are deprecated.)
|
|
11
|
+
# sdist — build the sdist with the locked maturin, source-compile +
|
|
12
|
+
# import-smoke it, upload.
|
|
13
|
+
# publish-pypi — (release) provenance + inventory gate, then OIDC publish to PyPI.
|
|
14
|
+
# publish-testpypi (workflow_dispatch) — same gate, OIDC publish to TestPyPI.
|
|
15
|
+
#
|
|
16
|
+
# Conventions (match ci.yml): pinned Rust 1.94.1, --locked builds, setup-uv@v8.2.0
|
|
17
|
+
# (no floating major exists), checkout@v6 / upload-artifact@v7 / download-artifact@v7
|
|
18
|
+
# (node24, v4+ backend). Only the publish action is commit-SHA-pinned (it handles
|
|
19
|
+
# the OIDC credential / upload); other actions follow ci.yml's floating-major style.
|
|
20
|
+
|
|
21
|
+
name: Release
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
release:
|
|
25
|
+
types: [published] # -> real PyPI
|
|
26
|
+
workflow_dispatch: {} # -> TestPyPI dry-run
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
concurrency:
|
|
32
|
+
group: release-${{ github.event.release.tag_name || github.ref }}
|
|
33
|
+
cancel-in-progress: false # never cancel a partially-completed publish
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
version-guard:
|
|
37
|
+
name: version guard
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
if: github.event_name == 'release'
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v6
|
|
42
|
+
with:
|
|
43
|
+
persist-credentials: false
|
|
44
|
+
- uses: dtolnay/rust-toolchain@1.94.1
|
|
45
|
+
with:
|
|
46
|
+
# Pre-install rust-toolchain.toml's components: the cargo-metadata call
|
|
47
|
+
# below would otherwise trigger a flaky lazy rustup re-add of clippy
|
|
48
|
+
# (conflict on bin/cargo-clippy) and block the release.
|
|
49
|
+
components: rustfmt, clippy
|
|
50
|
+
- name: Assert tag matches crate version
|
|
51
|
+
shell: bash
|
|
52
|
+
run: |
|
|
53
|
+
set -euo pipefail
|
|
54
|
+
tag="${{ github.event.release.tag_name }}"
|
|
55
|
+
# v1 policy: final releases only, vX.Y.Z (no PEP 440 prereleases — see spec).
|
|
56
|
+
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
57
|
+
echo "::error::Release tag '$tag' is not of the form vX.Y.Z (final releases only)."
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
tag_version="${tag#v}"
|
|
61
|
+
crate_version="$(cargo metadata --locked --format-version=1 \
|
|
62
|
+
| python3 -c "import json,sys; print([p['version'] for p in json.load(sys.stdin)['packages'] if p['name']=='pylibgrit'][0])")"
|
|
63
|
+
echo "tag=$tag_version crate=$crate_version"
|
|
64
|
+
if [[ "$tag_version" != "$crate_version" ]]; then
|
|
65
|
+
echo "::error::Tag version ($tag_version) != crate version ($crate_version). Bump Cargo.toml AND Cargo.lock."
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
echo "version-guard OK: $tag_version"
|
|
69
|
+
|
|
70
|
+
build:
|
|
71
|
+
name: build (${{ matrix.os }} ${{ matrix.target }} ${{ matrix.manylinux }})
|
|
72
|
+
needs: [version-guard]
|
|
73
|
+
# version-guard is skipped on workflow_dispatch (its own `if`); allow build to
|
|
74
|
+
# run when the guard either succeeded (release) or was skipped (dispatch), but
|
|
75
|
+
# not when it actually failed.
|
|
76
|
+
if: always() && (needs.version-guard.result == 'success' || needs.version-guard.result == 'skipped')
|
|
77
|
+
runs-on: ${{ matrix.os }}
|
|
78
|
+
strategy:
|
|
79
|
+
fail-fast: false
|
|
80
|
+
matrix:
|
|
81
|
+
include:
|
|
82
|
+
# Linux glibc (manylinux_2_17 via "2014") — native + emulated-arm64 smoke.
|
|
83
|
+
- os: ubuntu-latest
|
|
84
|
+
target: x86_64
|
|
85
|
+
manylinux: "2014"
|
|
86
|
+
smoke: native
|
|
87
|
+
- os: ubuntu-latest
|
|
88
|
+
target: aarch64
|
|
89
|
+
manylinux: "2014"
|
|
90
|
+
smoke: container
|
|
91
|
+
image: python:3.11-slim
|
|
92
|
+
platform: linux/arm64
|
|
93
|
+
# Linux musl (musllinux_1_2) — Alpine container smoke (arm64 via QEMU).
|
|
94
|
+
- os: ubuntu-latest
|
|
95
|
+
target: x86_64
|
|
96
|
+
manylinux: musllinux_1_2
|
|
97
|
+
smoke: container
|
|
98
|
+
image: python:3.11-alpine
|
|
99
|
+
platform: linux/amd64
|
|
100
|
+
- os: ubuntu-latest
|
|
101
|
+
target: aarch64
|
|
102
|
+
manylinux: musllinux_1_2
|
|
103
|
+
smoke: container
|
|
104
|
+
image: python:3.11-alpine
|
|
105
|
+
platform: linux/arm64
|
|
106
|
+
# macOS — native smoke. `manylinux: auto` is a no-op here (Linux-only); it is
|
|
107
|
+
# set only so every leg defines it (unique artifact names; no empty input).
|
|
108
|
+
# (Intel x86_64 is NOT built: GitHub's macos-13 Intel runners are deprecated
|
|
109
|
+
# and queue unreliably — Intel Macs install from the sdist instead.)
|
|
110
|
+
- os: macos-14
|
|
111
|
+
target: aarch64
|
|
112
|
+
manylinux: auto
|
|
113
|
+
smoke: native
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v6
|
|
116
|
+
with:
|
|
117
|
+
persist-credentials: false
|
|
118
|
+
- uses: dtolnay/rust-toolchain@1.94.1
|
|
119
|
+
with:
|
|
120
|
+
components: rustfmt, clippy
|
|
121
|
+
- uses: PyO3/maturin-action@v1
|
|
122
|
+
with:
|
|
123
|
+
target: ${{ matrix.target }}
|
|
124
|
+
args: --release --locked --out dist
|
|
125
|
+
# glibc legs pin manylinux_2_17 ("2014") for the broadest-compatible tag on
|
|
126
|
+
# RELEASED wheels; musl legs build musllinux_1_2; macOS passes `auto` (no-op).
|
|
127
|
+
# ci.yml uses `auto` for glibc (newer glibc, fine for dev — intentional
|
|
128
|
+
# divergence). Ignored on macOS (manylinux is Linux-only).
|
|
129
|
+
manylinux: ${{ matrix.manylinux }}
|
|
130
|
+
- name: Verify exactly one cp311-abi3 wheel
|
|
131
|
+
shell: bash
|
|
132
|
+
run: |
|
|
133
|
+
set -euo pipefail
|
|
134
|
+
shopt -s nullglob
|
|
135
|
+
wheels=(dist/*-cp311-abi3-*.whl)
|
|
136
|
+
if [[ ${#wheels[@]} -ne 1 ]]; then
|
|
137
|
+
echo "::error::expected exactly one *-cp311-abi3-*.whl, found ${#wheels[@]}: ${wheels[*]:-<none>}"
|
|
138
|
+
ls -la dist || true
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
echo "abi3 wheel OK: ${wheels[0]}"
|
|
142
|
+
- name: Set up Python 3.11 for native smoke
|
|
143
|
+
if: matrix.smoke == 'native'
|
|
144
|
+
uses: actions/setup-python@v5
|
|
145
|
+
with:
|
|
146
|
+
python-version: "3.11"
|
|
147
|
+
- name: Import smoke (native, Python 3.11)
|
|
148
|
+
if: matrix.smoke == 'native'
|
|
149
|
+
shell: bash
|
|
150
|
+
run: |
|
|
151
|
+
set -euxo pipefail
|
|
152
|
+
python -m venv /tmp/smoke
|
|
153
|
+
/tmp/smoke/bin/python -m pip install --upgrade pip
|
|
154
|
+
/tmp/smoke/bin/pip install dist/*-cp311-abi3-*.whl
|
|
155
|
+
/tmp/smoke/bin/python -c "import pylibgrit; pylibgrit.Repository"
|
|
156
|
+
/tmp/smoke/bin/python -c "import pylibgrit, os; p=os.path.dirname(pylibgrit.__file__); assert 'site-packages' in p, p; print('imported from', p)"
|
|
157
|
+
- name: Set up QEMU for emulated arm64 smoke
|
|
158
|
+
if: matrix.smoke == 'container' && matrix.platform == 'linux/arm64'
|
|
159
|
+
uses: docker/setup-qemu-action@v3
|
|
160
|
+
- name: Import smoke (container)
|
|
161
|
+
if: matrix.smoke == 'container'
|
|
162
|
+
shell: bash
|
|
163
|
+
run: |
|
|
164
|
+
set -euxo pipefail
|
|
165
|
+
# One step for all container legs: glibc-arm64 (slim) + musl x86_64/arm64
|
|
166
|
+
# (alpine). `sh -c` works in both slim (dash) and alpine (busybox sh).
|
|
167
|
+
shopt -s nullglob
|
|
168
|
+
wheel=(dist/*-cp311-abi3-*.whl)
|
|
169
|
+
docker run --rm --platform ${{ matrix.platform }} \
|
|
170
|
+
-v "$PWD/dist:/dist:ro" ${{ matrix.image }} \
|
|
171
|
+
sh -c "pip install /dist/${wheel[0]##*/} && python -c 'import pylibgrit; pylibgrit.Repository'"
|
|
172
|
+
- uses: actions/upload-artifact@v7
|
|
173
|
+
with:
|
|
174
|
+
name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }}
|
|
175
|
+
path: dist
|
|
176
|
+
if-no-files-found: error
|
|
177
|
+
|
|
178
|
+
sdist:
|
|
179
|
+
name: sdist
|
|
180
|
+
needs: [version-guard]
|
|
181
|
+
if: always() && (needs.version-guard.result == 'success' || needs.version-guard.result == 'skipped')
|
|
182
|
+
runs-on: ubuntu-latest
|
|
183
|
+
steps:
|
|
184
|
+
- uses: actions/checkout@v6
|
|
185
|
+
with:
|
|
186
|
+
persist-credentials: false
|
|
187
|
+
- uses: dtolnay/rust-toolchain@1.94.1
|
|
188
|
+
with:
|
|
189
|
+
components: rustfmt, clippy
|
|
190
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
191
|
+
- run: uv sync --group dev
|
|
192
|
+
# `uv run maturin` uses the maturin pinned in uv.lock (reproducible release
|
|
193
|
+
# builds), NOT a floating `uvx maturin` as in ci.yml's smoke step — deliberate.
|
|
194
|
+
- name: Build sdist (locked maturin)
|
|
195
|
+
run: uv run maturin sdist --out dist
|
|
196
|
+
- name: Source-compile + import-smoke the sdist
|
|
197
|
+
shell: bash
|
|
198
|
+
run: |
|
|
199
|
+
set -euxo pipefail
|
|
200
|
+
python3 -m venv /tmp/sdisttest
|
|
201
|
+
/tmp/sdisttest/bin/python -m pip install --upgrade pip
|
|
202
|
+
/tmp/sdisttest/bin/pip install dist/pylibgrit-*.tar.gz
|
|
203
|
+
/tmp/sdisttest/bin/python -c "import pylibgrit; pylibgrit.Repository"
|
|
204
|
+
- uses: actions/upload-artifact@v7
|
|
205
|
+
with:
|
|
206
|
+
name: sdist
|
|
207
|
+
path: dist
|
|
208
|
+
if-no-files-found: error
|
|
209
|
+
|
|
210
|
+
publish-pypi:
|
|
211
|
+
name: publish to PyPI
|
|
212
|
+
needs: [build, sdist]
|
|
213
|
+
if: github.event_name == 'release'
|
|
214
|
+
runs-on: ubuntu-latest
|
|
215
|
+
environment: pypi
|
|
216
|
+
permissions:
|
|
217
|
+
id-token: write
|
|
218
|
+
contents: read
|
|
219
|
+
steps:
|
|
220
|
+
- uses: actions/checkout@v6
|
|
221
|
+
with:
|
|
222
|
+
fetch-depth: 0
|
|
223
|
+
persist-credentials: false
|
|
224
|
+
- name: Provenance — built commit must be an ancestor of origin/main
|
|
225
|
+
shell: bash
|
|
226
|
+
run: |
|
|
227
|
+
set -euo pipefail
|
|
228
|
+
# This repo's default branch is `main`; update both publish jobs if renamed.
|
|
229
|
+
git fetch --no-tags origin main
|
|
230
|
+
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
231
|
+
echo "::error::commit $GITHUB_SHA is not an ancestor of origin/main; refusing to publish."
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
234
|
+
echo "provenance OK: $GITHUB_SHA is on origin/main"
|
|
235
|
+
- uses: actions/download-artifact@v7
|
|
236
|
+
with:
|
|
237
|
+
pattern: "*"
|
|
238
|
+
merge-multiple: true
|
|
239
|
+
path: dist
|
|
240
|
+
- name: Inventory — exactly 5 wheels + 1 sdist, single version
|
|
241
|
+
shell: bash
|
|
242
|
+
run: |
|
|
243
|
+
set -euo pipefail
|
|
244
|
+
ls -la dist
|
|
245
|
+
python3 .github/scripts/check_release_inventory.py dist
|
|
246
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
247
|
+
with:
|
|
248
|
+
packages-dir: dist
|
|
249
|
+
|
|
250
|
+
publish-testpypi:
|
|
251
|
+
name: publish to TestPyPI
|
|
252
|
+
needs: [build, sdist]
|
|
253
|
+
if: github.event_name == 'workflow_dispatch'
|
|
254
|
+
runs-on: ubuntu-latest
|
|
255
|
+
environment: testpypi
|
|
256
|
+
permissions:
|
|
257
|
+
id-token: write
|
|
258
|
+
contents: read
|
|
259
|
+
steps:
|
|
260
|
+
- uses: actions/checkout@v6
|
|
261
|
+
with:
|
|
262
|
+
fetch-depth: 0
|
|
263
|
+
persist-credentials: false
|
|
264
|
+
- name: Provenance — built commit must be an ancestor of origin/main
|
|
265
|
+
shell: bash
|
|
266
|
+
run: |
|
|
267
|
+
set -euo pipefail
|
|
268
|
+
# This repo's default branch is `main`; update both publish jobs if renamed.
|
|
269
|
+
git fetch --no-tags origin main
|
|
270
|
+
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
271
|
+
echo "::error::commit $GITHUB_SHA is not an ancestor of origin/main; refusing to publish."
|
|
272
|
+
exit 1
|
|
273
|
+
fi
|
|
274
|
+
echo "provenance OK: $GITHUB_SHA is on origin/main"
|
|
275
|
+
- uses: actions/download-artifact@v7
|
|
276
|
+
with:
|
|
277
|
+
pattern: "*"
|
|
278
|
+
merge-multiple: true
|
|
279
|
+
path: dist
|
|
280
|
+
- name: Inventory — exactly 5 wheels + 1 sdist, single version
|
|
281
|
+
shell: bash
|
|
282
|
+
run: |
|
|
283
|
+
set -euo pipefail
|
|
284
|
+
ls -la dist
|
|
285
|
+
python3 .github/scripts/check_release_inventory.py dist
|
|
286
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
287
|
+
with:
|
|
288
|
+
repository-url: https://test.pypi.org/legacy/
|
|
289
|
+
packages-dir: dist
|