From 6452d4bd17f35fe10a838cc1b11d4cd167d07170 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 25 Oct 2026 15:33:07 +0900 Subject: [PATCH 1/2] ci: a small matrix on pull requests, the full one on main, nightly and releases The organisation shares one runner cap with ArcadeData/arcadedb, so every extra macOS or Windows leg on a pull request delays upstream too. scripts/select_matrix.py picks the legs from the event name. A pull request runs linux/amd64 on Python 3.10-3.14 and the other three platforms on 3.10 and 3.14 (bindings, 11 legs), and all four platforms on 3.14 (examples, 4 legs). Every other event, including the release workflow's workflow_call, runs all 20 legs of each workflow as before. The first job of each workflow runs the script and the test job takes its matrix from that job's output, so the job names stay "Test arcadedb (, Python )". A pull request now runs 21 checks instead of 46. tests/test_select_matrix.py pins the full matrix at 20 legs and checks the Python list against the pyproject classifiers. New docs page development/ci.adoc. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/release.yml | 5 +- .github/workflows/test-python-bindings.yml | 45 ++++--- .github/workflows/test-python-examples.yml | 47 ++++---- CLAUDE.md | 2 +- README.md | 2 +- docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/development/ci.adoc | 39 ++++++ docs/modules/ROOT/pages/index.adoc | 2 +- docs/modules/ROOT/pages/installation.adoc | 2 +- scripts/select_matrix.py | 104 ++++++++++++++++ tests/test_select_matrix.py | 127 ++++++++++++++++++++ 11 files changed, 321 insertions(+), 55 deletions(-) create mode 100644 docs/modules/ROOT/pages/development/ci.adoc create mode 100644 scripts/select_matrix.py create mode 100644 tests/test_select_matrix.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3e4c9d8..38364e4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -175,8 +175,9 @@ jobs: wheels = sorted(pathlib.Path("dist").glob("*.whl")) # One wheel per platform and supported interpreter. The interpreters are the classifiers of - # pyproject.toml, which test-python-bindings.yml's matrix mirrors; the four platforms are - # the matrix platforms. Adding either to the matrix changes the expected count here. + # pyproject.toml, which the full matrix of scripts/select_matrix.py mirrors (the release runs the full + # matrix, not the small pull request one); the four platforms are the matrix platforms. Adding either to + # the matrix changes the expected count here. prefix = "Programming Language :: Python :: 3." classifiers = tomllib.load(open("pyproject.toml", "rb"))["project"]["classifiers"] pythons = [c.rsplit(" ", 1)[1] for c in classifiers if c.startswith(prefix)] diff --git a/.github/workflows/test-python-bindings.yml b/.github/workflows/test-python-bindings.yml index bf092cdb..bcffc56c 100644 --- a/.github/workflows/test-python-bindings.yml +++ b/.github/workflows/test-python-bindings.yml @@ -189,10 +189,25 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.draft == false outputs: arcadedb-tag: ${{ steps.jars.outputs.arcadedb-tag }} + matrix: ${{ steps.matrix.outputs.matrix }} steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # The platform x Python legs of this run (scripts/select_matrix.py): a small matrix on + # pull requests, the full one on every other event. The organisation shares one runner + # cap with ArcadeData/arcadedb, so a pull request does not run every combination. + # Done first, so a failure here costs no Docker pull. + - name: Select the test matrix + id: matrix + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + run: | + matrix="$(python3 scripts/select_matrix.py bindings --event "$EVENT_NAME")" + echo "$matrix" + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 @@ -250,30 +265,12 @@ jobs: strategy: # Don't cancel other matrix jobs if one fails fail-fast: false - matrix: - python-version: - - '3.10' - - '3.11' - - '3.12' - - '3.13' - - '3.14' - # Temporarily limit to four platforms (skip macOS x86_64, Windows ARM64) - # platform: ['linux/amd64', 'linux/arm64', 'darwin/amd64', 'darwin/arm64', 'windows/amd64', 'windows/arm64'] - platform: ['linux/amd64', 'linux/arm64', 'darwin/arm64', 'windows/amd64'] - include: - - platform: linux/amd64 - runs-on: ubuntu-24.04 - - platform: linux/arm64 - runs-on: ubuntu-24.04-arm - - platform: darwin/arm64 - runs-on: macos-15 - - platform: windows/amd64 - runs-on: windows-2025 - # - platform: darwin/amd64 - # runs-on: macos-15-intel - # - platform: windows/arm64 - # runs-on: windows-11-arm - # macOS x86_64 and Windows ARM64 temporarily disabled + # Chosen by the "Select the test matrix" step of download-jars from the event that + # started the run (scripts/select_matrix.py holds the platform and Python lists): + # small on pull requests, the full 4 platforms x 5 Pythons on every other event. + # Every leg carries platform, runs-on and python-version, so the job names stay + # "Test arcadedb (, Python )". + matrix: ${{ fromJSON(needs.download-jars.outputs.matrix) }} steps: - name: Checkout code diff --git a/.github/workflows/test-python-examples.yml b/.github/workflows/test-python-examples.yml index 93d54b4e..234378ad 100644 --- a/.github/workflows/test-python-examples.yml +++ b/.github/workflows/test-python-examples.yml @@ -106,10 +106,25 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.draft == false outputs: arcadedb-tag: ${{ steps.jars.outputs.arcadedb-tag }} + matrix: ${{ steps.matrix.outputs.matrix }} steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # The platform x Python legs of this run (scripts/select_matrix.py): a small matrix on + # pull requests, the full one on every other event. The organisation shares one runner + # cap with ArcadeData/arcadedb, so a pull request does not run every combination. + # Done first, so a failure here costs no Docker pull. + - name: Select the test matrix + id: matrix + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + run: | + matrix="$(python3 scripts/select_matrix.py examples --event "$EVENT_NAME")" + echo "$matrix" + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 @@ -163,36 +178,18 @@ jobs: BUILD_VERSION: ${{ inputs.build-version }} # Pin the Hugging Face cache inside the workspace so it is cacheable with # one path on every platform (the default differs per OS). The examples - # embed with sentence-transformers, and this matrix is 20 jobs, so + # embed with sentence-transformers, and the full matrix is 20 jobs, so # uncached that is 20 independent model downloads per release and 20 # chances for a transient hub outage to fail a release. It has happened. HF_HOME: ${{ github.workspace }}/.hf-cache strategy: fail-fast: false - matrix: - python-version: - - '3.10' - - '3.11' - - '3.12' - - '3.13' - - '3.14' - # Temporarily limit to four platforms (skip macOS x86_64, Windows ARM64) - # platform: ['linux/amd64', 'linux/arm64', 'darwin/amd64', 'darwin/arm64', 'windows/amd64', 'windows/arm64'] - platform: ['linux/amd64', 'linux/arm64', 'darwin/arm64', 'windows/amd64'] - include: - - platform: linux/amd64 - runs-on: ubuntu-24.04 - - platform: linux/arm64 - runs-on: ubuntu-24.04-arm - - platform: darwin/arm64 - runs-on: macos-15 - - platform: windows/amd64 - runs-on: windows-2025 - # - platform: darwin/amd64 - # runs-on: macos-15-intel - # - platform: windows/arm64 - # runs-on: windows-11-arm - # macOS x86_64 and Windows ARM64 temporarily disabled + # Chosen by the "Select the test matrix" step of download-jars from the event that + # started the run (scripts/select_matrix.py holds the platform and Python lists): + # small on pull requests, the full 4 platforms x 5 Pythons on every other event. + # Every leg carries platform, runs-on and python-version, so the job names stay + # "Test Python Examples (, Python )". + matrix: ${{ fromJSON(needs.download-jars.outputs.matrix) }} steps: - name: Checkout code diff --git a/CLAUDE.md b/CLAUDE.md index 2a7cfb75..2d82fbe8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,6 @@ python -m bandit -c pyproject.toml -r examples --severity-level medium --confide ## CI -`.github/workflows/test-python-bindings.yml` builds wheels for linux/amd64, linux/arm64, darwin/arm64, windows/amd64 and tests Python 3.10–3.14; also runs nightly against `arcadedata/arcadedb:latest`. Both workflows accept an `arcadedb-tag` input to pin the image. `test-python-examples.yml` runs `examples/`. A dependency-floor audit resolves extras with `--resolution lowest-direct` for each Python classifier and runs pip-audit, so raise lower bounds when a floor has an advisory. +`.github/workflows/test-python-bindings.yml` builds wheels for linux/amd64, linux/arm64, darwin/arm64, windows/amd64 and tests Python 3.10–3.14; also runs nightly against `arcadedata/arcadedb:latest`. The org shares a runner cap with upstream, so a pull request runs a small matrix (bindings: linux/amd64 on all five Pythons, the other three platforms on 3.10 and 3.14 = 11 legs; examples: all four platforms on 3.14 = 4 legs, 21 checks in all), while push to main, the nightly run, dispatch and the release run all 20 legs of each workflow (46 checks). `scripts/select_matrix.py` holds both lists (`PULL_REQUEST_LEGS`), the first job of each workflow runs it, and `tests/test_select_matrix.py` pins the full matrix; docs page `development/ci.adoc`. Keep the job names `Test arcadedb (, Python )` unchanged. Both workflows accept an `arcadedb-tag` input to pin the image. `test-python-examples.yml` runs `examples/`. A dependency-floor audit resolves extras with `--resolution lowest-direct` for each Python classifier and runs pip-audit, so raise lower bounds when a floor has an advisory. `.github/workflows/release.yml` publishes on a bare-version tag (`26.11.1.dev0`, `26.11.1`, `26.11.1.post1`; no `v`, never `aN`/`bN`/`rcN`). Its first job runs `scripts/check_version.py` (the devN/postN counting rule, read-only, fails closed); `scripts/after_tag_verify.sh` checks the finished release. The rule is stated once, on the docs page `development/release.adoc`. diff --git a/README.md b/README.md index ec1f9b65..5c37bccd 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ pip install arcadedb # or: uv add arcadedb This package was published as arcadedb-embedded until 26.10.x; install arcadedb now. -- Python 3.10 to 3.14. CI builds and tests all five versions on every platform below. +- Python 3.10 to 3.14. CI builds and tests all five versions on every platform below for `main`, every night and each release. A pull request runs a smaller selection. - Linux x86_64 and ARM64, macOS on Apple Silicon, Windows x86_64. One wheel per platform and Python version. - Import name: `arcadedb`. Optional extras: `arrow`, `pandas`, `vector`. diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index dff0f2fa..a4014eeb 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -6,4 +6,5 @@ ** xref:how-to/databases-and-queries.adoc[Databases and queries] ** xref:how-to/concurrency.adoc[Concurrency, processes and crashes] * Development +** xref:development/ci.adoc[Continuous integration] ** xref:development/release.adoc[Release] diff --git a/docs/modules/ROOT/pages/development/ci.adoc b/docs/modules/ROOT/pages/development/ci.adoc new file mode 100644 index 00000000..3c39f855 --- /dev/null +++ b/docs/modules/ROOT/pages/development/ci.adoc @@ -0,0 +1,39 @@ += Continuous integration +:description: Which platforms and Python versions the CI runs for a pull request, and which for main, the nightly run and a release. + +Two workflows test the package: `test-python-bindings.yml` builds a wheel and runs the unit tests, and `test-python-examples.yml` runs the scripts in `examples/`. Both build the wheels on a native runner of each platform. + +The package supports four platforms and five Python versions (3.10 to 3.14). That is 20 combinations for each workflow. A pull request does not run all of them, because the organisation shares one runner limit with ArcadeData/arcadedb. Each macOS or Windows job on a pull request delays the upstream jobs too. + +== What runs when + +The script `scripts/select_matrix.py` chooses the combinations. The first job of each workflow, `Download ArcadeDB JARs`, runs it and passes the result to the test job. + +[cols="2,3,3"] +|=== +| Event | Unit tests (`Test arcadedb`) | Examples (`Test Python Examples`) + +| Pull request +| `linux/amd64` on all five Python versions. `linux/arm64`, `darwin/arm64` and `windows/amd64` on Python 3.10 and 3.14. +| All four platforms on Python 3.14. + +| Push to `main`, the nightly run, a manual run, a release +| All 20 combinations. +| All 20 combinations. +|=== + +A pull request runs 21 checks: 15 test jobs (11 and 4), the two `Download ArcadeDB JARs` jobs, the two summary jobs, `Bandit security scan` and `Dependency floor audit`. The full matrix runs 46. + +The names of the test jobs do not change: `Test arcadedb (, Python )`. A check keeps its history when a pull request runs fewer of them. + +A release always runs the full matrix, because `release.yml` expects one wheel for each platform and Python version. The nightly run also tests against the newest `arcadedata/arcadedb:latest` image. + +== Other rules + +* A pull request that changes only documentation (`docs/**`, `*.adoc`, `CLAUDE.md`, `examples/README.md`, `LICENSE*`) runs nothing. +* A draft pull request skips the platform jobs. Marking it ready for review starts them. +* A new push to a pull request cancels the run it replaces. A run on `main` is never cancelled. + +== Change the selection + +Edit `PULL_REQUEST_LEGS` in `scripts/select_matrix.py`. The test `tests/test_select_matrix.py` fixes the full matrix at 20 combinations, so a change that shrinks it fails. When you add a Python version, add it to `PYTHONS` in the script and to the classifiers in `pyproject.toml`. A test checks that both lists are the same. diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index aa89c6c2..add9c4cf 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -87,7 +87,7 @@ Both packages import as `{import-name}`, so an environment holds one of them. Th == Requirements -* Python 3.10 to 3.14. The wheels' CI runs all five versions on every supported platform. +* Python 3.10 to 3.14. The wheels' CI runs all five versions on every supported platform for `main`, the nightly run and each release. A pull request runs a smaller selection (xref:development/ci.adoc[Continuous integration]). * Linux (x86_64 and ARM64), macOS on Apple Silicon, or Windows (x86_64). * No Java installation. diff --git a/docs/modules/ROOT/pages/installation.adoc b/docs/modules/ROOT/pages/installation.adoc index 2f35eeeb..8827d405 100644 --- a/docs/modules/ROOT/pages/installation.adoc +++ b/docs/modules/ROOT/pages/installation.adoc @@ -24,7 +24,7 @@ The package is self-contained. It carries a Java 25 runtime for its platform, so == Requirements -* Python 3.10 to 3.14. The wheels' CI runs all five versions on every supported platform. +* Python 3.10 to 3.14. The wheels' CI runs all five versions on every supported platform for `main`, the nightly run and each release. A pull request runs a smaller selection (xref:development/ci.adoc[Continuous integration]). * One of the four platforms below. Every wheel is platform-specific, built on a native runner of its platform, and carries a JRE for that platform, so pip needs no hint to pick the right one. diff --git a/scripts/select_matrix.py b/scripts/select_matrix.py new file mode 100644 index 00000000..6f03d498 --- /dev/null +++ b/scripts/select_matrix.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Pick the platform x Python legs a CI workflow runs for the event that started it. + +The wheels support four platforms and five Python versions, which is 20 legs per workflow. A +pull request does not need all of them: the organisation shares one runner cap with +ArcadeData/arcadedb, so every extra macOS or Windows leg on a pull request delays upstream too. + +- `pull_request` runs the small matrix below. +- Every other event runs the full matrix: a push to main, the nightly schedule, a manual + dispatch, and the release workflow (which calls the workflows as `workflow_call`; the event + name seen there is the caller's, a tag push). The release needs one wheel per platform and + Python version, and `release.yml` counts them. + +The selection fails open to the full matrix: an event name this script does not know runs +everything, never less. + +usage: + select_matrix.py bindings [--event NAME] # test-python-bindings.yml + select_matrix.py examples [--event NAME] # test-python-examples.yml + +`--event` defaults to $GITHUB_EVENT_NAME. The output is one line of JSON for +`strategy.matrix`: {"include": [{"platform": ..., "runs-on": ..., "python-version": ...}, ...]}. +The names of the jobs stay "Test arcadedb (, Python )", so the history of a +check is comparable before and after the selection. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +# Temporarily limited to four platforms: macOS x86_64 (runner macos-15-intel) and Windows ARM64 +# (windows-11-arm) are off. This is the platform column both workflows had before this script. +RUNNERS = { + "linux/amd64": "ubuntu-24.04", + "linux/arm64": "ubuntu-24.04-arm", + "darwin/arm64": "macos-15", + "windows/amd64": "windows-2025", +} + +# Every Python the package supports. tests/test_select_matrix.py compares this with the +# classifiers of pyproject.toml, so adding a version there without adding it here fails. +PYTHONS = ("3.10", "3.11", "3.12", "3.13", "3.14") +OLDEST = PYTHONS[0] +NEWEST = PYTHONS[-1] + +# The legs of a pull request, per workflow: platform -> Python versions. +# +# bindings: all Pythons on linux/amd64 (the cheap, fast runner and the one most contributors +# use), the oldest and the newest on the three other platforms. A bug that depends on the +# interpreter shows on the ends of the range, and one that depends on the platform shows on +# any version of it. +# +# examples: the newest Python only, on every platform. The examples are slow (torch, +# sentence-transformers), and they carry platform-specific code (`os.name == "nt"` branches in +# examples 07 to 12) and per-platform steps in the workflow (a CPU torch index on macOS, UTF-8 +# on Windows), so all four platforms stay. +PULL_REQUEST_LEGS = { + "bindings": { + "linux/amd64": PYTHONS, + "linux/arm64": (OLDEST, NEWEST), + "darwin/arm64": (OLDEST, NEWEST), + "windows/amd64": (OLDEST, NEWEST), + }, + "examples": {platform: (NEWEST,) for platform in RUNNERS}, +} + + +def _legs(per_platform: dict[str, tuple[str, ...]]) -> list[dict[str, str]]: + return [ + {"platform": platform, "runs-on": RUNNERS[platform], "python-version": python} + for platform in RUNNERS + for python in per_platform.get(platform, ()) + ] + + +def select(workflow: str, event: str) -> dict[str, list[dict[str, str]]]: + """The `strategy.matrix` of `workflow` ("bindings" or "examples") for `event`.""" + if workflow not in PULL_REQUEST_LEGS: + raise ValueError( + f"unknown workflow {workflow!r}; use one of {sorted(PULL_REQUEST_LEGS)}" + ) + if event == "pull_request": + return {"include": _legs(PULL_REQUEST_LEGS[workflow])} + return {"include": _legs({platform: PYTHONS for platform in RUNNERS})} + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("workflow", choices=sorted(PULL_REQUEST_LEGS)) + parser.add_argument( + "--event", + default=os.environ.get("GITHUB_EVENT_NAME", ""), + help="the event that started the workflow (default: $GITHUB_EVENT_NAME)", + ) + args = parser.parse_args(argv) + print(json.dumps(select(args.workflow, args.event), separators=(",", ":"))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_select_matrix.py b/tests/test_select_matrix.py new file mode 100644 index 00000000..a5ae64c5 --- /dev/null +++ b/tests/test_select_matrix.py @@ -0,0 +1,127 @@ +"""Which platform x Python legs CI runs per event: a small matrix on pull requests, the full one everywhere else.""" + +from __future__ import annotations + +import importlib.util +import itertools +import json +import subprocess # nosec B404 - test-controlled child processes, no untrusted input +import sys +import tomllib +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT_PATH = ROOT / "scripts" / "select_matrix.py" +WORKFLOWS = { + "bindings": ROOT / ".github" / "workflows" / "test-python-bindings.yml", + "examples": ROOT / ".github" / "workflows" / "test-python-examples.yml", +} + +# The matrix both workflows had before the selection existed: 4 platforms x 5 Pythons. Written +# out here on purpose, not imported from the script, so the full matrix cannot shrink unnoticed. +PLATFORMS = { + "linux/amd64": "ubuntu-24.04", + "linux/arm64": "ubuntu-24.04-arm", + "darwin/arm64": "macos-15", + "windows/amd64": "windows-2025", +} +PYTHONS = ["3.10", "3.11", "3.12", "3.13", "3.14"] +FULL = {(p, v) for p, v in itertools.product(PLATFORMS, PYTHONS)} +NON_PULL_REQUEST_EVENTS = [ + "push", + "schedule", + "workflow_dispatch", + "workflow_call", + "", + "some_new_event", +] + + +def _load(): + spec = importlib.util.spec_from_file_location("select_matrix", SCRIPT_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules["select_matrix"] = module + spec.loader.exec_module(module) + return module + + +sm = _load() + + +def _pairs(matrix): + legs = matrix["include"] + pairs = [(leg["platform"], leg["python-version"]) for leg in legs] + assert len(pairs) == len(set(pairs)), "a leg is listed twice" + for leg in legs: + assert set(leg) == {"platform", "runs-on", "python-version"} + assert leg["runs-on"] == PLATFORMS[leg["platform"]] + return set(pairs) + + +@pytest.mark.parametrize("workflow", ["bindings", "examples"]) +@pytest.mark.parametrize("event", NON_PULL_REQUEST_EVENTS) +def test_every_event_but_pull_request_runs_the_full_matrix(workflow, event): + matrix = sm.select(workflow, event) + assert len(matrix["include"]) == 20 + assert _pairs(matrix) == FULL + + +def test_pull_request_bindings_matrix(): + pairs = _pairs(sm.select("bindings", "pull_request")) + assert {v for p, v in pairs if p == "linux/amd64"} == set(PYTHONS) + for platform in ("linux/arm64", "darwin/arm64", "windows/amd64"): + assert {v for p, v in pairs if p == platform} == {"3.10", "3.14"}, platform + assert len(pairs) == 11 + + +def test_pull_request_examples_matrix(): + pairs = _pairs(sm.select("examples", "pull_request")) + assert pairs == {(p, "3.14") for p in PLATFORMS} + + +@pytest.mark.parametrize("workflow", ["bindings", "examples"]) +def test_pull_request_matrix_is_part_of_the_full_matrix(workflow): + assert _pairs(sm.select(workflow, "pull_request")) < FULL + + +def test_unknown_workflow_is_refused(): + with pytest.raises(ValueError): + sm.select("docs", "pull_request") + + +def test_pythons_match_the_classifiers_of_pyproject(): + # release.yml expects one wheel per classifier Python and platform from the full matrix; a + # Python added to one place and not the other would fail the release, not a pull request. + prefix = "Programming Language :: Python :: 3." + classifiers = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ + "project" + ]["classifiers"] + assert list(sm.PYTHONS) == [ + c.rsplit(" ", 1)[1] for c in classifiers if c.startswith(prefix) + ] + + +def test_command_line_prints_one_line_of_matrix_json(): + for event, count in (("pull_request", 11), ("push", 20)): + out = subprocess.run( # nosec B603 - fixed argument list, no untrusted input + [sys.executable, "-I", str(SCRIPT_PATH), "bindings", "--event", event], + capture_output=True, + text=True, + check=True, + ).stdout + assert out.count("\n") == 1 + assert len(json.loads(out)["include"]) == count + + +@pytest.mark.parametrize("workflow", sorted(WORKFLOWS)) +def test_workflow_takes_its_matrix_from_the_script(workflow): + text = WORKFLOWS[workflow].read_text(encoding="utf-8") + assert f"scripts/select_matrix.py {workflow}" in text + assert "matrix: ${{ fromJSON(needs.download-jars.outputs.matrix) }}" in text + # the lists are in the script; a copy left in the workflow would say one thing and run another + assert ( + "python-version:\n" not in text.split("strategy:", 1)[1].split("steps:", 1)[0] + ) From 49b7b3ab29e5bc3689c9c002cddfbb99bd08b46d Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 25 Oct 2026 15:50:24 +0900 Subject: [PATCH 2/2] tests: read the pyproject classifiers without tomllib in test_select_matrix tomllib is not in Python 3.10, so the file failed to collect on every 3.10 leg (linux/amd64, linux/arm64, windows/amd64). A regex over the classifier lines does the same check. Co-Authored-By: Claude Sonnet 5.5 --- tests/test_select_matrix.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_select_matrix.py b/tests/test_select_matrix.py index a5ae64c5..618d79bb 100644 --- a/tests/test_select_matrix.py +++ b/tests/test_select_matrix.py @@ -5,9 +5,9 @@ import importlib.util import itertools import json +import re import subprocess # nosec B404 - test-controlled child processes, no untrusted input import sys -import tomllib from pathlib import Path import pytest @@ -95,13 +95,11 @@ def test_unknown_workflow_is_refused(): def test_pythons_match_the_classifiers_of_pyproject(): # release.yml expects one wheel per classifier Python and platform from the full matrix; a # Python added to one place and not the other would fail the release, not a pull request. - prefix = "Programming Language :: Python :: 3." - classifiers = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ - "project" - ]["classifiers"] - assert list(sm.PYTHONS) == [ - c.rsplit(" ", 1)[1] for c in classifiers if c.startswith(prefix) - ] + # A regex, not tomllib: this test also runs on Python 3.10, which has no tomllib. + text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + classifiers = re.findall(r'"Programming Language :: Python :: (3\.\d+)"', text) + assert classifiers, "no Python version classifiers found in pyproject.toml" + assert list(sm.PYTHONS) == classifiers def test_command_line_prints_one_line_of_matrix_json():