Files
buzz/.github/workflows/_ci-desktop.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

340 lines
15 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-26.04
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-26.04
# A lane needs up to ~15m. ci-apt-retry.sh bounds the apt steps 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
env:
# Resolved and installed by the steps below; the .deb cache key hashes
# what apt resolves for this list.
TAURI_APT_PACKAGES: >-
build-essential
curl
file
libasound2-dev
libayatana-appindicator3-dev
libgtk-3-dev
librsvg2-dev
libssl-dev
libwebkit2gtk-4.1-dev
libxdo-dev
patchelf
wget
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' }}
# A degraded Ubuntu mirror serves .debs at ~50 KB/s, and the ~62 MB
# Tauri download (one 25.5 MB libwebkit2gtk .deb) then outlasts every
# ci-apt-retry.sh attempt. Cache the .debs in the Actions cache so the
# mirror only serves `apt-get update` and packages Ubuntu has updated
# since main last saved.
- name: Resolve Tauri apt archives
id: tauri-debs
run: |
set -euo pipefail
CI_APT_ATTEMPT_SECONDS=90 scripts/ci-apt-retry.sh sudo apt-get update
read -ra packages <<< "$TAURI_APT_PACKAGES"
# Name, size and SHA256 (from the signed index) of each .deb the
# install will fetch. The mirror URI is left out so the key doesn't
# depend on which mirror answered. Runs before the restore:
# --print-uris omits files that are already in
# /var/cache/apt/archives.
sudo apt-get install --print-uris -qq -y --no-install-recommends \
-o Acquire::ForceHash=SHA256 "${packages[@]}" |
awk '{print $2, $3, $4}' | sort > "$RUNNER_TEMP/tauri-debs.txt"
. /etc/os-release
prefix="tauri-debs-${ID}${VERSION_ID}-"
echo "prefix=$prefix" >> "$GITHUB_OUTPUT"
echo "key=$prefix$(sha256sum "$RUNNER_TEMP/tauri-debs.txt" | cut -c1-16)" >> "$GITHUB_OUTPUT"
- name: Restore Tauri apt archives
id: tauri-debs-cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.cache/tauri-debs
key: ${{ steps.tauri-debs.outputs.key }}
restore-keys: ${{ steps.tauri-debs.outputs.prefix }}
- name: Install Tauri dependencies (Linux)
id: tauri-debs-install
env:
DEBIAN_FRONTEND: noninteractive
run: |
set -euo pipefail
debs=~/.cache/tauri-debs
mkdir -p "$debs"
# apt reuses an archived .deb of the expected size without checking
# its hash, so seed only restored files whose SHA256 matches the
# signed index. apt downloads and verifies the rest. sha256sum exits
# non-zero when a file fails or none is restored; neither is fatal.
awk '{sub(/^SHA256:/, "", $3); print $3 " " $1}' "$RUNNER_TEMP/tauri-debs.txt" > "$RUNNER_TEMP/tauri-debs.sha256"
(cd "$debs" && { sha256sum --check --ignore-missing "$RUNNER_TEMP/tauri-debs.sha256" || true; }) |
sed -n 's/: OK$//p' | (cd "$debs" && xargs -r sudo cp -t /var/cache/apt/archives/)
read -ra packages <<< "$TAURI_APT_PACKAGES"
scripts/ci-apt-retry.sh sudo apt-get install -y --no-install-recommends "${packages[@]}"
# Keep exactly the resolved files, so a restore-keys fallback can't
# pile up superseded versions. The packages are installed by now, so
# a failure here only warns and skips the save.
if rm -rf "$debs" && mkdir -p "$debs" &&
awk '{print "/var/cache/apt/archives/" $1}' "$RUNNER_TEMP/tauri-debs.txt" | xargs -r cp -t "$debs"; then
echo "rebuilt=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Could not rebuild the Tauri apt archive cache; not saving it."
fi
# Like the other caches here, only pushes save; PR runs restore. All
# three lanes resolve the same key, so one lane saves it.
- name: Save Tauri apt archives
if: matrix.lane == 'core' && github.event_name == 'push' && steps.tauri-debs-cache.outputs.cache-hit != 'true' && steps.tauri-debs-install.outputs.rebuilt == 'true'
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.cache/tauri-debs
key: ${{ steps.tauri-debs.outputs.key }}
- 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-26.04
# 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-26.04
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-26.04
timeout-minutes: 5
permissions: {}
outputs:
desktop_result: ${{ needs.desktop.result }}
steps:
- run: echo "Captured Desktop job results"