Files
buzz/.github/workflows/_ci-flake.yml
Luke Tornquist ce42853982 ci: fix main — stale RunCause call sites and Playwright on ubuntu-26.04 (#9418)
## Summary

Two separate CI breaks are hitting every PR right now. Each one blocks a
required check, and neither fix can go green on its own, so this PR
fixes both.

**1. Rust test targets stopped compiling on main at fc8c8022.** #9227
added a required `RunCause` argument to `create_workflow_run` and a
`may_direct_agents` flag to `ActionSink::send_message`. Two tests that
merged around the same time still used the old signatures:

- `attach_rejects_run_from_another_workflow` in
`crates/buzz-db/src/store/workflow.rs` (from #8317)
- the agent-DM refusal test in `crates/buzz-relay/src/workflow_sink.rs`
(from #9371)

Each PR passed CI on its own base. Together they break Rust Lint, Unit
Tests, Windows Rust and the Desktop E2E Relay artifact build (main CI
run 43291564834). The fix passes `RunCause::Schedule` and `true`,
matching the neighbouring tests. Neither test is about run cause or
agent mentions, so neither assertion changes.

**2. Desktop E2E Integration fails on GitHub's new Ubuntu 26.04
runners.** `ubuntu-latest` is rolling to 26.04, and Playwright 1.60 (our
lockfile) fails with `Playwright does not support chromium on
ubuntu26.04-x64`. Playwright 1.61 is the first release that knows
ubuntu26.04. This PR bumps `@playwright/test` to `^1.65.0` in
`desktop/`, `web/` and `admin-web/` and regenerates `pnpm-lock.yaml`. It
also pins every `ubuntu-latest` job in `ci.yml` and `_ci-*.yml` to
`ubuntu-26.04`, so the next runner image change arrives in a reviewed
diff. macOS and Windows jobs are unchanged.

The newer Chromium bundled with Playwright 1.65 changed two smoke specs.
It reports Tailwind's `items-end` as `align-items: flow-end`
(`doctor-states` now accepts `flex-end` or `flow-end`), and it draws the
textarea resize grip differently, so the
`workflow-template-variable-autocomplete` baseline is refreshed from the
ubuntu-26.04 CI output.

It also exposed a timing race in `profile.spec.ts` "owned agent profile
stays in parity": when the Message click lands while the profile panel
is still settling, Playwright's scroll-into-view retry scrolls the
profile body, which hides the owned-agent action row (aria-hidden +
inert) for good. The test now pins the body to the top and waits for the
row before clicking (12/12 repeats plus the full profile spec pass on
Linux).

## Verification

- `cargo clippy -p buzz-db -p buzz-relay --tests -- -D warnings` is
clean on fc8c8022 with commit 1 applied.
- `pnpm install --frozen-lockfile` succeeds with the new lockfile, and
`playwright --version` reports 1.65.0.
- `tsc --noEmit` passes for `desktop`, `web` and `admin-web`.
- This PR's CI is the E2E check on 26.04.

## Notes

- Workflows that don't run on PRs (release, helm charts, sprig, image
builds, codex-security-review) still use `ubuntu-latest`. This PR can't
exercise them, so they're left for a follow-up.
- The hermit-pinned actionlint (1.7.12) doesn't know the `ubuntu-26.04`
label yet. CI only actionlints `codex-security-review.yml`, which this
PR doesn't touch.

## Simpler

No production code changes. The runner pin swaps an implicit moving
target for an explicit one.

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

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 22:25:00 +00:00

333 lines
15 KiB
YAML

name: CI / Flake Check
on:
workflow_call:
env:
CARGO_TERM_COLOR: always
BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
# Advisory flake check: repeat the tests a pull request changes, ten times
# each with retries off, so a race inside a test surfaces before it lands on
# main. The lanes skip when no test changed, so they stay out of the required
# checks in ci.yml (whose gates reject skipped lanes); their own statuses and
# step summaries carry the result. Selections reach the lanes through env
# vars, never interpolated into run scripts; flake-check.mjs admits only plain
# path and identifier characters.
jobs:
flake-select:
name: Flake check selection
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
outputs:
rust: ${{ steps.select.outputs.rust }}
tauri: ${{ steps.select.outputs.tauri }}
node: ${{ steps.select.outputs.node }}
playwright: ${{ steps.select.outputs.playwright }}
playwright_shards: ${{ steps.select.outputs.playwright_shards }}
playwright_shard_total: ${{ steps.select.outputs.playwright_shard_total }}
dart: ${{ steps.select.outputs.dart }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# The merge commit's first parent is the base branch tip, so this
# diff is exactly the pull request's change without a full clone.
fetch-depth: 2
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
# Only a changed spec needs desktop dependencies: Playwright lists its
# smoke journeys and the desktop app's TypeScript parses it. Pull
# requests without one skip the install.
- name: Check for changed Playwright specs
id: specs
run: |
if git diff --quiet --diff-filter=AM HEAD^1 HEAD -- 'desktop/tests/e2e/*.spec.ts'; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Get pnpm store directory
if: steps.specs.outputs.changed == 'true'
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
if: steps.specs.outputs.changed == 'true'
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
if: steps.specs.outputs.changed == 'true'
run: just desktop-install-ci
- name: Map changed hunks to tests
id: select
run: node scripts/flake-check.mjs select HEAD^1 HEAD
flake-rust:
name: Flake check (Rust)
needs: [flake-select]
if: needs.flake-select.outputs.rust != ''
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- uses: rui314/setup-mold@339e96b4ff1f3bd865856ba4cbbe8818a7acb038 # v1
# Restores the Unit Tests lane's cache (_ci-rust.yml) read-only; this
# lane never saves one of its own. The restore only hits while the cache
# key matches: the same shared-key, and rust-cache's environment hash —
# the CARGO*/RUST*/CC* environment variables, the lockfiles and the rustc
# version — must stay identical to the Unit Tests job's, so any env this
# job sets must be set there too (scripts/test-rust-cache-contract.sh
# checks the key).
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
shared-key: unit-tests-sherpa-cache-v1
save-if: false
- name: Install cargo-nextest
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
with:
tool: cargo-nextest@0.9.136
# nextest has no repeat flag; the script runs the selection once per
# iteration under the `flake` profile (.config/nextest.toml).
- name: Repeat changed Rust tests
env:
SELECTED: ${{ needs.flake-select.outputs.rust }}
run: node scripts/flake-check.mjs rust --iterations=10 $SELECTED
- name: Rust iteration reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flake-rust
path: test-results/flake
if-no-files-found: ignore
retention-days: 7
flake-tauri:
name: Flake check (Desktop Tauri)
needs: [flake-select]
if: needs.flake-select.outputs.tauri != ''
runs-on: ubuntu-26.04
# A Desktop Tauri lane's bound (_ci-desktop.yml: ci-apt-retry.sh bounds
# the apt step at ~34m worst case and a cold build needs ~15m), plus a
# second cold build: a lib selection also builds the mesh-llm graph.
timeout-minutes: 70
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- uses: rui314/setup-mold@339e96b4ff1f3bd865856ba4cbbe8818a7acb038 # v1
# Restores the Desktop Tauri core lane's cache (_ci-desktop.yml)
# read-only, under the same constraints as flake-rust's restore above:
# the workspace, shared-key and environment hash must stay identical to
# that job's.
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: desktop/src-tauri
shared-key: desktop-core
save-if: false
- 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: Install cargo-nextest
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
with:
tool: cargo-nextest@0.9.136
# tauri-build validates the sidecar binaries at compile time; the Desktop
# Tauri recipes create the same empty stubs.
- name: Create sidecar stubs
run: just _ensure-sidecar-stubs
# Same script as flake-rust, against the Tauri workspace's manifest and
# its own .config/nextest.toml `flake` profile. A selected lib test also
# repeats under mesh-llm, as desktop-tauri-test-mesh runs it; the
# restored desktop-core cache does not hold that dependency graph, so a
# lib selection builds it cold, and an integration-test one never does.
- name: Repeat changed desktop Tauri tests
env:
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
SELECTED: ${{ needs.flake-select.outputs.tauri }}
run: node scripts/flake-check.mjs tauri --iterations=10 $SELECTED
- name: Desktop Tauri iteration reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flake-tauri
path: test-results/flake
if-no-files-found: ignore
retention-days: 7
flake-node:
name: Flake check (node:test)
needs: [flake-select]
if: needs.flake-select.outputs.node != ''
runs-on: ubuntu-26.04
timeout-minutes: 15
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: Repeat changed node:test files
env:
SELECTED: ${{ needs.flake-select.outputs.node }}
run: node scripts/flake-check.mjs node --iterations=10 $SELECTED
- name: node:test iteration logs
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flake-node
path: test-results/flake
if-no-files-found: ignore
retention-days: 7
flake-playwright:
name: Flake check (Playwright smoke, ${{ matrix.shard }}/${{ needs.flake-select.outputs.playwright_shard_total }})
needs: [flake-select]
if: needs.flake-select.outputs.playwright != ''
runs-on: ubuntu-26.04
# Same bound as a smoke shard: Chromium dependency setup is bounded by
# ci-apt-retry.sh (~25m worst case), and flake-check.mjs sizes the shards
# so the repetitions fit the rest.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(needs.flake-select.outputs.playwright_shards) }}
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: Desktop E2E build
run: pnpm -C desktop build:e2e
# --retries=0 overrides the config's CI retries, so every failed
# repetition reports as unexpected and fails the step.
- name: Repeat changed smoke journeys
env:
PLAYWRIGHT_JSON_OUTPUT_FILE: playwright-report.json
SELECTED: ${{ needs.flake-select.outputs.playwright }}
SHARD: ${{ matrix.shard }}
SHARD_TOTAL: ${{ needs.flake-select.outputs.playwright_shard_total }}
run: cd desktop && pnpm exec playwright test --project=smoke --repeat-each=10 --retries=0 --shard="$SHARD/$SHARD_TOTAL" --reporter=list,json $SELECTED
- name: Summarize repetitions
if: ${{ !cancelled() }}
env:
SHARD: ${{ matrix.shard }}
SHARD_TOTAL: ${{ needs.flake-select.outputs.playwright_shard_total }}
run: node scripts/flake-check.mjs playwright-summary desktop/playwright-report.json "Flake check (Playwright smoke, $SHARD/$SHARD_TOTAL)"
- name: Playwright repetition artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flake-playwright-${{ matrix.shard }}
path: |
desktop/playwright-report.json
desktop/test-results
if-no-files-found: ignore
retention-days: 7
flake-dart:
name: Flake check (Flutter)
needs: [flake-select]
if: needs.flake-select.outputs.dart != ''
runs-on: ubuntu-26.04
# Large mobile changes take about three minutes per repetition, plus SDK
# setup. Allow all ten repetitions to finish with runner variance.
timeout-minutes: 45
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
# Restores the Mobile lane's Hermit and pub caches read-only; that lane
# saves them.
- name: Compute Hermit cache key
id: hermit-bin-hash
run: |
hash="$(find ./bin ! -type d | sort | xargs openssl sha256 | openssl sha256 -r | cut -d' ' -f1)"
echo "hash=$hash" >> "$GITHUB_OUTPUT"
- name: Restore Hermit package cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.cache/hermit/pkg
key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }}
restore-keys: ${{ runner.os }}-hermit-cache-
- name: Prime Flutter SDK
run: flutter --version
- name: Restore pub cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.pub-cache
key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }}
restore-keys: pub-${{ runner.os }}-
- name: Install dependencies
run: cd mobile && flutter pub get
- name: Repeat changed Flutter tests
env:
SELECTED: ${{ needs.flake-select.outputs.dart }}
run: node scripts/flake-check.mjs dart --iterations=10 $SELECTED
- name: Flutter iteration reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flake-dart
path: test-results/flake
if-no-files-found: ignore
retention-days: 7