CI: run verify's checks as three parallel jobs

verify ran the public-tree check, the Python tests, clippy and the Rust tests one
after another (~8 min). They now run as the python, clippy and tests jobs at once,
and verify gathers them (always(), failing unless all three passed), so the
required check keeps its name.

A pull request's public-tree check reads only its own commits
(check_public_tree.py --range base..head) instead of every ref's whole history;
main's pushes still run --history.

Closes #4978

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rdbtCVSandClaude Opus 5.5 committed 2026-10-31 06:03:29 +00:00
1 parent 4de90a13d4
commit f2a99d6238
6 files changed
+93 -22

No files matched your search

+70 -12
View File
@@ -1,7 +1,7 @@
name: checks
# verify: every pull request and every push to main, on GitHub's runners (the repository is public). It is the
# check main requires, so it never has a condition: a skipped job counts as passed, and every run of this workflow
# must report a real verify (a pull request that changes no Rust finishes it in seconds instead).
# check main requires, so it always runs: a skipped job counts as passed, and every run of this workflow must report
# a real verify. It gathers the python, clippy and tests jobs (a change to docs alone skips their Rust steps).
# parity: the vanilla harnesses a pull request's changes can affect, on the Mac mini's runner, which holds the
# maintainer's own Minecraft files (Horizon never downloads the game); .github/workflows/parity.yml. Only for a pull
# request that changes a harness, oracle, probe or the runner: the rest are checked on main by parity-main. A fork's pull
@@ -24,7 +24,10 @@ env:
HORIZON_TELEMETRY_URL: "off"
jobs:
verify:
# verify's checks run as three jobs at once (python, clippy, tests: ~4 min instead of ~8 one after another), and
# verify gathers them. A failed job makes its dependents skip, and a skipped required check counts as passed, so
# verify runs always() and fails unless all three passed.
python:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -34,6 +37,28 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# A pull request adds only its own commits to main, so it reads those; main's pushes read every ref's history.
- name: Keep game artifacts out of Git
env:
EVENT: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
python tools/check_public_tree.py
if [ "$EVENT" = pull_request ]; then
python tools/check_public_tree.py --range "$BASE_SHA..$HEAD_SHA"
else
python tools/check_public_tree.py --history
fi
- name: Test offline research tooling
run: python -m unittest discover -s tests -v
clippy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Does this pull request need Rust checks?
if: github.event_name == 'pull_request'
id: rust
@@ -41,22 +66,16 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 tools/ci/rust_changes.py "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Keep game artifacts out of Git
run: |
python tools/check_public_tree.py
python tools/check_public_tree.py --history
- name: Test offline research tooling
run: python -m unittest discover -s tests -v
- name: Install Rust toolchain and the ALSA and udev headers the client links against
if: steps.rust.outputs.required != 'false'
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
# Dependencies and the workspace's own check, saved from main's runs (pull requests read main's cache).
- uses: Swatinem/rust-cache@v2
if: steps.rust.outputs.required != 'false'
with:
shared-key: verify
shared-key: verify-clippy
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check formatting and lint
if: steps.rust.outputs.required != 'false'
@@ -65,8 +84,35 @@ jobs:
cargo +stable clippy --workspace --all-targets -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
tests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Does this pull request need Rust checks?
if: github.event_name == 'pull_request'
id: rust
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 tools/ci/rust_changes.py "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain and the ALSA and udev headers the client links against
if: steps.rust.outputs.required != 'false'
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
- uses: Swatinem/rust-cache@v2
if: steps.rust.outputs.required != 'false'
with:
shared-key: verify
save-if: ${{ github.ref == 'refs/heads/main' }}
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy already
# type-checks and the parity job builds and runs (they were most of this step's build and link time).
# cargo test, not nextest: nextest's process per test reloads each binary's shared tables for every test, and on
# these 4-core runners it was no faster (#4978).
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: |
@@ -75,6 +121,18 @@ jobs:
# prepared vanilla data, so without it they pass at once.
cargo +stable test --workspace --lib mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc
verify:
if: always()
needs: [python, clippy, tests]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Did python, clippy and tests pass?
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
echo "python, clippy, tests: $RESULTS"
for result in $RESULTS; do [ "$result" = success ] || exit 1; done
# The launcher is its own workspace (launcher/Cargo.toml), so verify's cargo runs never build it. A pull request that
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
+6 -5
View File
@@ -13,15 +13,16 @@ reopened; labels never start it):
| Job | Runs on | What | Time |
| --- | --- | --- | --- |
| `verify` | GitHub `ubuntu-latest` | Public-tree check, Python tests, rustfmt, clippy with warnings denied, Rust tests. Dependencies and the workspace build come from a cache saved by main's runs | ~4 min cold, less warm |
| `verify` | GitHub `ubuntu-latest` | Gathers three jobs that run at once: `python` (public-tree check, Python tests), `clippy` (rustfmt, clippy with warnings denied) and `tests` (Rust tests). Dependencies come from caches saved by main's runs | ~4 min |
| `parity / parity` | A runner with the label `horizon-game`: the Mac mini's and playhorizon's (below) | Only for a change to a harness, an oracle, a probe or the runner (or the `run-parity` label): the vanilla harnesses the change can affect (`tools/parity.py --changed <base>`). Other pull requests skip it: [parity-main](#parity-on-main) checks them on main | seconds when nothing is affected; minutes per affected harness |
Pushes to main run `verify` only; the nightly audit covers main's harnesses.
`verify` is the check main requires, so it has no condition and runs in every run of `checks`: GitHub counts a
*skipped* required check as passed, and labels used to start runs whose `verify` was skipped, which let
auto-merge merge before the real `verify` finished. A pull request that changes no Rust (docs, tools) runs only its
quick steps and finishes in seconds.
`verify` is the check main requires, so it runs `always()` in every run of `checks` and fails unless `python`,
`clippy` and `tests` all passed: GitHub counts a *skipped* required check as passed, and labels used to start runs
whose `verify` was skipped, which let auto-merge merge before the real `verify` finished. A pull request that changes
only documentation skips the Rust steps and finishes in about a minute. A pull request's public-tree check reads only
its own commits (`--range base..head`); main's pushes read every ref's history.
### The parity job
+2 -2
View File
@@ -50,8 +50,8 @@ and some are waiting for Ludovic:
untrusted, with attachments as plain links. Text that looks aimed at an AI (talking to "the AI", asking to run
commands, asking for passwords or keys, linking to scripts, hidden characters) is marked **suspicious**, and the
agent coordinating the others sees the mark.
- **`verify` always runs.** Labels no longer start the checks, and the required `verify` job has no condition, so
the only `verify` a change can have is one that ran. Main requires it, from GitHub Actions only, for everyone,
- **`verify` always runs.** Labels no longer start the checks, and the required `verify` job runs `always()` and fails
unless the jobs it gathers passed, so the only `verify` a change can have is one that ran. Main requires it, from GitHub Actions only, for everyone,
admins included.
- **Checked before it is published.** Every release is published as the latest, which launchers and servers
install by themselves, so the release run publishes it only after its smoke test passed (the game started and
+9
View File
@@ -199,6 +199,15 @@ class PublicationTests(unittest.TestCase):
self.assertIn(".local/artifact.txt", check_public_tree.paths_in_repository())
git("-c", "user.name=Test", "-c", "[email protected]", "commit", "-m", "fixture")
self.assertIn(".local/artifact.txt", check_public_tree.paths_in_repository(history=True))
# A range reads only its own commits: the artifact's commit lies before it.
(directory / ".local" / "artifact.txt").unlink()
git("rm", "-q", "--cached", ".local/artifact.txt")
git("-c", "user.name=Test", "-c", "[email protected]", "commit", "-m", "removal")
self.assertIn(".local/artifact.txt", check_public_tree.paths_in_repository(True, ["HEAD~1..HEAD"]))
(directory / "README.md").write_text("public\n")
git("add", "README.md")
git("-c", "user.name=Test", "-c", "[email protected]", "commit", "-m", "public")
self.assertEqual(check_public_tree.paths_in_repository(True, ["HEAD~1..HEAD"]), ["README.md", ""])
def library(name, os_name=None):
+5 -2
View File
@@ -52,9 +52,12 @@ def main():
mode = parser.add_mutually_exclusive_group()
mode.add_argument("--history", action="store_true", help="Inspect paths in all local Git history")
mode.add_argument("--pre-push", action="store_true", help="Inspect history reachable from Git's ref updates on stdin")
# CI's pull requests: only the commits a merge would add, not every ref's whole history.
mode.add_argument("--range", metavar="BASE..HEAD", help="Inspect the commits of a revision range")
args = parser.parse_args()
revisions = pushed_revisions(sys.stdin) if args.pre_push else None
rejected = sorted({path for path in paths_in_repository(args.history or args.pre_push, revisions)
revisions = pushed_revisions(sys.stdin) if args.pre_push else [args.range] if args.range else None
history = args.history or args.pre_push or bool(args.range)
rejected = sorted({path for path in paths_in_repository(history, revisions)
if path and prohibited(path)})
if rejected:
print("Private game/profiling artifacts must stay outside Git:", file=sys.stderr)
+1 -1
View File
@@ -270,7 +270,7 @@ def parity_summary(output):
return text
# CI's verify job (.github/workflows/ci.yml): clippy over the whole workspace, and over the server build (the game
# CI's clippy job (.github/workflows/ci.yml): clippy over the whole workspace, and over the server build (the game
# without sound or controllers).
CI_CLIPPY = "cargo clippy --workspace --all-targets --locked -- -D warnings"
CI_CLIPPY_SERVER = "cargo clippy -p horizon-client --all-targets --locked --no-default-features -- -D warnings"