From 891960382459d7a998d7985ce9c5fad6511b2bf2 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Mon, 26 Oct 2026 22:05:54 +0900 Subject: [PATCH] CI: test-python-examples builds the JVM wheel once for each platform Since #92 the JVM wheel is py3-none-, the same file for every Python, but each of the 16 legs of test-python-examples.yml still built it. On push to main, the nightly run, dispatch and the release that was 16 identical builds, 12 of them wasted on macOS, Windows and Linux runners the organisation shares with ArcadeData/arcadedb. A `build` job now builds the wheel once for each platform (select_matrix.py examples --wheels, the shape of test-jvm.yml), checks the tag, and uploads it as examples-wheel- (not wheel-: release-jvm.yml publishes every wheel-* artifact, and the release runs both workflows in one run). Each Test Python Examples leg downloads the wheel of its platform and installs it; it no longer sets up Java or Docker Buildx, downloads the jars, or installs build tools. The leg names, the examples matrix (newest Python on each platform on a pull request, 16 legs otherwise) and the examples run are unchanged. build.sh still checks the size, licences and tag of the wheel; the tag check is repeated in the job as in test-jvm.yml. Counts in ci.adoc and CLAUDE.md follow: a pull request runs 28 checks (was 24), the other events 40 (was 36). A new test pins the build job, the wheel download and the artifact name. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/test-python-examples.yml | 128 ++++++++++++++++---- CLAUDE.md | 2 +- docs/modules/ROOT/pages/development/ci.adoc | 6 +- scripts/select_matrix.py | 1 + tests/test_select_matrix.py | 21 ++++ 5 files changed, 132 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test-python-examples.yml b/.github/workflows/test-python-examples.yml index f7ba78f..27da294 100644 --- a/.github/workflows/test-python-examples.yml +++ b/.github/workflows/test-python-examples.yml @@ -1,3 +1,5 @@ +# Runs examples/ against the `arcadedb-jvm` wheel. The wheel is py3-none- (setup.py): the `build` job builds it once for each +# platform, and the `test-examples` job installs that one file into each Python and runs the examples. Nothing is built per Python. name: Test Python Examples on: @@ -115,6 +117,7 @@ jobs: outputs: arcadedb-tag: ${{ steps.jars.outputs.arcadedb-tag }} matrix: ${{ steps.matrix.outputs.matrix }} + wheel-matrix: ${{ steps.matrix.outputs.wheel-matrix }} steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -122,6 +125,8 @@ jobs: # 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. + # The wheel matrix is the platforms of those legs, one build each: the wheel is + # py3-none-, the same file for every Python. # Done first, so a failure here costs no Docker pull. - name: Select the test matrix id: matrix @@ -130,8 +135,11 @@ jobs: EVENT_NAME: ${{ github.event_name }} run: | matrix="$(python3 scripts/select_matrix.py examples --event "$EVENT_NAME")" + wheel_matrix="$(python3 scripts/select_matrix.py examples --event "$EVENT_NAME" --wheels)" echo "$matrix" + echo "$wheel_matrix" echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + echo "wheel-matrix=$wheel_matrix" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 @@ -176,28 +184,22 @@ jobs: path: src/arcadedb/jars/*.jar retention-days: 1 - # Second job: Test examples on each platform - test-examples: - name: Test Python Examples (${{ matrix.platform }}, Python ${{ matrix.python-version }}) + # Second job: build the wheel of each platform, once. The wheel is py3-none- (setup.py), so the file built here is + # the one every Python of the test-examples job below installs: nothing is built per Python. build.sh takes a Python only for the + # Linux container that builds and smoke-tests the wheel; any of them gives the same file. It also checks the size, the licences + # and the tag of the wheel, as in test-jvm.yml. + build: + name: Build wheel for examples (${{ matrix.platform }}) runs-on: ${{ matrix.runs-on }} needs: download-jars if: github.event_name != 'pull_request' || github.event.pull_request.draft == false env: 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 the full matrix is 16 jobs, so - # uncached that is 16 independent model downloads per release and 16 - # chances for a transient hub outage to fail a release. It has happened. - HF_HOME: ${{ github.workspace }}/.hf-cache strategy: fail-fast: false - # 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 4 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) }} + # One leg per platform of the test matrix, from the "Select the test matrix" step of download-jars + # (scripts/select_matrix.py --wheels): {"include": [{"platform": ..., "runs-on": ...}, ...]}. + matrix: ${{ fromJSON(needs.download-jars.outputs.wheel-matrix) }} steps: - name: Checkout code @@ -223,7 +225,7 @@ jobs: - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: ${{ matrix.python-version }} + python-version: "3.12" - name: Setup UV package manager shell: bash @@ -242,6 +244,13 @@ jobs: run: | uv pip install --system build wheel setuptools + - name: Set UTF-8 encoding (Windows only) + if: matrix.platform == 'windows/amd64' || matrix.platform == 'windows/arm64' + shell: bash + run: | + echo "PYTHONIOENCODING=utf-8" >> $GITHUB_ENV + echo "PYTHONUTF8=1" >> $GITHUB_ENV + # Windows currently enabled, no symlink needed # - name: Create python3 symlink (Windows only) # if: matrix.platform == 'windows/amd64' || matrix.platform == 'windows/arm64' @@ -258,17 +267,92 @@ jobs: # the Docker build takes its base image by this tag, the jars from the artifact ARCADEDB_TAG: ${{ needs.download-jars.outputs.arcadedb-tag }} run: | - echo "🔨 Building arcadedb for ${{ matrix.platform }} with Python ${{ matrix.python-version }}..." + echo "🔨 Building arcadedb-jvm for ${{ matrix.platform }}..." # The jars download-jars fetched, on every platform, so all wheels of this # run carry the same engine build (and build.sh checks the integration jar) - ./scripts/build.sh ${{ matrix.platform }} ${{ matrix.python-version }} src/arcadedb/jars + ./scripts/build.sh ${{ matrix.platform }} 3.12 src/arcadedb/jars + + # build.sh already ran this (and the size check); it is repeated here as in test-jvm.yml, so the tag shows in this job's log and + # summary and a change to build.sh cannot drop it unnoticed. The wheel must be py3-none-, the file every Python installs. + - name: Check the wheel tag + shell: bash + run: | + python scripts/verify_wheel_tag.py dist/*.whl + { + echo "## 📦 Wheel: arcadedb-jvm (${{ matrix.platform }})" + echo + for w in dist/*.whl; do + echo "- \`$(basename "$w")\`, $(du -m "$w" | cut -f1) MB" + done + } >> "$GITHUB_STEP_SUMMARY" + + # One wheel per platform, for the test-examples legs of that platform. Named examples-wheel-, not wheel-: + # a release runs this workflow and test-jvm.yml in one run, and release-jvm.yml publishes every wheel-* artifact. + - name: Upload wheel artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: examples-wheel-${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || matrix.platform == 'linux/arm64' && 'linux-arm64' || matrix.platform == 'darwin/amd64' && 'darwin-amd64' || matrix.platform == 'darwin/arm64' && 'darwin-arm64' || matrix.platform == 'windows/amd64' && 'windows-amd64' || 'windows-arm64' }} + path: dist/*.whl + if-no-files-found: error + retention-days: 1 + + # Third job: install the wheel of the platform into each Python and run the examples. Nothing is built here: the wheel is + # the file the build job made (py3-none-), so each Python runs the examples against the same file that is published. + test-examples: + name: Test Python Examples (${{ matrix.platform }}, Python ${{ matrix.python-version }}) + runs-on: ${{ matrix.runs-on }} + needs: [download-jars, build] + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + env: + # 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 the full matrix is 16 jobs, so + # uncached that is 16 independent model downloads per release and 16 + # chances for a transient hub outage to fail a release. It has happened. + HF_HOME: ${{ github.workspace }}/.hf-cache + strategy: + fail-fast: false + # 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 4 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 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup UV package manager + shell: bash + run: | + # Retry transient network failures (a connection reset failed a whole examples job on #8897); the + # script is downloaded first so a retried transfer is never piped half-written into sh. + installer="$(mktemp)" + curl -LsSf --retry 5 --retry-all-errors --retry-delay 5 -o "$installer" https://astral.sh/uv/install.sh + sh "$installer" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.local/bin:$PATH" + uv --version + + # The wheel of this platform, built once by the build job and the same file for every Python. + - name: Download the wheel of ${{ matrix.platform }} + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: examples-wheel-${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || matrix.platform == 'linux/arm64' && 'linux-arm64' || matrix.platform == 'darwin/amd64' && 'darwin-amd64' || matrix.platform == 'darwin/arm64' && 'darwin-arm64' || matrix.platform == 'windows/amd64' && 'windows-amd64' || 'windows-arm64' }} + path: test-install # Note: Java is NOT required - arcadedb has bundled JRE! - name: Install ArcadeDB Python bindings shell: bash run: | - uv pip install --system dist/arcadedb_jvm-*.whl + uv pip install --system test-install/arcadedb_jvm-*.whl - name: Install example dependencies shell: bash @@ -763,7 +847,7 @@ jobs: # Summary job that checks all platforms test-examples-summary: name: Examples Test Summary - needs: test-examples + needs: [build, test-examples] runs-on: ubuntu-latest # Skipped (not red) when the matrix was skipped for a draft PR. if: always() && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) @@ -774,10 +858,10 @@ jobs: echo "## 🎯 Overall Examples Test Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ needs.test-examples.result }}" = "success" ]; then + if [ "${{ needs.build.result }}" = "success" ] && [ "${{ needs.test-examples.result }}" = "success" ]; then echo "✅ **All platforms passed example testing!**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "All examples ran successfully across all enabled platforms." >> $GITHUB_STEP_SUMMARY + echo "All examples ran successfully across all enabled platforms, against one wheel built for each platform." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Platforms tested**: linux/amd64, linux/arm64, darwin/arm64, windows/amd64" >> $GITHUB_STEP_SUMMARY else diff --git a/CLAUDE.md b/CLAUDE.md index 5730ee4..bcaf961 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ python -m bandit -c pyproject.toml -r examples --severity-level medium --confide Each package has one test and one release workflow: `test-jvm.yml` and `release-jvm.yml` for `arcadedb-jvm`, `test-native.yml` and `release-native.yml` for `arcadedb` (the release workflows call the test workflows with `workflow_call`; the PyPI trusted publishers are keyed on the release file names and the environments `pypi-jvm` and `pypi`, so never rename them without changing PyPI first). `test-python-examples.yml` runs `examples/`. -`.github/workflows/test-jvm.yml` builds ONE wheel per platform (linux/amd64, linux/arm64, darwin/arm64, windows/amd64), tagged `py3-none-` by `setup.py` (`bdist_wheel.get_tag`; `scripts/verify_wheel_tag.py` checks it), and tests that wheel on Python 3.11–3.14; also runs nightly against `arcadedata/arcadedb:latest`. `requires-python` is `>=3.11,<3.15` because the tag no longer keeps pip off 3.15 and JPype has no 3.15 wheel; drop the cap with it. The org shares a runner cap with upstream, so the unit tests run 10 legs on every event (linux/amd64 on all four Pythons, the other three platforms on 3.11 and 3.14, the shape of test-native.yml), and the examples run all four platforms on 3.14 on a pull request (4 legs, 24 checks in all) and 16 legs on push to main, the nightly run, dispatch and the release (36 checks). `scripts/select_matrix.py` holds both lists (`PULL_REQUEST_LEGS`, keyed `jvm` and `examples`), 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/test-jvm.yml` builds ONE wheel per platform (linux/amd64, linux/arm64, darwin/arm64, windows/amd64), tagged `py3-none-` by `setup.py` (`bdist_wheel.get_tag`; `scripts/verify_wheel_tag.py` checks it), and tests that wheel on Python 3.11–3.14; also runs nightly against `arcadedata/arcadedb:latest`. `requires-python` is `>=3.11,<3.15` because the tag no longer keeps pip off 3.15 and JPype has no 3.15 wheel; drop the cap with it. The org shares a runner cap with upstream, so the unit tests run 10 legs on every event (linux/amd64 on all four Pythons, the other three platforms on 3.11 and 3.14, the shape of test-native.yml), and the examples run all four platforms on 3.14 on a pull request (4 legs, 28 checks in all) and 16 legs on push to main, the nightly run, dispatch and the release (40 checks). `scripts/select_matrix.py` holds both lists (`PULL_REQUEST_LEGS`, keyed `jvm` and `examples`), 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-jvm.yml` publishes `arcadedb-jvm` 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/docs/modules/ROOT/pages/development/ci.adoc b/docs/modules/ROOT/pages/development/ci.adoc index efd3d8a..f53b179 100644 --- a/docs/modules/ROOT/pages/development/ci.adoc +++ b/docs/modules/ROOT/pages/development/ci.adoc @@ -16,7 +16,7 @@ Each package has one test workflow and one release workflow: | `release-native.yml` |=== -The release workflows call the test workflows (`workflow_call`) and then publish; see xref:development/release.adoc[Release]. Two workflows test the `arcadedb-jvm` package: `test-jvm.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. `test-native.yml` builds and tests the `arcadedb` (native) package; see <<_native_build,Native build>>. +The release workflows call the test workflows (`workflow_call`) and then publish; see xref:development/release.adoc[Release]. Two workflows test the `arcadedb-jvm` package: `test-jvm.yml` builds a wheel and runs the unit tests, and `test-python-examples.yml` runs the scripts in `examples/`. Both build the wheel once for each platform, on a native runner of that platform (`Build arcadedb-jvm wheel ()` and `Build wheel for examples ()`), and install that one file into each Python they test. `test-native.yml` builds and tests the `arcadedb` (native) package; see <<_native_build,Native build>>. The package supports four platforms and four Python versions (3.11 to 3.14). That is 16 combinations. The organisation shares one runner limit with ArcadeData/arcadedb, so each macOS or Windows job delays the upstream jobs too, and not every combination runs. @@ -31,7 +31,7 @@ The `requires-python` of the package is `>=3.11,<3.15`. The wheel tag no longer == 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 next jobs: the test legs, and for `test-jvm.yml` also the platforms to build (`--wheels`). +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 next jobs: the test legs and the platforms to build (`--wheels`). [cols="2,3,3"] |=== @@ -46,7 +46,7 @@ The script `scripts/select_matrix.py` chooses the combinations. The first job of | All 16 combinations. |=== -The unit tests run the same 10 legs on every event, like the native package (all Python versions on one Linux platform, the oldest and the newest elsewhere). They were 16 legs on `main`, the nightly run and a release before the wheel became `py3-none`; the extra legs tested the same file again. A pull request runs 24 checks: 14 test jobs (10 and 4), 4 wheel builds, the two `Download ArcadeDB JARs` jobs, the two summary jobs, `Bandit security scan` and `Dependency floor audit`. The other events run 36: the examples run all 16 combinations there. +The unit tests run the same 10 legs on every event, like the native package (all Python versions on one Linux platform, the oldest and the newest elsewhere). They were 16 legs on `main`, the nightly run and a release before the wheel became `py3-none`; the extra legs tested the same file again. A pull request runs 28 checks: 14 test jobs (10 and 4), 8 wheel builds (4 for each workflow), the two `Download ArcadeDB JARs` jobs, the two summary jobs, `Bandit security scan` and `Dependency floor audit`. The other events run 40: the examples run all 16 combinations there, from 4 wheel builds. 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. diff --git a/scripts/select_matrix.py b/scripts/select_matrix.py index 13c1ba3..05e7d9d 100644 --- a/scripts/select_matrix.py +++ b/scripts/select_matrix.py @@ -20,6 +20,7 @@ select_matrix.py jvm [--event NAME] # test-jvm.yml: the test legs select_matrix.py jvm [--event NAME] --wheels # test-jvm.yml: the build legs, one for each platform of the test legs select_matrix.py examples [--event NAME] # test-python-examples.yml + select_matrix.py examples [--event NAME] --wheels # test-python-examples.yml: the build legs, one for each platform `--event` defaults to $GITHUB_EVENT_NAME. The output is one line of JSON for `strategy.matrix`: {"include": [{"platform": ..., "runs-on": ..., "python-version": ...}, ...]}, and with diff --git a/tests/test_select_matrix.py b/tests/test_select_matrix.py index a8ddc28..adc3cad 100644 --- a/tests/test_select_matrix.py +++ b/tests/test_select_matrix.py @@ -154,3 +154,24 @@ def test_the_jvm_workflow_builds_each_platform_once_and_tests_that_wheel(): assert "python-version: ${{ matrix.python-version }}" not in build assert "scripts/build.sh" in build and "scripts/build.sh" not in test.split("\n test-summary:")[0] assert "actions/download-artifact" in test and "name: wheel-" in test + + +def test_the_examples_workflow_builds_each_platform_once_and_runs_the_examples_against_that_wheel(): + text = WORKFLOWS["examples"].read_text(encoding="utf-8") + assert "scripts/select_matrix.py examples --event \"$EVENT_NAME\" --wheels" in text + assert "matrix: ${{ fromJSON(needs.download-jars.outputs.wheel-matrix) }}" in text + build, rest = text.split("\n build:\n")[1].split("\n test-examples:\n") + test = rest.split("\n test-examples-summary:")[0] + # no Python in the build matrix, and the examples job builds nothing: it installs the wheel the build job uploaded + assert "python-version: ${{ matrix.python-version }}" not in build + assert "scripts/build.sh" in build and "scripts/verify_wheel_tag.py" in build + assert "scripts/build.sh" not in test + assert "actions/download-artifact" in test and "name: examples-wheel-" in test + assert "dist/" not in test + # release-jvm.yml publishes every wheel-* artifact of the run, and the release runs this workflow beside test-jvm.yml + assert "name: wheel-" not in text + # the legs of the pull request and of the other events are the same platforms, one build each + for event in ["pull_request", *NON_PULL_REQUEST_EVENTS]: + assert sm.select_wheels("examples", event) == { + "include": [{"platform": p, "runs-on": r} for p, r in PLATFORMS.items()] + }