pygritlib 0.5.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.
Files changed (104) hide show
  1. pygritlib-0.5.0/.github/scripts/check_release_inventory.py +94 -0
  2. pygritlib-0.5.0/.github/scripts/test_check_release_inventory.py +84 -0
  3. pygritlib-0.5.0/.github/workflows/ci.yml +201 -0
  4. pygritlib-0.5.0/.github/workflows/release.yml +297 -0
  5. pygritlib-0.5.0/.gitignore +11 -0
  6. pygritlib-0.5.0/CHANGELOG.md +148 -0
  7. pygritlib-0.5.0/Cargo.lock +1541 -0
  8. pygritlib-0.5.0/Cargo.toml +21 -0
  9. pygritlib-0.5.0/LICENSE +21 -0
  10. pygritlib-0.5.0/PKG-INFO +608 -0
  11. pygritlib-0.5.0/README.md +590 -0
  12. pygritlib-0.5.0/docs/superpowers/api-matrix.md +334 -0
  13. pygritlib-0.5.0/docs/superpowers/plans/2026-06-13-pygrit-bindings.md +2023 -0
  14. pygritlib-0.5.0/docs/superpowers/plans/2026-06-14-pygrit-6wheel-coverage.md +564 -0
  15. pygritlib-0.5.0/docs/superpowers/plans/2026-06-14-pygrit-release-readiness.md +813 -0
  16. pygritlib-0.5.0/docs/superpowers/plans/2026-06-15-pygritlib-write-core-phase-a.md +2426 -0
  17. pygritlib-0.5.0/docs/superpowers/plans/2026-06-16-pygritlib-worktree-merge.md +2200 -0
  18. pygritlib-0.5.0/docs/superpowers/plans/2026-06-17-pygritlib-networking-clone.md +1947 -0
  19. pygritlib-0.5.0/docs/superpowers/plans/2026-06-17-pygritlib-push.md +1299 -0
  20. pygritlib-0.5.0/docs/superpowers/plans/2026-06-17-pygritlib-ssh-transport.md +899 -0
  21. pygritlib-0.5.0/docs/superpowers/specs/2026-06-13-pygrit-bindings-design.md +299 -0
  22. pygritlib-0.5.0/docs/superpowers/specs/2026-06-14-pygrit-6wheel-coverage-design.md +173 -0
  23. pygritlib-0.5.0/docs/superpowers/specs/2026-06-14-pygrit-release-readiness-design.md +277 -0
  24. pygritlib-0.5.0/docs/superpowers/specs/2026-06-14-pygritlib-write-core-design.md +307 -0
  25. pygritlib-0.5.0/docs/superpowers/specs/2026-06-15-grit-lib-write-surface-spike.md +112 -0
  26. pygritlib-0.5.0/docs/superpowers/specs/2026-06-16-pygritlib-worktree-merge-design.md +332 -0
  27. pygritlib-0.5.0/docs/superpowers/specs/2026-06-17-pygritlib-networking-clone-design.md +302 -0
  28. pygritlib-0.5.0/docs/superpowers/specs/2026-06-17-pygritlib-push-design.md +216 -0
  29. pygritlib-0.5.0/docs/superpowers/specs/2026-06-17-pygritlib-ssh-transport-design.md +259 -0
  30. pygritlib-0.5.0/pyproject.toml +43 -0
  31. pygritlib-0.5.0/python/pygritlib/__init__.py +90 -0
  32. pygritlib-0.5.0/python/pygritlib/__init__.pyi +565 -0
  33. pygritlib-0.5.0/python/pygritlib/py.typed +0 -0
  34. pygritlib-0.5.0/rust-toolchain.toml +3 -0
  35. pygritlib-0.5.0/src/checkout.rs +146 -0
  36. pygritlib-0.5.0/src/config.rs +57 -0
  37. pygritlib-0.5.0/src/diff.rs +246 -0
  38. pygritlib-0.5.0/src/error.rs +157 -0
  39. pygritlib-0.5.0/src/index.rs +345 -0
  40. pygritlib-0.5.0/src/lib.rs +71 -0
  41. pygritlib-0.5.0/src/merge.rs +126 -0
  42. pygritlib-0.5.0/src/net_credentials.rs +131 -0
  43. pygritlib-0.5.0/src/net_progress.rs +47 -0
  44. pygritlib-0.5.0/src/net_transport.rs +158 -0
  45. pygritlib-0.5.0/src/objects.rs +671 -0
  46. pygritlib-0.5.0/src/odb.rs +64 -0
  47. pygritlib-0.5.0/src/push.rs +397 -0
  48. pygritlib-0.5.0/src/refs.rs +391 -0
  49. pygritlib-0.5.0/src/remote.rs +478 -0
  50. pygritlib-0.5.0/src/repository.rs +1339 -0
  51. pygritlib-0.5.0/src/revwalk.rs +62 -0
  52. pygritlib-0.5.0/tests/__init__.py +0 -0
  53. pygritlib-0.5.0/tests/conftest.py +433 -0
  54. pygritlib-0.5.0/tests/githttp.py +103 -0
  55. pygritlib-0.5.0/tests/gitlib.py +54 -0
  56. pygritlib-0.5.0/tests/test_atomic_cas.py +157 -0
  57. pygritlib-0.5.0/tests/test_checkout.py +146 -0
  58. pygritlib-0.5.0/tests/test_clone.py +80 -0
  59. pygritlib-0.5.0/tests/test_commit_index.py +168 -0
  60. pygritlib-0.5.0/tests/test_concurrency.py +38 -0
  61. pygritlib-0.5.0/tests/test_config.py +32 -0
  62. pygritlib-0.5.0/tests/test_create_commit.py +146 -0
  63. pygritlib-0.5.0/tests/test_create_tag.py +96 -0
  64. pygritlib-0.5.0/tests/test_diff.py +305 -0
  65. pygritlib-0.5.0/tests/test_errors.py +8 -0
  66. pygritlib-0.5.0/tests/test_fetch.py +53 -0
  67. pygritlib-0.5.0/tests/test_ffi_lifetime.py +81 -0
  68. pygritlib-0.5.0/tests/test_git_daemon_fixture.py +12 -0
  69. pygritlib-0.5.0/tests/test_http_auth.py +45 -0
  70. pygritlib-0.5.0/tests/test_http_clone.py +23 -0
  71. pygritlib-0.5.0/tests/test_index_entry.py +39 -0
  72. pygritlib-0.5.0/tests/test_index_write.py +170 -0
  73. pygritlib-0.5.0/tests/test_init.py +50 -0
  74. pygritlib-0.5.0/tests/test_ls_remote.py +52 -0
  75. pygritlib-0.5.0/tests/test_merge.py +324 -0
  76. pygritlib-0.5.0/tests/test_net_errors.py +17 -0
  77. pygritlib-0.5.0/tests/test_objectid.py +35 -0
  78. pygritlib-0.5.0/tests/test_objectkind.py +21 -0
  79. pygritlib-0.5.0/tests/test_objects.py +446 -0
  80. pygritlib-0.5.0/tests/test_odb.py +41 -0
  81. pygritlib-0.5.0/tests/test_odb_write.py +60 -0
  82. pygritlib-0.5.0/tests/test_push.py +83 -0
  83. pygritlib-0.5.0/tests/test_push_fixture.py +35 -0
  84. pygritlib-0.5.0/tests/test_push_followups.py +48 -0
  85. pygritlib-0.5.0/tests/test_push_http.py +32 -0
  86. pygritlib-0.5.0/tests/test_push_http_auth.py +52 -0
  87. pygritlib-0.5.0/tests/test_push_progress.py +91 -0
  88. pygritlib-0.5.0/tests/test_push_semantics.py +143 -0
  89. pygritlib-0.5.0/tests/test_ref_write.py +169 -0
  90. pygritlib-0.5.0/tests/test_reflog.py +136 -0
  91. pygritlib-0.5.0/tests/test_refs.py +53 -0
  92. pygritlib-0.5.0/tests/test_repository.py +76 -0
  93. pygritlib-0.5.0/tests/test_resolve.py +52 -0
  94. pygritlib-0.5.0/tests/test_revwalk.py +169 -0
  95. pygritlib-0.5.0/tests/test_signature.py +17 -0
  96. pygritlib-0.5.0/tests/test_smoke.py +6 -0
  97. pygritlib-0.5.0/tests/test_ssh.py +166 -0
  98. pygritlib-0.5.0/tests/test_tag_ref.py +92 -0
  99. pygritlib-0.5.0/tests/test_worktree_merge_smoke.py +77 -0
  100. pygritlib-0.5.0/tests/test_write_concurrency.py +39 -0
  101. pygritlib-0.5.0/tests/test_write_errors.py +32 -0
  102. pygritlib-0.5.0/tests/test_write_smoke.py +46 -0
  103. pygritlib-0.5.0/tests/test_write_validation.py +166 -0
  104. pygritlib-0.5.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 = "pygritlib"
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"pygritlib-{version}.tar.gz")
23
+ _touch(
24
+ directory,
25
+ f"pygritlib-{version}-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
26
+ )
27
+ _touch(
28
+ directory,
29
+ f"pygritlib-{version}-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
30
+ )
31
+ _touch(directory, f"pygritlib-{version}-cp311-abi3-musllinux_1_2_x86_64.whl")
32
+ _touch(directory, f"pygritlib-{version}-cp311-abi3-musllinux_1_2_aarch64.whl")
33
+ _touch(directory, f"pygritlib-{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 / "pygritlib-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, "pygritlib-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 / "pygritlib-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, "pygritlib-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 / "pygritlib-0.1.0-cp311-abi3-macosx_11_0_arm64.whl").unlink()
74
+ _touch(tmp_path, "pygritlib-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 / "pygritlib-0.1.0-cp311-abi3-macosx_11_0_arm64.whl").unlink()
82
+ _touch(tmp_path, "pygritlib-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,201 @@
1
+ # CI for pygritlib (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/pygritlib` 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 pygritlib, 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 pygritlib
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
+ # AIDEV-NOTE: ring 0.17 (via grit-lib http-ureq -> rustls -> ring) ships pregenerated
133
+ # ARMv8 asm whose asm_base.h aborts with `#error "ARM assembler must define __ARM_ARCH"`
134
+ # under the manylinux glibc aarch64 cross-gcc, which doesn't predefine that macro for .S
135
+ # files. Force-define it for the glibc-aarch64 target only (the CFLAGS var is
136
+ # target-specific, so x86_64 / musl / macOS legs are untouched). Runs in the build
137
+ # container so the export reaches cargo's cc invocation.
138
+ before-script-linux: |
139
+ export CFLAGS_aarch64_unknown_linux_gnu="-D__ARM_ARCH=8"
140
+ - name: list wheels
141
+ run: ls -la dist
142
+ - name: verify abi3 tag
143
+ shell: bash
144
+ run: ls dist/*.whl | grep -q abi3 && echo "abi3 OK"
145
+ - name: test the INSTALLED wheel (clean venv, full suite + stubtest)
146
+ if: matrix.smoke
147
+ shell: bash
148
+ run: |
149
+ set -euxo pipefail
150
+ # Clean venv: only the built wheel + test deps. git is preinstalled on
151
+ # GitHub runners (the suite shells out to it via tests/conftest.py).
152
+ python3 -m venv /tmp/wheeltest
153
+ /tmp/wheeltest/bin/python -m pip install --upgrade pip
154
+ /tmp/wheeltest/bin/pip install dist/*.whl pytest mypy
155
+ # Stage ONLY tests/ + the pyproject pytest config in an empty dir, so the
156
+ # source `python/pygritlib` is NOT importable. pyproject's pythonpath=["."]
157
+ # then resolves to this dir (no source pygritlib), making the installed wheel
158
+ # the only `import pygritlib`.
159
+ workdir="$(mktemp -d)"
160
+ cp -r tests "$workdir/"
161
+ cp pyproject.toml "$workdir/"
162
+ cd "$workdir"
163
+ # Prove the import comes from the installed wheel, not a source tree.
164
+ /tmp/wheeltest/bin/python -c "import pygritlib, os; p = os.path.dirname(pygritlib.__file__); print('pygritlib from:', p); assert 'site-packages' in p, p"
165
+ /tmp/wheeltest/bin/python -m pytest tests/ -q
166
+ /tmp/wheeltest/bin/python -m mypy.stubtest pygritlib
167
+ - name: import-smoke the musl wheel (Alpine container)
168
+ if: matrix.musl
169
+ shell: bash
170
+ run: |
171
+ set -euxo pipefail
172
+ # The full pytest+stubtest suite runs on glibc x86_64 + both macOS legs; this
173
+ # leg just proves the wheel loads and links against musl libc. Native docker
174
+ # (x86_64 host) — no QEMU.
175
+ shopt -s nullglob
176
+ wheel=(dist/*-cp311-abi3-*.whl)
177
+ if [[ ${#wheel[@]} -ne 1 ]]; then
178
+ echo "::error::expected exactly one *-cp311-abi3-*.whl, found ${#wheel[@]}: ${wheel[*]:-<none>}"
179
+ ls -la dist || true
180
+ exit 1
181
+ fi
182
+ docker run --rm -v "$PWD/dist:/dist:ro" python:3.11-alpine \
183
+ sh -c "pip install /dist/${wheel[0]##*/} && python -c 'import pygritlib; pygritlib.Repository'"
184
+ - name: build + install + smoke the sdist (fail hard)
185
+ # glibc x86_64 leg only — the musl x86_64 leg also matches os+target.
186
+ if: matrix.os == 'ubuntu-latest' && matrix.target == 'x86_64' && matrix.manylinux == 'auto'
187
+ shell: bash
188
+ run: |
189
+ set -euxo pipefail
190
+ # Build the sdist into dist/ (no `|| true` — a failure must fail the job).
191
+ uvx maturin sdist --out dist
192
+ # Install the sdist into a clean venv: this COMPILES from source, using
193
+ # the Rust toolchain set up for this job.
194
+ python3 -m venv /tmp/sdisttest
195
+ /tmp/sdisttest/bin/python -m pip install --upgrade pip
196
+ /tmp/sdisttest/bin/pip install dist/pygritlib-*.tar.gz
197
+ /tmp/sdisttest/bin/python -c "import pygritlib; pygritlib.Repository"
198
+ - uses: actions/upload-artifact@v7
199
+ with:
200
+ name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }}
201
+ path: dist
@@ -0,0 +1,297 @@
1
+ # Trusted-publishing release pipeline for pygritlib (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']=='pygritlib'][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
+ # AIDEV-NOTE: ring 0.17 (via grit-lib http-ureq -> rustls -> ring) ships pregenerated
131
+ # ARMv8 asm whose asm_base.h aborts with `#error "ARM assembler must define __ARM_ARCH"`
132
+ # under the manylinux glibc aarch64 cross-gcc, which doesn't predefine that macro for .S
133
+ # files. Force-define it for the glibc-aarch64 target only (the CFLAGS var is
134
+ # target-specific, so x86_64 / musl / macOS legs are untouched). Runs in the build
135
+ # container so the export reaches cargo's cc invocation.
136
+ before-script-linux: |
137
+ export CFLAGS_aarch64_unknown_linux_gnu="-D__ARM_ARCH=8"
138
+ - name: Verify exactly one cp311-abi3 wheel
139
+ shell: bash
140
+ run: |
141
+ set -euo pipefail
142
+ shopt -s nullglob
143
+ wheels=(dist/*-cp311-abi3-*.whl)
144
+ if [[ ${#wheels[@]} -ne 1 ]]; then
145
+ echo "::error::expected exactly one *-cp311-abi3-*.whl, found ${#wheels[@]}: ${wheels[*]:-<none>}"
146
+ ls -la dist || true
147
+ exit 1
148
+ fi
149
+ echo "abi3 wheel OK: ${wheels[0]}"
150
+ - name: Set up Python 3.11 for native smoke
151
+ if: matrix.smoke == 'native'
152
+ uses: actions/setup-python@v5
153
+ with:
154
+ python-version: "3.11"
155
+ - name: Import smoke (native, Python 3.11)
156
+ if: matrix.smoke == 'native'
157
+ shell: bash
158
+ run: |
159
+ set -euxo pipefail
160
+ python -m venv /tmp/smoke
161
+ /tmp/smoke/bin/python -m pip install --upgrade pip
162
+ /tmp/smoke/bin/pip install dist/*-cp311-abi3-*.whl
163
+ /tmp/smoke/bin/python -c "import pygritlib; pygritlib.Repository"
164
+ /tmp/smoke/bin/python -c "import pygritlib, os; p=os.path.dirname(pygritlib.__file__); assert 'site-packages' in p, p; print('imported from', p)"
165
+ - name: Set up QEMU for emulated arm64 smoke
166
+ if: matrix.smoke == 'container' && matrix.platform == 'linux/arm64'
167
+ uses: docker/setup-qemu-action@v3
168
+ - name: Import smoke (container)
169
+ if: matrix.smoke == 'container'
170
+ shell: bash
171
+ run: |
172
+ set -euxo pipefail
173
+ # One step for all container legs: glibc-arm64 (slim) + musl x86_64/arm64
174
+ # (alpine). `sh -c` works in both slim (dash) and alpine (busybox sh).
175
+ shopt -s nullglob
176
+ wheel=(dist/*-cp311-abi3-*.whl)
177
+ docker run --rm --platform ${{ matrix.platform }} \
178
+ -v "$PWD/dist:/dist:ro" ${{ matrix.image }} \
179
+ sh -c "pip install /dist/${wheel[0]##*/} && python -c 'import pygritlib; pygritlib.Repository'"
180
+ - uses: actions/upload-artifact@v7
181
+ with:
182
+ name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }}
183
+ path: dist
184
+ if-no-files-found: error
185
+
186
+ sdist:
187
+ name: sdist
188
+ needs: [version-guard]
189
+ if: always() && (needs.version-guard.result == 'success' || needs.version-guard.result == 'skipped')
190
+ runs-on: ubuntu-latest
191
+ steps:
192
+ - uses: actions/checkout@v6
193
+ with:
194
+ persist-credentials: false
195
+ - uses: dtolnay/rust-toolchain@1.94.1
196
+ with:
197
+ components: rustfmt, clippy
198
+ - uses: astral-sh/setup-uv@v8.2.0
199
+ - run: uv sync --group dev
200
+ # `uv run maturin` uses the maturin pinned in uv.lock (reproducible release
201
+ # builds), NOT a floating `uvx maturin` as in ci.yml's smoke step — deliberate.
202
+ - name: Build sdist (locked maturin)
203
+ run: uv run maturin sdist --out dist
204
+ - name: Source-compile + import-smoke the sdist
205
+ shell: bash
206
+ run: |
207
+ set -euxo pipefail
208
+ python3 -m venv /tmp/sdisttest
209
+ /tmp/sdisttest/bin/python -m pip install --upgrade pip
210
+ /tmp/sdisttest/bin/pip install dist/pygritlib-*.tar.gz
211
+ /tmp/sdisttest/bin/python -c "import pygritlib; pygritlib.Repository"
212
+ - uses: actions/upload-artifact@v7
213
+ with:
214
+ name: sdist
215
+ path: dist
216
+ if-no-files-found: error
217
+
218
+ publish-pypi:
219
+ name: publish to PyPI
220
+ needs: [build, sdist]
221
+ if: github.event_name == 'release'
222
+ runs-on: ubuntu-latest
223
+ environment: pypi
224
+ permissions:
225
+ id-token: write
226
+ contents: read
227
+ steps:
228
+ - uses: actions/checkout@v6
229
+ with:
230
+ fetch-depth: 0
231
+ persist-credentials: false
232
+ - name: Provenance — built commit must be an ancestor of origin/main
233
+ shell: bash
234
+ run: |
235
+ set -euo pipefail
236
+ # This repo's default branch is `main`; update both publish jobs if renamed.
237
+ git fetch --no-tags origin main
238
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
239
+ echo "::error::commit $GITHUB_SHA is not an ancestor of origin/main; refusing to publish."
240
+ exit 1
241
+ fi
242
+ echo "provenance OK: $GITHUB_SHA is on origin/main"
243
+ - uses: actions/download-artifact@v7
244
+ with:
245
+ pattern: "*"
246
+ merge-multiple: true
247
+ path: dist
248
+ - name: Inventory — exactly 5 wheels + 1 sdist, single version
249
+ shell: bash
250
+ run: |
251
+ set -euo pipefail
252
+ ls -la dist
253
+ python3 .github/scripts/check_release_inventory.py dist
254
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
255
+ with:
256
+ packages-dir: dist
257
+
258
+ publish-testpypi:
259
+ name: publish to TestPyPI
260
+ needs: [build, sdist]
261
+ if: github.event_name == 'workflow_dispatch'
262
+ runs-on: ubuntu-latest
263
+ environment: testpypi
264
+ permissions:
265
+ id-token: write
266
+ contents: read
267
+ steps:
268
+ - uses: actions/checkout@v6
269
+ with:
270
+ fetch-depth: 0
271
+ persist-credentials: false
272
+ - name: Provenance — built commit must be an ancestor of origin/main
273
+ shell: bash
274
+ run: |
275
+ set -euo pipefail
276
+ # This repo's default branch is `main`; update both publish jobs if renamed.
277
+ git fetch --no-tags origin main
278
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
279
+ echo "::error::commit $GITHUB_SHA is not an ancestor of origin/main; refusing to publish."
280
+ exit 1
281
+ fi
282
+ echo "provenance OK: $GITHUB_SHA is on origin/main"
283
+ - uses: actions/download-artifact@v7
284
+ with:
285
+ pattern: "*"
286
+ merge-multiple: true
287
+ path: dist
288
+ - name: Inventory — exactly 5 wheels + 1 sdist, single version
289
+ shell: bash
290
+ run: |
291
+ set -euo pipefail
292
+ ls -la dist
293
+ python3 .github/scripts/check_release_inventory.py dist
294
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
295
+ with:
296
+ repository-url: https://test.pypi.org/legacy/
297
+ packages-dir: dist
@@ -0,0 +1,11 @@
1
+ /target
2
+ /.venv
3
+ __pycache__/
4
+ *.pyc
5
+ *.so
6
+ /dist
7
+ /build
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ *.egg-info/