From 25508adf34c1a671c098b301a3c2870bbb2c6839 Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Wed, 11 Nov 2026 13:48:02 -0500 Subject: [PATCH 1/2] CI: a pull request gets the fast answer, main and dispatch get everything A pull request now runs Linux only, with the tests split three ways (nextest --partition hash), and skips the release build unless the change touches packaging, Cargo.toml or Cargo.lock. macOS and Windows tests, the other release programs and doc tests on those platforms run on every push to main and on a manual dispatch, which is what a release already waits for. The measured problem: a PR waited 26-30 minutes on a macOS test job that rebuilt cold whenever the 10 GB cache had evicted it, and 12-28 on Windows, while Linux took 9. The tests are CPU-bound (about 1,300 CPU-seconds), so they scale with runners. Docs, site and the review and pr-gate tooling no longer start the Rust matrix at all. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 91 ++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa368c52..6019b5d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,16 @@ name: CI -# Pull requests, and pushes to main. A branch that also has a pull request used to run this -# twice. Tests and the release build run together: each used to wait for the other, and the -# release programs are what a later tag packages instead of compiling again. A pull request -# packages Linux x86_64 only; main builds every release program, Linux aarch64 included. +# A pull request gets the fast answer: Linux only, the tests split three ways, and nothing that +# is not about the change. Pushes to main (and a manual dispatch, `gh workflow run ci.yml --ref +# `) get everything: Linux, macOS and Windows, and every release program, which is what +# a later tag packages instead of compiling again. Tests and the release build run together: +# each used to wait for the other. +# +# What a pull request skips, and where it is caught instead: +# - macOS and Windows tests: main's push run, which a release waits for, and a dispatch. +# - the release build and packaging: unless the change touches packaging, Cargo.toml or +# Cargo.lock (the `changes` job), and always on main. +# - everything when only docs, the site, or the review and pr-gate tooling changed. on: push: branches: [main] @@ -16,6 +23,11 @@ on: - "site/**" - "docs/**" - "**/*.md" + - ".github/review/**" + - ".github/scripts/**" + - ".github/workflows/pr-review*.yml" + - ".github/pull_request_template.md" + workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} @@ -30,17 +42,17 @@ env: NEXTEST_PROFILE: ci jobs: + # The tests are CPU-bound (about 1,300 CPU-seconds), so they scale with runners: each shard builds + # (a warm cache makes that about a minute and a half) and runs its third or half of them. + # `hash` keeps a test in the same shard from run to run. test: - name: test (${{ matrix.os }}) + name: test (${{ matrix.os }} ${{ matrix.shard }}/${{ matrix.shards }}) strategy: fail-fast: false matrix: - os: [ubuntu-22.04, macos-latest, windows-latest] - include: - - os: macos-latest - features: --features tinyclient/gui + include: ${{ fromJSON(github.event_name == 'pull_request' && '[{"os":"ubuntu-22.04","shard":1,"shards":3},{"os":"ubuntu-22.04","shard":2,"shards":3},{"os":"ubuntu-22.04","shard":3,"shards":3}]' || '[{"os":"ubuntu-22.04","shard":1,"shards":3},{"os":"ubuntu-22.04","shard":2,"shards":3},{"os":"ubuntu-22.04","shard":3,"shards":3},{"os":"macos-latest","shard":1,"shards":2,"features":"--features tinyclient/gui"},{"os":"macos-latest","shard":2,"shards":2,"features":"--features tinyclient/gui"},{"os":"windows-latest","shard":1,"shards":2},{"os":"windows-latest","shard":2,"shards":2}]') }} runs-on: ${{ matrix.os }} - timeout-minutes: 60 + timeout-minutes: 25 steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-rust @@ -59,28 +71,73 @@ jobs: cargo build --workspace --all-targets ${{ matrix.features }} --timings echo "TINYCLIENT_TEST_MEDIAD=$GITHUB_WORKSPACE/target/debug/mediad${{ runner.os == 'Windows' && '.exe' || '' }}" >> "$GITHUB_ENV" - name: Test - run: cargo nextest run --workspace ${{ matrix.features }} - # nextest does not run doc tests. + run: cargo nextest run --workspace ${{ matrix.features }} --partition hash:${{ matrix.shard }}/${{ matrix.shards }} + # nextest does not run doc tests. Linux's run in `extras`; the other platforms' run here, once. - name: Doc tests + if: matrix.shard == 1 && runner.os != 'Linux' run: cargo test --workspace --doc ${{ matrix.features }} - # The cfg(target_os = "ios"|"android") arms are not type-checked on this host; this proves - # the mobile feature does not disturb the desktop arms. It shares this job's cache. + + # Linux's doc tests, and the check nothing else covers: the cfg(target_os = "ios"|"android") arms + # are not type-checked on this host; this proves the mobile feature does not disturb the desktop + # arms. It runs beside the test shards and shares their cache. + extras: + name: doc tests and mobile check + runs-on: ubuntu-22.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v5 + - uses: ./.github/actions/setup-rust + with: + cache-key: test + - name: Build test programs + run: | + cargo build --workspace --all-targets + echo "TINYCLIENT_TEST_MEDIAD=$GITHUB_WORKSPACE/target/debug/mediad" >> "$GITHUB_ENV" + - name: Doc tests + run: cargo test --workspace --doc - name: Window with the mobile feature enabled - if: runner.os == 'Linux' run: cargo check -p tinyclient --features "gui mobile" + # Whether the change reaches the release build or its packaging. Only a pull request asks. + changes: + name: what changed + if: github.event_name == 'pull_request' + runs-on: ubuntu-22.04 + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + outputs: + packaging: ${{ steps.files.outputs.packaging }} + steps: + - id: files + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate --jq '.[].filename') + if grep -qE '^(packaging/|\.github/actions/|\.github/workflows/ci\.yml$|Cargo\.toml$|Cargo\.lock$)' <<<"$files"; then + echo "packaging=true" >> "$GITHUB_OUTPUT" + else + echo "packaging=false" >> "$GITHUB_OUTPUT" + fi + package: name: package (${{ matrix.artifact }}) + needs: [changes] + if: ${{ !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.packaging == 'true') }} strategy: fail-fast: false matrix: - # A pull request packages Linux x86_64, which proves the release build and the packaging. + # A pull request that touches packaging packages Linux x86_64, which proves the release build + # and the packaging. # The others build only on main, which is what a tag reuses. On a pull request the macOS # build took a second macOS runner beside the tests and queued behind the phone jobs. artifact: ${{ fromJSON(github.event_name == 'push' && '["linux-x86_64","linux-aarch64","macos","windows"]' || '["linux-x86_64"]') }} # Ubuntu 22.04 is the oldest glibc the release packages promise. runs-on: ${{ fromJSON('{"linux-x86_64":"ubuntu-22.04","linux-aarch64":"ubuntu-22.04-arm","macos":"macos-latest","windows":"windows-latest"}')[matrix.artifact] }} - timeout-minutes: 45 + timeout-minutes: 30 steps: - uses: actions/checkout@v5 # Linux aarch64 builds without a cache. It runs only on main, beside the longer macOS From 1f47245f787abaf3efb0cbe6ba1cefd76655f62f Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Wed, 11 Nov 2026 13:57:00 -0500 Subject: [PATCH 2/2] CI: five Linux shards on a pull request The first run of the three-shard split measured 4.2-6.1 minutes per shard (1.9-3.0 of tests, uneven because hash partitions by test count, not time). Five shards bring the slowest under five minutes. Main keeps three. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6019b5d8..25aa4e8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -# A pull request gets the fast answer: Linux only, the tests split three ways, and nothing that +# A pull request gets the fast answer: Linux only, the tests split five ways, and nothing that # is not about the change. Pushes to main (and a manual dispatch, `gh workflow run ci.yml --ref # `) get everything: Linux, macOS and Windows, and every release program, which is what # a later tag packages instead of compiling again. Tests and the release build run together: @@ -43,14 +43,14 @@ env: jobs: # The tests are CPU-bound (about 1,300 CPU-seconds), so they scale with runners: each shard builds - # (a warm cache makes that about a minute and a half) and runs its third or half of them. + # (a warm cache makes that about a minute and a half) and runs its fifth (on main: third or half) of them. # `hash` keeps a test in the same shard from run to run. test: name: test (${{ matrix.os }} ${{ matrix.shard }}/${{ matrix.shards }}) strategy: fail-fast: false matrix: - include: ${{ fromJSON(github.event_name == 'pull_request' && '[{"os":"ubuntu-22.04","shard":1,"shards":3},{"os":"ubuntu-22.04","shard":2,"shards":3},{"os":"ubuntu-22.04","shard":3,"shards":3}]' || '[{"os":"ubuntu-22.04","shard":1,"shards":3},{"os":"ubuntu-22.04","shard":2,"shards":3},{"os":"ubuntu-22.04","shard":3,"shards":3},{"os":"macos-latest","shard":1,"shards":2,"features":"--features tinyclient/gui"},{"os":"macos-latest","shard":2,"shards":2,"features":"--features tinyclient/gui"},{"os":"windows-latest","shard":1,"shards":2},{"os":"windows-latest","shard":2,"shards":2}]') }} + include: ${{ fromJSON(github.event_name == 'pull_request' && '[{"os":"ubuntu-22.04","shard":1,"shards":5},{"os":"ubuntu-22.04","shard":2,"shards":5},{"os":"ubuntu-22.04","shard":3,"shards":5},{"os":"ubuntu-22.04","shard":4,"shards":5},{"os":"ubuntu-22.04","shard":5,"shards":5}]' || '[{"os":"ubuntu-22.04","shard":1,"shards":3},{"os":"ubuntu-22.04","shard":2,"shards":3},{"os":"ubuntu-22.04","shard":3,"shards":3},{"os":"macos-latest","shard":1,"shards":2,"features":"--features tinyclient/gui"},{"os":"macos-latest","shard":2,"shards":2,"features":"--features tinyclient/gui"},{"os":"windows-latest","shard":1,"shards":2},{"os":"windows-latest","shard":2,"shards":2}]') }} runs-on: ${{ matrix.os }} timeout-minutes: 25 steps: