Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 114 additions & 2 deletions .github/scripts/test_ci_workflow_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,81 @@


class WorkflowContractsTest(unittest.TestCase):
def test_wheels_preserve_abi_coverage_and_release_gates(self):
workflow = (ROOT / ".github/workflows/release.yml").read_text()
config = tomllib.loads((ROOT / "pyproject.toml").read_text())
wheels = config["tool"]["cibuildwheel"]
# Keep the full installed-wheel suite and platform repair hooks.
self.assertIn("pytest {project}/tests/python", wheels["test-command"])
self.assertIn("delocate-wheel", wheels["macos"]["repair-wheel-command"])
self.assertIn("delvewheel repair", wheels["windows"]["repair-wheel-command"])
self.assertIn("scripts/build-macos-openmp.sh", wheels["macos"]["before-all"])
self.assertEqual(workflow.count("pattern: '*-wheels'"), 2)
artifacts = set()
for prefix, tag, check_name in (
("linux", "manylinux_x86_64", "Linux x86-64 wheels"),
(
"linux-arm64",
"manylinux_aarch64",
"Linux ARM64 wheels (CPython 3.11–3.14)",
),
("windows", "win_amd64", "Windows x86-64 wheels"),
("macos", "macosx_arm64", "macOS arm64 wheels (CPython 3.11–3.14)"),
):
with self.subTest(platform=prefix):
build = workflow.split(f" {prefix}-wheel-build:\n", 1)[1].split(
f" {prefix}-wheels:\n", 1
)[0]
selection = re.search(
r"CIBW_BUILD: \$\{\{ github.event_name == 'pull_request' "
r"&& '([^']+)' \|\| '([^']+)' \}\}",
build,
)
self.assertIsNotNone(selection)
supported = {
identifier
for identifier in wheels["build"].split()
if identifier.endswith(f"-{tag}")
}
for patterns, abis in zip(
selection.groups(), ((311, 314), (311, 312, 313, 314)), strict=True
):
selected = {
identifier
for identifier in supported
if any(
fnmatch.fnmatchcase(identifier, p) for p in patterns.split()
)
}
self.assertEqual(selected, {f"cp{abi}-{tag}" for abi in abis})
self.assertNotIn("CIBW_TEST", build)
self.assertNotIn("continue-on-error", build)
self.assertNotIn("matrix", build)
artifact = re.search(r" name: (\S+)", build).group(1)
self.assertTrue(fnmatch.fnmatchcase(artifact, "*-wheels"))
self.assertNotIn(artifact, artifacts)
artifacts.add(artifact)
gate = re.search(
rf"^ {prefix}-wheels:\n(.*?)(?=^ [\w-]+:|\Z)",
workflow,
re.MULTILINE | re.DOTALL,
).group(1)
self.assertIn(f"name: {check_name}", gate)
self.assertIn(f"needs: {prefix}-wheel-build", gate)
self.assertIn(
f"BUILD_RESULT: ${{{{ needs.{prefix}-wheel-build.result }}}}", gate
)
command = gate.split(" run: ", 1)[1].strip()
for result in ("success", "failure", "cancelled", "skipped", ""):
with self.subTest(result=result):
verdict = subprocess.run(
["bash", "-e", "-c", command],
env=dict(os.environ, BUILD_RESULT=result),
capture_output=True,
check=False,
)
self.assertEqual(verdict.returncode == 0, result == "success")

def test_installed_typing_checks_are_required(self):
config = tomllib.loads((ROOT / "pyproject.toml").read_text())
test_dependencies = config["project"]["optional-dependencies"]["test"]
Expand Down Expand Up @@ -54,7 +129,7 @@ def test_installed_typing_checks_are_required(self):
)[0]
self.assertIn('pip install ".[test,typing]"', build)
self.assertIn(
"python -m pytest tests/python/test_editor_support.py tests/test_mypy.py",
"python -m pytest tests/python/test_editor_support.py::test_installed_public_declarations",
build,
)
self.assertNotIn("continue-on-error", build)
Expand All @@ -69,12 +144,49 @@ def test_installed_typing_checks_are_required(self):
for path in (
"python_bindings/__init__.pyi",
"tests/python/typing/public_api.py",
"tests/test_mypy.py",
):
with self.subTest(path=path):
self.assertLessEqual(
{"python_build", "python_quality", "wheels"}, classify([path])
)
self.assertEqual(
{"python_quality", "python_build", "wheels"},
classify(["tests/test_mypy.py"]),
)

def test_nonrelease_push_keeps_typing_checks_when_wheels_skip(self):
from test_prepare_release import ReleaseTest

release = ReleaseTest().run_prepare(tags="v0.3.3", tag_sha="b" * 40)
self.assertEqual(release["build"], "false")
workflow = (ROOT / ".github/workflows/python.yml").read_text()
build = workflow.split(" python-build:\n", 1)[1].split(
" python-result:\n", 1
)[0]
steps = re.findall(
r"^ - name: (.*?)(?=^ - |\Z)",
build,
re.MULTILINE | re.DOTALL,
)
typing_steps = [step for step in steps if "tests/test_mypy.py" in step]
self.assertEqual(len(typing_steps), 1)
self.assertIn("if: github.event_name == 'push'", typing_steps[0])
self.assertIn(
"run: python -m pytest tests/python/test_editor_support.py tests/test_mypy.py",
typing_steps[0],
)
declaration_steps = [
step for step in steps if "::test_installed_public_declarations" in step
]
self.assertEqual(len(declaration_steps), 1)
self.assertIn("if: github.event_name == 'pull_request'", declaration_steps[0])
for path in (
"tests/test_mypy.py",
"tests/python/test_editor_support.py",
"python_bindings/__init__.pyi",
):
with self.subTest(path=path):
self.assertIn("python_build", classify([path]))

def test_pyright_checks_are_required_and_scoped(self):
config = tomllib.loads((ROOT / "pyproject.toml").read_text())
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ jobs:
- name: Build and install wheel
run: pip install ".[test,typing]" -v

- name: Check installed Python types and editor metadata
# PR wheels own mypy and the full editor suite. Non-release branch pushes
# may skip wheels, so source installs must retain those checks on pushes.
- name: Check source-installed Python declarations
if: github.event_name == 'pull_request'
run: python -m pytest tests/python/test_editor_support.py::test_installed_public_declarations --tb=short -q

- name: Check installed Python types and editor metadata on pushes
if: github.event_name == 'push'
env:
OMP_NUM_THREADS: "2"
run: python -m pytest tests/python/test_editor_support.py tests/test_mypy.py --tb=short -q
Expand Down
49 changes: 17 additions & 32 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,31 @@ jobs:
run: python .github/scripts/prepare_release.py

linux-wheel-build:
name: Linux x86-64 ${{ matrix.abi }} wheel
name: Linux x86-64 wheel builds
needs: [prepare, changes]
if: >-
!cancelled() && needs.prepare.result == 'success' &&
needs.prepare.outputs.build == 'true' &&
(needs.changes.result != 'success' || needs.changes.outputs.wheels == 'true')
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
abi: ${{ fromJSON(github.event_name == 'pull_request' && '["cp311","cp314"]' || '["cp311","cp312","cp313","cp314"]') }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.sha }}
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Build and test wheel
- name: Build and test wheels
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_BUILD: ${{ matrix.abi }}-manylinux_x86_64
CIBW_BUILD: ${{ github.event_name == 'pull_request' && 'cp311-manylinux_x86_64 cp314-manylinux_x86_64' || 'cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64' }}
with:
output-dir: wheelhouse
- name: Store wheel artifact
uses: actions/upload-artifact@v7
with:
name: linux-${{ matrix.abi }}-wheels
name: linux-wheels
path: wheelhouse/*.whl
if-no-files-found: error

Expand All @@ -101,18 +97,14 @@ jobs:
run: test "$BUILD_RESULT" = success

linux-arm64-wheel-build:
name: Linux ARM64 ${{ matrix.abi }} wheel
name: Linux ARM64 wheel builds
needs: [prepare, changes]
if: >-
!cancelled() && needs.prepare.result == 'success' &&
needs.prepare.outputs.build == 'true' &&
(needs.changes.result != 'success' || needs.changes.outputs.wheels == 'true')
runs-on: ubuntu-24.04-arm
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
abi: ${{ fromJSON(github.event_name == 'pull_request' && '["cp311","cp314"]' || '["cp311","cp312","cp313","cp314"]') }}
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -122,16 +114,16 @@ jobs:
python-version: "3.14"
- name: Verify native ARM64 runner
run: test "$(uname -m)" = aarch64
- name: Build and test repaired wheel
- name: Build and test repaired wheels
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_BUILD: ${{ matrix.abi }}-manylinux_aarch64
CIBW_BUILD: ${{ github.event_name == 'pull_request' && 'cp311-manylinux_aarch64 cp314-manylinux_aarch64' || 'cp311-manylinux_aarch64 cp312-manylinux_aarch64 cp313-manylinux_aarch64 cp314-manylinux_aarch64' }}
with:
output-dir: wheelhouse
- name: Store wheel artifact
uses: actions/upload-artifact@v7
with:
name: linux-arm64-${{ matrix.abi }}-wheels
name: linux-arm64-wheels
path: wheelhouse/*.whl
if-no-files-found: error

Expand All @@ -147,35 +139,31 @@ jobs:
run: test "$BUILD_RESULT" = success

windows-wheel-build:
name: Windows x86-64 ${{ matrix.abi }} wheel
name: Windows x86-64 wheel builds
needs: [prepare, changes]
if: >-
!cancelled() && needs.prepare.result == 'success' &&
needs.prepare.outputs.build == 'true' &&
(needs.changes.result != 'success' || needs.changes.outputs.wheels == 'true')
runs-on: windows-2025-vs2026
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
abi: ${{ fromJSON(github.event_name == 'pull_request' && '["cp311","cp314"]' || '["cp311","cp312","cp313","cp314"]') }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.sha }}
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Build and test wheel
- name: Build and test wheels
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_BUILD: ${{ matrix.abi }}-win_amd64
CIBW_BUILD: ${{ github.event_name == 'pull_request' && 'cp311-win_amd64 cp314-win_amd64' || 'cp311-win_amd64 cp312-win_amd64 cp313-win_amd64 cp314-win_amd64' }}
with:
output-dir: wheelhouse
- name: Store wheel artifact
uses: actions/upload-artifact@v7
with:
name: windows-${{ matrix.abi }}-wheels
name: windows-wheels
path: wheelhouse/*.whl
if-no-files-found: error

Expand All @@ -191,18 +179,15 @@ jobs:
run: test "$BUILD_RESULT" = success

macos-wheel-build:
name: macOS arm64 ${{ matrix.abi }} wheel
name: macOS arm64 wheel builds
needs: [prepare, changes]
if: >-
!cancelled() && needs.prepare.result == 'success' &&
needs.prepare.outputs.build == 'true' &&
(needs.changes.result != 'success' || needs.changes.outputs.wheels == 'true')
runs-on: macos-15
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
abi: ${{ fromJSON(github.event_name == 'pull_request' && '["cp311","cp314"]' || '["cp311","cp312","cp313","cp314"]') }}
# Share one runner and one before-all OpenMP build across the selected ABIs.
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -212,16 +197,16 @@ jobs:
python-version: "3.14"
- name: Verify native Apple Silicon runner
run: test "$(uname -m)" = arm64
- name: Build and test wheel with bundled OpenMP
- name: Build and test wheels with bundled OpenMP
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_BUILD: ${{ matrix.abi }}-macosx_arm64
CIBW_BUILD: ${{ github.event_name == 'pull_request' && 'cp311-macosx_arm64 cp314-macosx_arm64' || 'cp311-macosx_arm64 cp312-macosx_arm64 cp313-macosx_arm64 cp314-macosx_arm64' }}
with:
output-dir: wheelhouse
- name: Store wheel artifact
uses: actions/upload-artifact@v7
with:
name: macos-${{ matrix.abi }}-wheels
name: macos-wheels
path: wheelhouse/*.whl
if-no-files-found: error

Expand Down
Loading