Files
buzz/.github/workflows/_ci-desktop.yml
Matt Toohey eda26b9423 ci: repeat changed tests ten times on pull requests (#8613)
This ports the advisory flake check from block/buzz-app
(https://github.com/block/buzz-app/pull/890) to this repository: pull
requests repeat the tests they change ten times, with retries off, so a
test that passes CI by luck is caught before it merges.

## What it does

`flake-select` diffs the PR merge commit against its first parent
(`HEAD^1..HEAD`, `fetch-depth: 2`) and maps changed hunks to tests per
runner: a hunk inside a test body selects that test; a change anywhere
else in a test file, or a new file, selects the whole file. One lane per
runner then repeats only that selection 10× with retries disabled and
writes a test × iteration table to its step summary. Lanes skip when
nothing was selected, run only on `pull_request`, and are not read by
any required gate in `ci.yml` — their own statuses carry the result.

## Per runner

| Runner | Selection granularity | Repeat mechanism |
|---|---|---|
| Rust (cargo-nextest) | `crates/<crate>/<path>.rs::<module::test>` from
`#[test]`-style attribute spans (comments and literals blanked first). A
hunk outside any test body selects the file's tests. `#[ignore]`d and
Postgres / `external_infra` tests are excluded. | One `cargo nextest run
--profile flake` per package per iteration, with target flags derived
from the path (`src/**` → `--lib --bins`, `tests/<stem>.rs` → `--test
<stem>`), a filterset of exact `binary_id & test` pairs, and JUnit
parsed per iteration. New `[profile.flake]`: `retries = 0`, `fail-fast =
false`. |
| node:test | Whole changed `desktop/src/**/*.test.mjs`,
`*.jsdom-test.mjs`, `desktop/scripts/*.test.mjs` | 10× loop, each file
alone, with the flag sets from `desktop/package.json` `test` |
| Playwright `smoke` | `file:line` journeys from `test()`/`it()` spans
(TypeScript AST) intersected with `playwright test --list
--project=smoke`, deduplicated | Native `--repeat-each=10 --retries=0`,
sharded `min(10, ceil(journeys / 5))` ways; `playwright-summary`
tabulates the JSON report |
| Flutter | Whole changed `mobile/test/**/*_test.dart` | 10× `flutter
test --file-reporter=json` loop with the mobile lane's `--dart-define` |

A runner whose selection exceeds 64 KiB of entries, or more than 250
Playwright journeys, is skipped with a note rather than overrunning its
timeout.

## Differences from the buzz-app version

- Lanes live in a reusable `_ci-flake.yml` called from `ci.yml`,
matching this repo's `_ci-*.yml` convention, instead of inline jobs.
- Playwright specs are parsed with the desktop app's own `typescript`
package rather than `rolldown/parseAst`.
- No Playwright `integration` project lane: it needs the relay artifact
chain. Integration-only specs list nothing for `smoke` and the selection
stays empty.
- Playwright shards are capped at 10, the same bound as the regular
smoke lane.
- Rust entries carry the file and in-file module path, and the filterset
is anchored to exact `(binary, test)` pairs, so a same-named test in
another module is not repeated.
- nextest target flags are derived from the changed file's path, so a
`buzz-relay` change does not compile every test target of the crate.
- A journey-count cap (250) skips the Playwright lane for selections ten
repetitions could not finish within its 45-minute timeout.

## Heads-up

- The Unit Tests `rust-cache` moves from `key: sherpa-cache-v1` to
`shared-key: unit-tests-sherpa-cache-v1` so the flake lane can restore
it read-only. This changes the cache key once: the first Unit Tests run
after merge builds cold.
- This PR itself changes no file the selector recognises
(`scripts/flake-check.test.mjs` is a root script, run by the Desktop
lane), so its own Flake Check lanes are expected to skip.

## Verification

- `node --test scripts/flake-check.test.mjs` — all unit tests pass (also
wired into the Desktop lane).
- `actionlint` on the changed workflows; `biome check` on the two
scripts.
- `scripts/test-rust-cache-contract.sh` and its regression harness pass
with the new shared key.
- Synthetic-diff selection runs for each runner, plus `select` against
this PR's own merge-base (empty, as expected).
- 2-iteration repeats of a real selection on all four runners locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-29 14:34:20 +11:00

278 lines
11 KiB
YAML

name: CI / Desktop
on:
workflow_call:
inputs:
rust:
required: true
type: boolean
desktop:
required: true
type: boolean
desktop_rust:
required: true
type: boolean
outputs:
desktop_result:
value: ${{ jobs.results.outputs.desktop_result }}
env:
CARGO_TERM_COLOR: always
BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
jobs:
# Biome, node:test, and the Vite build do not compile Rust, but node:test
# also pins desktop constants to relay/DB crate sources (for example
# relayReconnectReplay.test.mjs), so Rust changes select this job too.
desktop-js:
name: Desktop JS
runs-on: ubuntu-latest
timeout-minutes: 20
# Keep in sync with JS_SELECTED in the Desktop gate (pinned by ci-required-gates.test.mjs).
if: github.event_name == 'push' || inputs.desktop || inputs.rust
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 2
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Desktop lint and format
run: just desktop-check
- name: Desktop unit tests
run: just desktop-test
- name: Flake check selection tests
run: node --test scripts/flake-check.test.mjs
- name: Desktop build
run: just desktop-build
- name: Save pnpm store cache
if: github.event_name == 'push'
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
# Tauri Rust lanes run in parallel. The compiled-flags lane only rebuilds
# buzz_lib under other BUZZ_BUILD_* values, so it restores the core lane's
# cache read-only; the mesh-llm feature pulls its own dependency graph.
desktop-tauri:
name: Desktop Tauri (${{ matrix.lane }})
runs-on: ubuntu-latest
# A lane needs up to ~15m. ci-apt-retry.sh bounds the apt step at ~34m worst
# case (update ~8.5m + install ~25m); 55m fits both, so a degraded mirror
# fails with the wrapper's error instead of the job timeout.
timeout-minutes: 55
# Keep in sync with RUST_SELECTED in the Desktop gate (pinned by ci-required-gates.test.mjs).
if: github.event_name == 'push' || inputs.desktop_rust || inputs.rust
strategy:
fail-fast: false
matrix:
include:
- lane: core
cache: desktop-core
save-cache: true
run: just desktop-tauri-clippy && just desktop-tauri-check && just desktop-tauri-test
- lane: mesh
cache: desktop-mesh
save-cache: true
run: just desktop-tauri-test-mesh
- lane: compiled-flags
cache: desktop-core
save-cache: false
run: just desktop-tauri-test-compiled-flags
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 2
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: desktop/src-tauri
shared-key: ${{ matrix.cache }}
save-if: ${{ matrix.save-cache && github.event_name != 'pull_request' }}
- name: Install Tauri dependencies (Linux)
env:
DEBIAN_FRONTEND: noninteractive
run: |
CI_APT_ATTEMPT_SECONDS=90 scripts/ci-apt-retry.sh sudo apt-get update
scripts/ci-apt-retry.sh sudo apt-get install -y --no-install-recommends \
build-essential \
curl \
file \
libasound2-dev \
libayatana-appindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
patchelf \
wget
- name: Desktop Tauri ${{ matrix.lane }}
run: ${{ matrix.run }}
env:
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
desktop-smoke-e2e:
name: Desktop Smoke E2E (${{ matrix.shard }})
runs-on: ubuntu-latest
# A smoke shard needs ~12m, and Chromium dependency setup is bounded by
# ci-apt-retry.sh (~25m worst case). Leave time for the full suite
# and its reports to finish.
timeout-minutes: 45
# Keep in sync with FRONTEND_SELECTED in the Desktop gate (pinned by ci-required-gates.test.mjs).
if: github.event_name == 'push' || inputs.desktop
strategy:
fail-fast: false
matrix:
# Playwright shards smoke specs by test count, not duration; ten
# shards keep the slowest one near 11 minutes of test time.
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Get Playwright version
id: pw-version
run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT"
- name: Restore Playwright browser cache
id: playwright-cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: cd desktop && pnpm exec playwright install chromium
- name: Install Playwright system dependencies
run: cd desktop && ../scripts/ci-apt-retry.sh pnpm exec playwright install-deps chromium
- name: Save Playwright browser cache
if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Desktop E2E build
run: pnpm -C desktop build:e2e
- name: Desktop smoke e2e
run: cd desktop && pnpm exec playwright test --project=smoke --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Summarize flaky tests
if: ${{ !cancelled() }}
run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop Smoke E2E (${{ matrix.shard }})"
working-directory: desktop
- name: Upload desktop smoke e2e artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-smoke-e2e-artifacts-${{ matrix.shard }}
path: |
desktop/playwright-report
desktop/playwright-report.json
desktop/test-results
if-no-files-found: ignore
retention-days: 7
desktop:
name: Desktop
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [desktop-js, desktop-tauri, desktop-smoke-e2e, desktop-windows-build]
if: always() && (github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust)
permissions:
contents: read
steps:
- name: Check desktop jobs
env:
JS_SELECTED: ${{ github.event_name == 'push' || inputs.desktop || inputs.rust }}
RUST_SELECTED: ${{ github.event_name == 'push' || inputs.desktop_rust || inputs.rust }}
FRONTEND_SELECTED: ${{ github.event_name == 'push' || inputs.desktop }}
JS: ${{ needs.desktop-js.result }}
TAURI: ${{ needs.desktop-tauri.result }}
SMOKE: ${{ needs.desktop-smoke-e2e.result }}
WINDOWS: ${{ needs.desktop-windows-build.result }}
run: |
# A selected job must succeed; an unselected job must be skipped.
status=0
check() {
local name="$1" selected="$2" result="$3" want=skipped
[ "$selected" = true ] && want=success
if [ "$result" != "$want" ]; then
echo "$name finished with: $result (expected $want)"
status=1
fi
}
check "Desktop JS" "$JS_SELECTED" "$JS"
check "Desktop Tauri lanes" "$RUST_SELECTED" "$TAURI"
check "Desktop Smoke E2E shards" "$FRONTEND_SELECTED" "$SMOKE"
check "Desktop Windows Build" "$FRONTEND_SELECTED" "$WINDOWS"
[ "$status" = 0 ] && echo "Desktop jobs passed"
exit "$status"
desktop-windows-build:
name: Desktop Windows Build
runs-on: windows-latest
timeout-minutes: 20
# Keep in sync with FRONTEND_SELECTED in the Desktop gate (pinned by ci-required-gates.test.mjs).
if: github.event_name == 'push' || inputs.desktop
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24.14.1
package-manager-cache: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: 11.4.0
- name: Install desktop dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: Build both protected-feature selections
shell: pwsh
run: |
Remove-Item Env:VITE_BUZZ_BESTIE -ErrorAction SilentlyContinue
pnpm -C desktop build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$env:VITE_BUZZ_BESTIE = "1"
pnpm -C desktop build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
results:
name: Results
if: ${{ always() }}
needs: [desktop]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
outputs:
desktop_result: ${{ needs.desktop.result }}
steps:
- run: echo "Captured Desktop job results"