From 0a6109b8d087c552040d9edeee74932575164c35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 00:22:28 +0000 Subject: [PATCH] ci: split the release test gate into Java and Rust jobs The release ran everything through one `bazel test //...` inside the reusable workflow's build job, so the Kotlin and Rust suites shared a runner and a fate: whichever failed first ended the run, and the other suite's verdict was never produced. v44.0.0 lost the whole release to "//tests:e2e_test TIMEOUT in 3600.0s" on that single job. Move the two language suites into `java-tests` (//cli/...) and `rust-tests` (//src/..., //tests/..., //tools/coverage/... and the two Rust lint gates). They run concurrently, each on its own runner with its own 6h GitHub job limit, and both gate the reusable workflow through `needs` -- so a release still requires both to be green, but a failure in one no longer suppresses the other's results. The Rust job names its packages recursively instead of going through the hand-maintained //:rust_tests suite, so a new test under //src or //tests is in the release gate whether or not it is added to that suite. What is left for the reusable workflow's build job is the Python and Go release tooling (//tools:all, //tools/go/...), which is fast enough not to warrant a third runner. The three commands together still cover every test in //...; the header comment records that invariant. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UCA8NTE34CAFHATYM6nU3a --- .bazelrc | 7 ++- .github/workflows/release.yaml | 100 +++++++++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/.bazelrc b/.bazelrc index 3f2ebd6..86c96b9 100644 --- a/.bazelrc +++ b/.bazelrc @@ -75,9 +75,10 @@ build --output_groups=+rustfmt_checks # coverage mode anyway, so plain `bazel test` runs are never affected. coverage --coverage_output_generator=//tools/coverage:lcov_merger -# Releases run the whole suite on one cold runner (see release.yaml), where the -# long suites -- //tests:e2e_test (size = "enormous") and //cli:E2ETest -# (timeout = "eternal") -- have no headroom left: v44.0.0 died on +# Releases run the suite on cold runners (see release.yaml, which splits it +# into a Java job and a Rust job), where the long suites -- //tests:e2e_test +# (size = "enormous") and //cli:E2ETest (timeout = "eternal") -- have no +# headroom left: v44.0.0 died on # "//tests:e2e_test TIMEOUT in 3600.0s" and published nothing. Per-size timeouts # are there to catch hangs on PRs, where each suite gets its own runner and an # overrun means something is wrong. A release has no such signal to give, so the diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 881ef04..e3868ad 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,6 +1,21 @@ # Cut a release whenever a new tag is pushed or via workflow_dispatch. # Uses bazel-contrib release ruleset: build, attest, and publish to GitHub Releases. # A separate matrix job attaches host-native Rust CLI binaries to the draft release. +# +# The test suites gate the release from two independent jobs -- `java-tests` and +# `rust-tests` -- rather than from one `bazel test //...` inside the reusable +# workflow's build job. They run concurrently on their own runners, so a Kotlin +# failure no longer cancels the Rust suite's results (or vice versa): a release +# candidate gets both verdicts from one run instead of one verdict at a time. +# It also splits the two long suites -- //cli:E2ETest and //tests:e2e_test -- +# across two cold runners, each with its own 6h GitHub job limit. +# +# Between them the three test commands cover every test in //...; keep it that +# way when adding a package: +# java-tests //cli/... (Kotlin/Java) +# rust-tests //src/..., //tests/..., //tools/coverage/..., the Rust lint +# gates //:rust_clippy_check and //:rust_format_check +# release //tools:all, //tools/go/... (Python/Go release tooling) name: Release on: workflow_dispatch: @@ -17,14 +32,91 @@ permissions: attestations: write contents: write jobs: + # The Kotlin/Java half of the release gate. //cli/... is the whole JVM CLI, + # including //cli:E2ETest (timeout = "eternal"), which is why this suite gets + # a runner to itself. Java is set up explicitly even though .bazelrc pins a + # hermetic --java_runtime_version for tests, so this job matches what the + # `test-jre21` job in ci.yaml already runs these suites under. + java-tests: + name: Java tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag_name || github.ref_name }} + - name: Setup Java JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version: ^1.17 + - name: Setup Bazelisk + run: | + go install github.com/bazelbuild/bazelisk@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + # No USE_BAZEL_VERSION: bazelisk takes the version from .bazelversion, + # which is the one a release is cut with. --config=no-test-timeouts is in + # .bazelrc; it is what keeps the long suites from timing out on a cold + # release runner (see the comment on that config). + - name: Run Kotlin/Java tests + run: bazelisk test //cli/... --config=no-test-timeouts + # The Rust half: the three Rust packages plus the two lint gates, which live + # in the root package because they pin their own roots (see //BUILD). The + # packages are named recursively rather than through the //:rust_tests suite + # ci.yaml uses, so a Rust test added to //src or //tests is in the release + # gate whether or not it also gets added to that hand-maintained suite. + # + # //tests:e2e_test spawns a nested Bazel per fixture workspace, so this job + # needs the Java and $BAZEL setup that `rust-candidate-e2e` in ci.yaml uses -- + # without $BAZEL the suite shells out to the outer Bazel's own binary. + rust-tests: + name: Rust tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag_name || github.ref_name }} + - name: Setup Java JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version: ^1.17 + - name: Setup Bazelisk + run: | + go install github.com/bazelbuild/bazelisk@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + echo "BAZEL=$(go env GOPATH)/bin/bazelisk" >> "$GITHUB_ENV" + - name: Run Rust tests + run: | + bazelisk test \ + //src/... \ + //tests/... \ + //tools/coverage/... \ + //:rust_clippy_check \ + //:rust_format_check \ + --config=no-test-timeouts release: + # Both suites gate the release, but neither gates the other: they run + # concurrently and the reusable workflow starts only once both are green. + needs: [java-tests, rust-tests] uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v7.4.0 with: release_files: archives/*.* - # Same target pattern as the reusable workflow's default (`bazel test - # //...`), restated because the only way to add a flag is to replace the - # whole command. --config=no-test-timeouts is in .bazelrc. - bazel_test_command: "bazel test //... --config=no-test-timeouts" + # Replaces the reusable workflow's default (`bazel test //...`), which is + # the only way to change what its build job runs. The Kotlin and Rust + # suites moved to the two jobs above; what is left is the Python and Go + # release tooling, which is fast enough to keep here rather than spend a + # third runner on. --config=no-test-timeouts is in .bazelrc. + bazel_test_command: "bazel test //tools:all //tools/go/... --config=no-test-timeouts" prerelease: false draft: true tag_name: ${{ inputs.tag_name || github.ref_name }}