Skip to content

Commit 69b3888

Browse files
codexByron
authored andcommitted
Build and gate tested macOS distributions for Trusted Publishing
Limit release artifacts to Apple Silicon and Intel macOS 11+, with ordinary ABI3 and free-threaded CPython wheels. Build each wheel from the source archive and test the installed package. Add an explicit manual release workflow, version-tag validation, a complete artifact set check, and environment-gated PyPI Trusted Publishing with attestations. Move version/feature integration checks, source packaging, and no-C validation to the supported macOS platform. Retain Linux and Windows portability checks. No automatic publication is triggered. Validation: actionlint 1.7.12 and offline pedantic zizmor 1.30.1 pass; three release-gate regression tests pass; rebuilt source archive and ABI3 Apple Silicon wheel pass artifact validation; installed ABI3 wheel passes 105 integration tests with two expected skips.
1 parent 4257749 commit 69b3888

7 files changed

Lines changed: 217 additions & 19 deletions

File tree

‎.github/workflows/artifacts.yml‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Distribution artifacts
33
# Build reviewable candidates only. This workflow does not publish packages or releases.
44
on:
55
workflow_dispatch:
6+
workflow_call:
67

78
permissions:
89
contents: read
@@ -14,6 +15,9 @@ concurrency:
1415
env:
1516
CARGO_TERM_COLOR: always
1617
UV_NO_INDEX: '1'
18+
MACOSX_DEPLOYMENT_TARGET: '11.0'
19+
CC: /usr/bin/false
20+
CXX: /usr/bin/false
1721

1822
defaults:
1923
run:
@@ -25,7 +29,7 @@ jobs:
2529
strategy:
2630
fail-fast: false
2731
matrix:
28-
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-latest, windows-latest]
32+
os: [macos-15-intel, macos-latest]
2933
python: ['3.11', '3.14t']
3034
runs-on: ${{ matrix.os }}
3135
env:
@@ -53,14 +57,10 @@ jobs:
5357
with:
5458
key: artifacts-${{ matrix.python }}
5559
save-if: ${{ github.ref == 'refs/heads/main' }}
56-
- name: Build host-platform wheel
60+
- name: Build macOS wheel from the source archive
5761
run: |
58-
args=()
59-
if [[ "$RUNNER_OS" == Linux ]]; then
60-
args+=(--compatibility linux)
61-
fi
6262
interpreter="$(uv python find "$PYTHON_VERSION")"
63-
maturin build --release --locked --out dist --interpreter "$interpreter" "${args[@]}"
63+
maturin build --sdist --release --locked --compatibility pypi --out dist --interpreter "$interpreter"
6464
- name: Verify wheel contents
6565
run: uv run --no-project --offline --python "$PYTHON_VERSION" python etc/check_artifact.py dist/*.whl
6666
- name: Install wheel without an index
@@ -82,7 +82,7 @@ jobs:
8282

8383
sdist:
8484
name: Source distribution
85-
runs-on: ubuntu-latest
85+
runs-on: macos-latest
8686
steps:
8787
- name: Checkout
8888
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

‎.github/workflows/ci.yml‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ jobs:
3939
save-if: ${{ github.ref == 'refs/heads/main' }}
4040
- name: Check formatting
4141
run: cargo +stable fmt --all -- --check
42+
- name: Check release validation
43+
run: python3 etc/test_check_release.py
4244
- name: Check clippy
4345
run: cargo +stable clippy --locked --all-targets -- -D warnings
4446
- name: Run Rust unit tests
@@ -50,29 +52,29 @@ jobs:
5052
fail-fast: false
5153
matrix:
5254
include:
53-
- os: ubuntu-latest
55+
- os: macos-latest
5456
python: '3.11'
5557
features: abi3
56-
- os: ubuntu-latest
58+
- os: macos-latest
5759
python: '3.12'
5860
features: abi3
59-
- os: ubuntu-latest
61+
- os: macos-latest
6062
python: '3.13'
6163
features: abi3
62-
- os: ubuntu-latest
64+
- os: macos-latest
6365
python: '3.14'
6466
features: abi3
65-
- os: macos-latest
67+
- os: macos-15-intel
6668
python: '3.14'
6769
features: abi3
6870
- os: windows-latest
6971
python: '3.14'
7072
features: abi3
71-
- os: ubuntu-latest
73+
- os: macos-latest
7274
python: '3.14'
7375
features: sha1,abi3
7476
minimal: true
75-
- os: ubuntu-latest
77+
- os: macos-latest
7678
python: '3.14'
7779
features: sha256,abi3
7880
minimal: true
@@ -170,7 +172,7 @@ jobs:
170172

171173
no-c:
172174
name: Build without a C or C++ compiler
173-
runs-on: ubuntu-latest
175+
runs-on: macos-latest
174176
env:
175177
CC: /usr/bin/false
176178
CXX: /usr/bin/false
@@ -187,7 +189,7 @@ jobs:
187189

188190
packaging:
189191
name: Rebuild and test the source distribution
190-
runs-on: ubuntu-latest
192+
runs-on: macos-latest
191193
steps:
192194
- name: Checkout
193195
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -212,7 +214,7 @@ jobs:
212214
key: sdist
213215
save-if: ${{ github.ref == 'refs/heads/main' }}
214216
- name: Build wheel from the source archive
215-
run: maturin build --sdist --locked --compatibility linux --out dist --interpreter "$(uv python find 3.11)"
217+
run: maturin build --sdist --locked --compatibility pypi --out dist --interpreter "$(uv python find 3.11)"
216218
- name: Verify archive and wheel contents
217219
run: uv run --no-project --offline --python 3.11 python etc/check_artifact.py dist/*.tar.gz dist/*.whl
218220
- name: Install rebuilt wheel without an index

‎.github/workflows/release.yml‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
publish:
7+
description: Publish the tested artifacts to PyPI (run from the version tag)
8+
type: boolean
9+
default: false
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: release-${{ github.ref }}
16+
cancel-in-progress: false
17+
18+
jobs:
19+
metadata:
20+
name: Validate release metadata
21+
runs-on: macos-latest
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25+
with:
26+
persist-credentials: false
27+
- name: Validate version and changelog
28+
env:
29+
PUBLISH: ${{ inputs.publish }}
30+
RELEASE_REF: ${{ github.ref }}
31+
RELEASE_TAG: ${{ github.ref_name }}
32+
run: |
33+
python3 etc/test_check_release.py
34+
if [[ "$PUBLISH" == true ]]; then
35+
[[ "$RELEASE_REF" == refs/tags/v* ]] || exit 1
36+
python3 etc/check_release.py --tag "$RELEASE_TAG"
37+
else
38+
python3 etc/check_release.py
39+
fi
40+
41+
artifacts:
42+
needs: metadata
43+
uses: ./.github/workflows/artifacts.yml # zizmor: ignore[self-repository] Keep the same-commit syntax supported by actionlint 1.7.12.
44+
45+
validate:
46+
name: Validate complete release set
47+
needs: artifacts
48+
runs-on: macos-latest
49+
steps:
50+
- name: Checkout
51+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
52+
with:
53+
persist-credentials: false
54+
- name: Download tested artifacts
55+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
56+
with:
57+
path: dist
58+
merge-multiple: true
59+
- name: Validate distributions
60+
run: python3 etc/check_release.py --dist dist
61+
62+
publish:
63+
name: Publish to PyPI
64+
if: inputs.publish
65+
needs: validate
66+
runs-on: ubuntu-latest
67+
environment:
68+
name: pypi
69+
url: https://pypi.org/project/GixPython/
70+
permissions:
71+
id-token: write # Authenticate this approved release with PyPI Trusted Publishing.
72+
steps:
73+
- name: Download tested artifacts
74+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
75+
with:
76+
path: dist
77+
merge-multiple: true
78+
- name: Publish distributions with attestations
79+
uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2

‎docs/validation.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Local validation
22

3+
## First release preparation (2026-10-05)
4+
5+
Version 0.1.0 now targets macOS 11+ on Apple Silicon and Intel. Updated
6+
Apple Silicon wheels were built from the source archive using maturin 1.15.0
7+
and Rust 1.99.0, without accessing a Python package index. Both installed
8+
wheels passed all 105 integration tests: ordinary CPython 3.14.7 had two
9+
expected skips; free-threaded CPython 3.14.7 with `PYTHON_GIL=0` had one.
10+
11+
Release metadata, source contents, licenses, type stubs, and both wheel
12+
contents passed local validation. Actionlint 1.7.12 and offline pedantic
13+
zizmor 1.30.1 passed; the same-commit reusable workflow syntax has one
14+
documented compatibility suppression. Three regression checks verify the
15+
release tag, artifact completeness, and wheel metadata version gates.
16+
17+
Current candidates and checksums are in `dist/0.1.0-macos-candidate/`.
18+
Intel wheels and the complete five-artifact release set await the configured
19+
GitHub workflow run. No PyPI account setup, tag, upload, or publication was
20+
performed. See [RELEASING.md](../RELEASING.md) for the exact setup and commands.
21+
22+
## Initial implementation validation (2026-10-01)
23+
324
These checks ran on macOS with Apple Silicon on 2026-10-01, using Rust 1.98.1
425
and maturin 1.15.0. They cover the final native implementation, including
526
configuration destruction/retry and clone revision-validation fixes.

‎etc/check_artifact.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def check_sdist(path, expected_stubs):
2020
prefix = roots.pop() + "/"
2121
names = {name.removeprefix(prefix) for name in entries}
2222
required = {"Cargo.toml", "Cargo.lock", "build.rs", "pyproject.toml", "src/lib.rs",
23-
"README.md", "LICENSE-MIT", "LICENSE-APACHE", "python/gix/__init__.py",
23+
"README.md", "CHANGELOG.md", "RELEASING.md", "LICENSE-MIT", "LICENSE-APACHE", "python/gix/__init__.py",
2424
"python/gix/py.typed", "etc/build.py", "etc/check_artifact.py", "tests/test_package.py"}
2525
required.update("python/gix/" + name for name in expected_stubs)
2626
manifest = tomllib.loads(archive.extractfile(prefix + "Cargo.toml").read().decode())

‎etc/check_release.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Validate a release tag and the complete macOS distribution set locally."""
2+
3+
import argparse
4+
from email.parser import BytesParser
5+
from pathlib import Path
6+
import tarfile
7+
import tomllib
8+
import zipfile
9+
10+
from check_artifact import check_sdist, check_wheel, require
11+
12+
13+
def main():
14+
parser = argparse.ArgumentParser(description=__doc__)
15+
parser.add_argument("--tag", help="Git release tag, for example v0.1.0")
16+
parser.add_argument("--dist", type=Path, help="Directory containing four wheels and one source archive")
17+
args = parser.parse_args()
18+
root = Path(__file__).resolve().parents[1]
19+
version = tomllib.loads((root / "Cargo.toml").read_text())["package"]["version"]
20+
require(args.tag is None or args.tag == "v" + version, "release tag does not match Cargo.toml")
21+
require("\n## " + version + " (" in (root / "CHANGELOG.md").read_text(), "release changelog entry missing")
22+
if args.dist is not None:
23+
expected = {f"gixpython-{version}-{abi}-macosx_11_0_{arch}.whl"
24+
for abi in ("cp311-abi3", "cp314-cp314t") for arch in ("arm64", "x86_64")}
25+
expected.add(f"gixpython-{version}.tar.gz")
26+
require({path.name for path in args.dist.iterdir()} == expected,
27+
"release must contain exactly four macOS wheels and one matching source archive")
28+
stubs = {path.name for path in (root / "python/gix").glob("*.pyi")}
29+
for path in sorted(args.dist.iterdir()):
30+
if path.suffix == ".whl":
31+
with zipfile.ZipFile(path) as archive:
32+
metadata_name, = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")]
33+
metadata = BytesParser().parsebytes(archive.read(metadata_name))
34+
wheel = BytesParser().parsebytes(archive.read(metadata_name.removesuffix("METADATA") + "WHEEL"))
35+
require(metadata["Version"] == version, "wheel metadata version does not match release")
36+
tag = path.name.removesuffix(".whl").split("-", 2)[2]
37+
require(wheel.get_all("Tag") == [tag], "wheel ABI/platform does not match release filename")
38+
check_wheel(path, stubs)
39+
else:
40+
with tarfile.open(path) as archive:
41+
metadata_name, = [name for name in archive.getnames() if name.count("/") == 1 and name.endswith("/PKG-INFO")]
42+
metadata = BytesParser().parsebytes(archive.extractfile(metadata_name).read())
43+
require(metadata["Version"] == version, "source metadata version does not match release")
44+
check_sdist(path, stubs)
45+
print(f"Validated GixPython {version}" + (" release artifacts" if args.dist else " release metadata"))
46+
47+
48+
if __name__ == "__main__":
49+
main()

‎etc/test_check_release.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Checks for release gates; these run without building or importing gix."""
2+
3+
from pathlib import Path
4+
import subprocess
5+
import sys
6+
import tempfile
7+
import tomllib
8+
import unittest
9+
import zipfile
10+
11+
12+
class ReleaseTests(unittest.TestCase):
13+
version = tomllib.loads((Path(__file__).resolve().parents[1] / "Cargo.toml").read_text())["package"]["version"]
14+
15+
def run_check(self, *args):
16+
return subprocess.run([sys.executable, str(Path(__file__).with_name("check_release.py")), *args],
17+
capture_output=True, text=True)
18+
19+
def test_matching_and_mismatched_tag(self):
20+
self.assertEqual(self.run_check("--tag", "v" + self.version).returncode, 0)
21+
result = self.run_check("--tag", "v99.0.0")
22+
self.assertNotEqual(result.returncode, 0)
23+
self.assertIn("release tag does not match", result.stderr)
24+
25+
def test_partial_set_is_rejected(self):
26+
with tempfile.TemporaryDirectory() as directory:
27+
result = self.run_check("--dist", directory)
28+
self.assertNotEqual(result.returncode, 0)
29+
self.assertIn("exactly four macOS wheels", result.stderr)
30+
31+
def test_renamed_wheel_cannot_hide_wrong_version(self):
32+
with tempfile.TemporaryDirectory() as directory:
33+
root = Path(directory)
34+
for abi in ("cp311-abi3", "cp314-cp314t"):
35+
for arch in ("arm64", "x86_64"):
36+
tag = f"{abi}-macosx_11_0_{arch}"
37+
with zipfile.ZipFile(root / f"gixpython-{self.version}-{tag}.whl", "w") as archive:
38+
archive.writestr(f"gixpython-{self.version}.dist-info/METADATA", "Name: GixPython\nVersion: 99.0.0\n")
39+
archive.writestr(f"gixpython-{self.version}.dist-info/WHEEL", f"Tag: {tag}\n")
40+
(root / f"gixpython-{self.version}.tar.gz").touch()
41+
result = self.run_check("--dist", directory)
42+
self.assertNotEqual(result.returncode, 0)
43+
self.assertIn("wheel metadata version does not match", result.stderr)
44+
45+
46+
if __name__ == "__main__":
47+
unittest.main()

0 commit comments

Comments
 (0)