diff --git a/.github/workflows/release-native.yml b/.github/workflows/release-native.yml index fb3b4ed..25acd33 100644 --- a/.github/workflows/release-native.yml +++ b/.github/workflows/release-native.yml @@ -5,7 +5,8 @@ name: Release native (arcadedb) # starts release-jvm.yml, which publishes `arcadedb-jvm`; the two workflows do not wait for each other. # # The build and the tests are test-native.yml, called with `release: true` (the full matrix: linux-x86_64, linux-aarch64, -# macos-arm64 and windows-x64, each on Python 3.11 to 3.14). A native image build takes about 5 to 30 minutes per platform; this workflow adds +# macos-arm64 and windows-x64, each on Python 3.11 to 3.14, and the `pyver` job: the linux-x86_64 wheel on Python 3.15 and on the +# free-threaded 3.14t). A native image build takes about 5 to 30 minutes per platform; this workflow adds # no build of its own. The way the JVM release calls test-jvm.yml is the model. # # PyPI keys a trusted publisher on this FILENAME and on the job's environment, so do not rename either without changing the publisher @@ -15,7 +16,8 @@ name: Release native (arcadedb) # The GitHub environment `pypi` only accepts deployments from tags that match [0-9]*. There is one environment and one publish job for # dev, final and post releases. # -# What is published (the `verify` job decides, the `publish` job uploads): +# What is published (the `verify` job decides, the `publish` job uploads). Every kind of release also needs the two `pyver` legs to have +# passed (3.15 and 3.14t on the linux-x86_64 wheel; the other platforms are not tested on them): # A.B.C.devN the parity run of linux-x86_64, linux-aarch64 and macos-arm64 passed on every Python. windows-x64 is published too when it # passed on every Python, and is informational otherwise. # A.B.C, A.B.C.postN ONLY when the readiness bar is met: the repository variable ARCADEDB_NATIVE_READINESS_BAR_MET is `true` (a maintainer @@ -135,7 +137,7 @@ jobs: path: wheels merge-multiple: true - - name: Every required platform passed on every Python, and the wheels are the ones that were tested + - name: Every required platform passed on every Python, the pyver legs passed, and the wheels are the ones that were tested id: verify env: VERSION: ${{ needs.validate-version.outputs.version }} @@ -146,6 +148,8 @@ jobs: mkdir -p passed dist ls -l passed wheels pythons="3.11 3.12 3.13 3.14" + # the `pyver` legs of a release (scripts/select_native_matrix.py; tests/test_select_native_matrix.py checks that this list is the same) + pyver_pythons="3.15 3.14t" if [ "$FINAL" = true ]; then required="linux-x86_64 linux-aarch64 macos-arm64 windows-x64" if [ "$BAR" != true ]; then @@ -160,6 +164,12 @@ jobs: [ -f "passed/$key-py$py.ok" ] || { echo "::error::required leg $key on Python $py did not pass its parity run: nothing is published" >&2; exit 1; } done done + # every release needs the `pyver` legs too, and they must have installed the wheel of linux-x86_64 that is published + tested="$(cat "passed/linux-x86_64-py3.12.ok")" + for py in $pyver_pythons; do + [ -f "passed/pyver-py$py.ok" ] || { echo "::error::the pyver leg on Python $py did not pass: nothing is published" >&2; exit 1; } + [ "$(cat "passed/pyver-py$py.ok")" = "$tested" ] || { echo "::error::the pyver leg on Python $py tested another wheel than $tested: nothing is published" >&2; exit 1; } + done # every platform that passed on every Python is published (Windows is informational for a pre-release) for key in linux-x86_64 linux-aarch64 macos-arm64 windows-x64; do all=true diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index 3515050..d6622be 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -13,7 +13,8 @@ # # Beside the full parity runs, the `pyver` job installs the already built linux-x86_64 wheel (py3-none: no rebuild) into the Pythons the full # runs of the event leave out (a pull request: 3.11, 3.13, 3.14; main, nightly, dispatch: 3.11, 3.13; a release: none), into 3.15 (always: the full suite -# needs jpype1 and ladybug, which have no cp315 wheel yet, so 3.15 runs without the test files that need jpype1) and into the free-threaded 3.14t, and runs a small subset of the tests there (about 20 s of tests per Python). It is informational: release-native.yml does not need it. +# needs jpype1 and ladybug, which have no cp315 wheel yet, so 3.15 runs without the test files that need jpype1) and into the free-threaded 3.14t, and runs a small subset of the tests there (about 20 s of tests per Python). A release needs it: a leg that passes uploads a marker (native-passed-pyver-py), and release-native.yml +# publishes nothing unless the legs of a release (3.15 and the free-threaded 3.14t) passed. Only linux-x86_64 is checked on them. # scripts/select_native_matrix.py decides its legs. # # The `examples` job runs every script of examples/ against the installed wheel of each platform (Python 3.12 and 3.14; a pull request: linux-x86_64, 3.12), through @@ -31,8 +32,9 @@ # test suite of this repository run against the INSTALLED wheel (native/wheel/parity_wheel.py): no failure, error, crash, timeout, or skip # besides the counted NOTYET and NA-JVM kinds (tests/conftest.py). # -# A leg that passes uploads a marker (native-passed--py) and the wheel (native-wheel-); release-native.yml -# reads them to decide what may be published. Do not rename these artifacts without changing release-native.yml. +# A leg that passes uploads a marker (native-passed--py) and the wheel (native-wheel-); a `pyver` leg that +# passes uploads a marker too (native-passed-pyver-py). release-native.yml reads them to decide what may be published. Do not +# rename these artifacts without changing release-native.yml. # # Crashes (ArcadeData/arcadedb-python#295): a test file whose process dies without a result is a CRASH row with its exit code, and fails the leg. # On the Windows legs only, the harness runs such a file once more in a fresh process (--retry-crash): a retry that produces a result is a @@ -968,8 +970,9 @@ jobs: # The cheap Python-version coverage of the wheel of linux-x86_64 (py3-none: the same file for every Python, nothing is rebuilt): a subset of # the test files, not the whole suite. The legs are the supported Pythons the full parity runs of this event leave out, plus the # free-threaded 3.14t (scripts/select_native_matrix.py). - # INFORMATIONAL for now: release-native.yml (verify and the publish jobs) reads only the native-passed-* markers and the native-wheel-* wheels, - # never an artifact of this job, so a red leg never blocks a release. Promote it once it has been green for a while. + # Part of the release gate (ArcadeData/arcadedb-python#349): a leg that passes uploads the marker native-passed-pyver-py (the file name of + # the wheel it installed), and `verify` in release-native.yml requires the markers of the legs a release has (3.15 and 3.14t, see + # scripts/select_native_matrix.py), so a red leg stops the release. Only the linux-x86_64 wheel is checked on these Pythons. # 3.14t runs without polars, and so without test_dataframe_ingest.py (it imports polars at module level): polars ships abi3 wheels only # (polars and polars-runtime-32 have no cp314t wheel, and an abi3 wheel cannot be installed on a free-threaded build). The other # dependencies of the subset (numpy, pandas, pyarrow, jpype1, requests) all have cp314t wheels. polars is an optional extra of the wheel. @@ -1090,6 +1093,21 @@ jobs: if (skipped + 0 != 0) { print "skipped tests that are neither NOTYET nor NA-JVM: " skipped > "/dev/stderr"; exit 1 } }' "$tsv" | tee -a "$GITHUB_STEP_SUMMARY" + # Reached only when the gate passed: release-native.yml reads this marker (one per Python, holding the wheel file name). + - name: Mark this leg as passed + run: | + set -euo pipefail + mkdir -p "$T/passed" + basename "$(ls "$T"/wheel/*.whl)" > "$T/passed/pyver-py$PY.ok" + cat "$T/passed/pyver-py$PY.ok" + + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: native-passed-pyver-py${{ matrix.python }} + path: ${{ runner.temp }}/passed/pyver-py${{ matrix.python }}.ok + if-no-files-found: error + retention-days: 30 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: ${{ !cancelled() }} with: diff --git a/docs/modules/ROOT/pages/development/ci.adoc b/docs/modules/ROOT/pages/development/ci.adoc index eefbda0..0f4aa0c 100644 --- a/docs/modules/ROOT/pages/development/ci.adoc +++ b/docs/modules/ROOT/pages/development/ci.adoc @@ -101,7 +101,7 @@ The workflow `test-native.yml` builds the native image of the `arcadedb` package * A pull request runs it only when native paths change: `+native/**+`, `+src/arcadedb/**+`, `+tests/**+`, `+examples/**+` (not `examples/README.md`), `pyproject.toml`, `setup.py`, `scripts/select_native_matrix.py`, `scripts/smoke_wheel.py`, `scripts/run_examples.sh`, `scripts/native_examples_skip.txt` or `test-native.yml` itself. Changes under `tests/` and `src/arcadedb/` are included because both backends share those files, so a shared test or source can break native parity without touching a native file; docs outside those paths do not run it. It then runs on `linux-x86_64` with Python 3.12 only. * A push to `main` (only when a native path changed, same filter as a pull request), the nightly run (skipped when nothing changed in the last 25 hours, except on Mondays) and a manual run (dispatch) run all four platforms: `linux-x86_64`, `linux-aarch64`, `macos-arm64` and `windows-x64` (Python 3.12 and 3.14). A release (`release-native.yml` calls the workflow with `release: true`) runs the same four platforms on all four Python versions, 16 legs. -* The `pyver` job installs the already built `linux-x86_64` wheel (no rebuild) into the Pythons that the full parity runs of the event leave out (3.11, 3.13 and 3.14 on a pull request; 3.11 and 3.13 on a push to `main`, the nightly run and a dispatch; none on a release) into 3.15 and into the free-threaded 3.14t, and runs 13 test files there (14 where polars installs; 3.15 has no jpype1 wheel, so it leaves out the two files that need jpype1), about 20 seconds of tests per Python. It is informational: `release-native.yml` reads only the `native-passed-*` markers and the `native-wheel-*` wheels, so a red `pyver` leg never blocks a release. The 3.14t leg has no polars (no free-threaded wheel exists), so it skips `test_dataframe_ingest.py`. `scripts/select_native_matrix.py` decides its legs. +* The `pyver` job installs the already built `linux-x86_64` wheel (no rebuild) into the Pythons that the full parity runs of the event leave out (3.11, 3.13 and 3.14 on a pull request; 3.11 and 3.13 on a push to `main`, the nightly run and a dispatch; none on a release) into 3.15 and into the free-threaded 3.14t, and runs 13 test files there (14 where polars installs; 3.15 has no jpype1 wheel, so it leaves out the two files that need jpype1), about 20 seconds of tests per Python. A release needs it: a `pyver` leg that passes uploads the marker `native-passed-pyver-py` (it holds the file name of the wheel the leg installed), and `Verify what may be published` in `release-native.yml` requires the markers of 3.15 and 3.14t, the two legs of a release, and that both tested the `linux-x86_64` wheel that is published. A red leg stops a release before anything reaches PyPI. Only `linux-x86_64` is checked on these two Pythons: the other platforms are not tested on 3.15 and 3.14t, and the JVM package has no such job. The 3.14t leg has no polars (no free-threaded wheel exists), so it skips `test_dataframe_ingest.py`. `scripts/select_native_matrix.py` decides its legs, and `tests/test_select_native_matrix.py` and `tests/test_release_native_verify.py` pin the gate. * Every test file runs in its own process (`native/wheel/parity_wheel.py`). A process that dies without writing its result is a `CRASH` row in `parity-wheel.tsv` with its exit code, for example `CRASH(rc=0xC0000005,3221225477)`, and it fails the leg. On the Windows legs only (the crashes of issue 295 happen there), the harness runs such a file once more in a fresh process (`--retry-crash`). A retry that produces a result is a `FLAKY-CRASH` row: the gate lets the leg pass, the job summary lists the file with the first exit code, and on a release or a dispatch the `crash-report` job adds one comment to issue 295 (run URL, Python, wheel version, exit codes). A second crash, a failing test, a timeout and every crash on Linux or macOS stay failures. A process that wrote its result and then died in shutdown is a `DIRTY-EXIT` record (exit code, file, attempt) in the metadata, the job summary and the issue comment: it is not retried and does not fail the leg. Each Windows test process also writes a faulthandler trace, `parity-wheel-xml/.attemptN.fault.txt`, into the parity artifact of the leg. Only a retry hides a crash, so treat a `FLAKY-CRASH` as an open bug: read the trace in the artifact before a release is announced. * The `examples` job runs every script of `examples/` against the installed wheel of each platform (Python 3.12 and 3.14; a pull request: `linux-x86_64`, Python 3.12) through `scripts/run_examples.sh`, the runner that `test-python-examples.yml` uses too. `scripts/native_examples_skip.txt` lists, with a reason, the examples that need the JVM package. `Examples summary` is the one check for all its legs. These legs do not decide what a release publishes. * A native image build takes about 5 to 30 minutes for each platform (macOS is the slowest). The organisation shares one runner limit with upstream ArcadeDB, so do not add runs lightly. Change the paths or the matrix of `test-native.yml` only for a reason that you can write down. diff --git a/docs/modules/ROOT/pages/development/release.adoc b/docs/modules/ROOT/pages/development/release.adoc index 08e3faa..652e538 100644 --- a/docs/modules/ROOT/pages/development/release.adoc +++ b/docs/modules/ROOT/pages/development/release.adoc @@ -101,13 +101,14 @@ The version rule above holds for both packages. One tag starts both release work `release-native.yml` has four jobs: . `Validate tag` checks that the tag is a bare version and the next one (`scripts/check_version.py`). -. `test` calls `test-native.yml` with the full matrix: four platforms, each on Python 3.11 to 3.14. It builds the native image and the wheel on each platform and runs the test suite of this repository against the installed wheel. +. `test` calls `test-native.yml` with the full matrix: four platforms, each on Python 3.11 to 3.14. It builds the native image and the wheel on each platform and runs the test suite of this repository against the installed wheel. It also runs the `pyver` job: a subset of the tests against the `linux-x86_64` wheel on Python 3.15 and on the free-threaded 3.14t. . `Verify what may be published` reads which legs passed, applies the readiness gate below and collects the wheels. . `Publish to PyPI` uploads the wheels through the environment `pypi`. It refuses any wheel that is not the distribution `arcadedb`. `arcadedb` has one trusted publisher on PyPI (`release-native.yml` with the environment `pypi`) for dev, final and post releases, and one publish job in the workflow. `arcadedb-jvm` has one trusted publisher (`release-jvm.yml` with the environment `pypi-jvm`). PyPI matches the workflow file name, so renaming either file means changing the publisher on PyPI first. There is no separate pre-release environment; the readiness gate below is what keeps a final release back. * A native pre-release (`A.B.C.devN`) publishes when the builds on `linux-x86_64`, `linux-aarch64` and `macos-arm64` pass. +* Every native release, pre-release or final, also needs the two `pyver` legs (Python 3.15 and 3.14t on the `linux-x86_64` wheel) to pass. The other platforms are not tested on those two Pythons. * A FINAL native release (`A.B.C` or `A.B.C.postN`) needs two more things: ** The repository variable `ARCADEDB_NATIVE_READINESS_BAR_MET` is `true`. A maintainer sets it after checking the readiness bar: the correctness fixes are merged; the full suite is green on every platform, Windows included, and on Python 3.11 to 3.14; the fault and concurrency probes pass; and the documented API raises no `NotOnNativeYet`. ** The parity run of every platform and every Python version passes with zero `NotOnNativeYet` skips. diff --git a/native/wheel/README.md b/native/wheel/README.md index ce528d8..65161f9 100644 --- a/native/wheel/README.md +++ b/native/wheel/README.md @@ -62,7 +62,7 @@ Intel macOS, Windows arm64 and musl-based Linux (Alpine) are not built and not s - **JVM-only APIs do not exist**: `get_java_database()` and the other `get_java_*` calls, `start_jvm()`, `jvm_kwargs` and JVM options. There is no JVM. Use `arcadedb-jvm` if you need them. - **A few API calls raise `NotOnNativeYet`** (a `NotImplementedError`) instead of misbehaving: `AsyncExecutor.transaction`, `find_nearest_approximate` over product quantization, `get_metadata` of a foreign index, and `get_stats`. -- Free-threaded Python (3.14t) runs a subset of the tests in CI; it is informational and does not gate a release. +- Python 3.15 and free-threaded Python (3.14t) run a subset of the tests in CI, on the `linux-x86_64` wheel only; a release needs both to pass. The other platforms are not tested on them. The bindings' own tests run against the installed wheel (the parity run). What does not pass is the list above, or JVM-only by nature. diff --git a/scripts/select_native_matrix.py b/scripts/select_native_matrix.py index 016c314..a106de9 100644 --- a/scripts/select_native_matrix.py +++ b/scripts/select_native_matrix.py @@ -11,8 +11,8 @@ Prints `build=`, `test=`, `pyver=` and `examples=` lines (GITHUB_OUTPUT format). Publishing is not decided here: release-native.yml does it. `pyver` is the cheap Python-version job (see PYVER_NOTE): a subset of the tests against the built -linux-x86_64 wheel on the Pythons the full parity run of the event does not cover, and always on the free-threaded 3.14t. It never -gates a release. The GraalVM CE archives are pinned by sha256 per platform (GraalVM Community Edition only, never Oracle GraalVM). +linux-x86_64 wheel on the Pythons the full parity run of the event does not cover, and always on the free-threaded 3.14t. A release +needs its legs to pass (release-native.yml, `verify`). The GraalVM CE archives are pinned by sha256 per platform (GraalVM Community Edition only, never Oracle GraalVM). """ from __future__ import annotations @@ -100,7 +100,7 @@ # full parity run of the event does not cover, and runs a subset of the test files there. 3.14t is the free-threaded build of 3.14: # it is not a classifier of the wheel and has no polars (polars ships abi3 wheels only, which a free-threaded Python cannot install). FREE_THREADED_PYTHONS = ["3.14t"] -PYVER_NOTE = "informational: release-native.yml does not read it" +PYVER_NOTE = "gates a release: release-native.yml requires a native-passed-pyver-py marker for each leg of a release" PULL_REQUEST_PLATFORMS = ["linux-x86_64"] PULL_REQUEST_PYTHONS = ["3.12"] diff --git a/tests/conftest.py b/tests/conftest.py index 4a862a0..fd7402f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,6 +39,8 @@ "test_dev_engine_pin.py": "runs the bash code of a workflow step", # runs the awk program of the parity gate step of test-native.yml, which is code for the bash of a runner "test_parity_gate_awk.py": "runs the workflows' awk step code", + # runs the bash code of the verify step of release-native.yml on small passed/ and wheels/ directories (#349) + "test_release_native_verify.py": "runs the bash code of a workflow step", } if sys.platform == "win32": collect_ignore.extend(WINDOWS_NOT_APPLICABLE) diff --git a/tests/test_release_native_verify.py b/tests/test_release_native_verify.py new file mode 100644 index 0000000..4ea5756 --- /dev/null +++ b/tests/test_release_native_verify.py @@ -0,0 +1,140 @@ +"""The `verify` job of release-native.yml publishes nothing unless the `pyver` legs passed too (ArcadeData/arcadedb-python#349). + +The job's check is bash inside the workflow file. These tests cut the shipped step out of release-native.yml and run it with bash on small +`passed/` and `wheels/` directories, as the job finds them after the artifact download, so the code that ships is the code that is tested. +The file is not collected on Windows (tests/conftest.py WINDOWS_NOT_APPLICABLE): the step runs in the bash of a Linux runner. +""" + +from __future__ import annotations + +import os +import subprocess # nosec B404 - test-controlled child processes, no untrusted input +import textwrap +from pathlib import Path + +import pytest + +REPO = Path(__file__).resolve().parents[1] +RELEASE = REPO / ".github" / "workflows" / "release-native.yml" +STEP = "- name: Every required platform passed on every Python" + +PLATFORM_TAGS = { + "linux-x86_64": "manylinux_2_28_x86_64", + "linux-aarch64": "manylinux_2_28_aarch64", + "macos-arm64": "macosx_11_0_arm64", + "windows-x64": "win_amd64", +} +PYTHONS = ["3.11", "3.12", "3.13", "3.14"] +PYVER_PYTHONS = ["3.15", "3.14t"] + + +def _script() -> str: + """The `run: |` block of the verify step, dedented.""" + lines = RELEASE.read_text(encoding="utf-8").splitlines() + start = next(i for i, line in enumerate(lines) if line.strip().startswith(STEP)) + begin = next(i for i in range(start, len(lines)) if lines[i].strip() == "run: |") + indent = len(lines[begin]) - len(lines[begin].lstrip()) + body = [] + for line in lines[begin + 1 :]: + if line.strip() and len(line) - len(line.lstrip()) <= indent: + break + body.append(line) + return textwrap.dedent("\n".join(body)) + "\n" + + +def _wheel(key: str, version: str) -> str: + return f"arcadedb-{version}-py3-none-{PLATFORM_TAGS[key]}.whl" + + +def _stage(root: Path, version: str, drop=(), pyver_wheel: str | None = None) -> None: + """What the job finds after the downloads: a marker per platform and Python, the wheels, and a marker per pyver leg.""" + passed, wheels = root / "passed", root / "wheels" + passed.mkdir() + wheels.mkdir() + for key in PLATFORM_TAGS: + name = _wheel(key, version) + (wheels / name).write_bytes(b"wheel") + for py in PYTHONS: + (passed / f"{key}-py{py}.ok").write_text(name + "\n") + for py in PYVER_PYTHONS: + (passed / f"pyver-py{py}.ok").write_text( + (pyver_wheel or _wheel("linux-x86_64", version)) + "\n" + ) + for marker in drop: + (passed / marker).unlink() + + +def _verify(root: Path, version: str, final: bool): + output = root / "github_output" + output.write_text("") + env = { + **os.environ, + "VERSION": version, + "FINAL": "true" if final else "false", + "BAR": "true", + "GITHUB_OUTPUT": str(output), + } + proc = subprocess.run( # nosec B603 B607 - fixed argv, the script is this repository's own workflow step + ["bash", "-c", _script()], + cwd=root, + env=env, + capture_output=True, + text=True, + timeout=60, + ) + dist = sorted(p.name for p in (root / "dist").glob("*.whl")) + return proc, output.read_text(), dist + + +KINDS = [("26.99.1.dev0", False), ("26.99.1", True), ("26.99.1.post1", True)] + + +@pytest.mark.parametrize("version,final", KINDS) +def test_every_leg_passed_the_release_is_verified_and_all_four_wheels_are_collected( + tmp_path, version, final +): + _stage(tmp_path, version) + proc, output, dist = _verify(tmp_path, version, final) + assert proc.returncode == 0, proc.stderr + assert "ok=true" in output + assert dist == sorted(_wheel(key, version) for key in PLATFORM_TAGS) + + +@pytest.mark.parametrize("version,final", KINDS) +@pytest.mark.parametrize("py", PYVER_PYTHONS) +def test_a_pyver_leg_that_did_not_pass_stops_every_kind_of_release( + tmp_path, version, final, py +): + # a failed leg uploads no marker (test-native.yml reaches the marker step only after the gate), so the marker is missing + _stage(tmp_path, version, drop=[f"pyver-py{py}.ok"]) + proc, output, dist = _verify(tmp_path, version, final) + assert proc.returncode != 0, "the release went on although a pyver leg did not pass" + assert f"pyver leg on Python {py} did not pass" in proc.stderr + assert "ok=true" not in output + assert dist == [] + + +def test_a_pyver_leg_that_tested_another_wheel_than_the_published_one_stops_the_release( + tmp_path, +): + version = "26.99.1.dev0" + _stage( + tmp_path, + version, + pyver_wheel=f"arcadedb-26.99.0-py3-none-{PLATFORM_TAGS['linux-x86_64']}.whl", + ) + proc, output, dist = _verify(tmp_path, version, final=False) + assert proc.returncode != 0 + assert "tested another wheel" in proc.stderr + assert "ok=true" not in output + assert dist == [] + + +def test_a_platform_leg_that_did_not_pass_still_stops_the_release(tmp_path): + # the fixture is good enough to be verified, so the refusal above comes from the pyver check and not from a broken fixture + version = "26.99.1.dev0" + _stage(tmp_path, version, drop=["macos-arm64-py3.13.ok"]) + proc, output, dist = _verify(tmp_path, version, final=False) + assert proc.returncode != 0 + assert "required leg macos-arm64 on Python 3.13" in proc.stderr + assert "ok=true" not in output diff --git a/tests/test_select_native_matrix.py b/tests/test_select_native_matrix.py index 2de4a71..0a4ca31 100644 --- a/tests/test_select_native_matrix.py +++ b/tests/test_select_native_matrix.py @@ -1,5 +1,6 @@ """The legs of the native test workflow per event: a pull request must stay small (the org shares a runner cap with upstream).""" +import fnmatch import importlib.util import json import re @@ -124,20 +125,55 @@ def test_the_python_version_job_never_repeats_a_full_parity_leg_and_has_no_polar assert leg["polars"] is (not leg["freethreaded"]), leg -def test_the_release_does_not_need_the_python_version_job(): +def _job(text, name): + """The text of one job of a workflow file (a job starts at two spaces of indentation).""" + parts = re.split( + r"^ ([A-Za-z0-9_-]+):\n", text.split("\njobs:\n", 1)[1], flags=re.M + ) + return dict(zip(parts[1::2], parts[2::2]))[name] + + +def test_a_release_needs_a_marker_from_every_leg_of_the_python_version_job(): + # ArcadeData/arcadedb-python#349: the pyver job was informational (a red leg could not stop a release); now a leg that passes uploads a + # marker and verify in release-native.yml requires the markers of the legs a release has. The bash of verify is run in + # tests/test_release_native_verify.py; here the two workflow files and the selector are checked against one another. workflows = ROOT / ".github" / "workflows" test_native = (workflows / "test-native.yml").read_text() release = (workflows / "release-native.yml").read_text() - assert re.search(r"^ pyver:\n", test_native, re.M) - # no job of either workflow waits for it - for text in (test_native, release): - for needs in re.findall(r"^ needs: (.*)$", text, re.M): - assert "pyver" not in needs, needs - # release-native.yml reads the markers and the wheels only, never the artifacts of the pyver job + pyver = _job(test_native, "pyver") + # the marker is written and uploaded after the gate step and by steps that run on success only, and a missing file fails the upload + gate = pyver.find("Gate: no failure, error, crash, timeout or uncounted skip") + mark = pyver.find("- name: Mark this leg as passed") + assert 0 <= gate < mark, "the pyver job has no marker step after its gate" + upload = re.search( + r" - uses: actions/upload-artifact@\S+ # v[0-9.]+\n" + r" with:\n" + r" name: native-passed-pyver-py\$\{\{ matrix\.python \}\}\n" + r" path: \$\{\{ runner\.temp \}\}/passed/pyver-py\$\{\{ matrix\.python \}\}\.ok\n" + r" if-no-files-found: error\n", + pyver, + ) + assert upload, "the pyver job uploads no marker native-passed-pyver-py" + assert gate < mark < upload.start() + marker_step = pyver[mark : upload.start()] + assert "if:" not in marker_step and 'pyver-py$PY.ok"' in marker_step + # verify requires the marker of every leg the selector gives a release, under the file name the upload has + chosen = re.search(r'^\s+pyver_pythons="([^"]*)"$', release, re.M) + assert chosen, "verify names no pyver legs" + assert chosen.group(1).split() == [ + leg["python"] for leg in sel.select("release")["pyver"] + ] + assert '"passed/pyver-py$py.ok"' in release + # the marker artifact is downloaded by a pattern verify already has; the artifacts of the parity runs of this job are not patterns = re.findall(r"^\s+pattern: (.*)$", release, re.M) assert patterns and all( p in ("native-passed-*", "native-wheel-*") for p in patterns ), patterns + assert fnmatch.fnmatch("native-passed-pyver-py3.14t", "native-passed-*") + # nothing calls the job informational any more + assert "informational" not in sel.PYVER_NOTE.lower() + assert "It is informational" not in test_native + assert "INFORMATIONAL for now" not in test_native def test_the_linux_legs_build_in_the_manylinux_container_of_the_tag_and_the_workflow_gates_that_floor():