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

469 lines
20 KiB
YAML

name: CI
on:
push:
branches: [main, release]
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
jobs:
changes:
name: Detect Changed Paths
runs-on: ubuntu-26.04
# The steps take about 75 seconds, but GitHub can finish the job minutes
# after the last step. A 2-minute limit cancelled finished runs.
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
rust: ${{ steps.selection.outputs.rust }}
desktop: ${{ steps.selection.outputs.desktop }}
desktop-rust: ${{ steps.selection.outputs.desktop-rust }}
web: ${{ steps.selection.outputs.web }}
mobile: ${{ steps.selection.outputs.mobile }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 2
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
# The first call of each Hermit tool downloads it. A stalled Node download
# hung the selection step until this job's cap cancelled every gate. One
# invocation fetches both tools, so its worst case (under 6 minutes)
# still leaves the rest of the job inside the 10-minute cap.
- name: Fetch Hermit tools
run: scripts/ci-hermit-retry.sh sh -c './bin/node --version && ./bin/just --version'
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
# PR files come from GitHub's PR diff. A tokenless comparison uses the
# checked-out merge commit and can include newer, unrelated base changes.
token: ${{ github.token }}
# Root and docs/ Markdown do not select runtime suites. Markdown under
# runtime directories remains covered by the directory filters below.
filters: |
rust:
- 'crates/**'
- 'migrations/**'
- 'schema/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '.config/nextest.toml'
- 'scripts/postgres-test-*.sh'
- 'scripts/reconcile-schema-after-pgschema.sql'
- 'scripts/repair-events-pkey.sql'
- 'scripts/start-relay-for-tests.sh'
- 'bin/pgschema'
- 'bin/.pgschema-*.pkg'
- 'scripts/check-postgres-test-discovery.py'
- 'scripts/test-postgres-test-discovery.sh'
- 'scripts/test-postgres-test-wrapper.sh'
- 'deny.toml'
- '.github/workflows/ci.yml'
- '.github/workflows/_ci-*.yml'
- 'docker-compose.yml'
- 'docker-compose.ci.yml'
- 'scripts/run-tests.sh'
- 'scripts/model-capabilities.json'
- 'scripts/normative-corpus.json'
- 'scripts/ci-apt-retry.sh'
- 'scripts/ci-hermit-retry.sh'
- 'Justfile'
desktop:
- '.github/workflows/ci.yml'
- '.github/workflows/_ci-desktop.yml'
- 'scripts/ci-apt-retry.sh'
- 'Justfile'
- 'scripts/generate-macos-icon.py'
- 'scripts/macos-icon.sh'
- 'bin/uv'
- 'bin/.uv-*.pkg'
- 'scripts/model-capabilities.json'
- 'scripts/normative-corpus.json'
- 'desktop/**'
- 'pnpm-lock.yaml'
- 'scripts/flake-check.mjs'
- 'scripts/flake-check.test.mjs'
- '.github/workflows/_ci-flake.yml'
desktop-rust:
- 'desktop/src-tauri/**'
web:
- 'web/**'
- 'pnpm-lock.yaml'
mobile:
- 'mobile/**'
- 'Justfile'
- 'scripts/test-mobile-gateway-recipes.sh'
- 'scripts/mobile-release.sh'
- 'scripts/mobile-release-source.sh'
- 'scripts/mobile-worktree-overrides.sh'
- 'scripts/mobile-worktree-clean.sh'
- 'scripts/release-rulesets.sh'
- 'scripts/test-mobile-release-contract.sh'
- 'scripts/test-mobile-worktree-overrides.sh'
- 'scripts/ci-hermit-retry.sh'
- '.github/workflows/ci.yml'
- '.github/workflows/_ci-*.yml'
- name: Select runtime suites conservatively at the PR file-list limit
id: selection
env:
FILTER_OUTPUTS: ${{ toJSON(steps.filter.outputs) }}
run: node scripts/ci-runtime-selection.mjs
- name: Validate PostgreSQL test discovery
if: github.event_name == 'push' || steps.selection.outputs.rust == 'true'
run: |
scripts/test-postgres-test-discovery.sh
scripts/test-postgres-test-wrapper.sh
- name: Release workflow source contract
run: scripts/test-release-ref-contract.sh
- name: Relay image eligibility contract
run: scripts/test-relay-image-eligibility-workflow.sh
- name: Desktop release candidate contract
run: scripts/test-desktop-release-candidate.sh
- name: OSS desktop promotion contract
run: |
scripts/test-oss-desktop-promotion.sh
scripts/test-oss-desktop-promotion-behavior.sh
- name: Mobile release contract
run: scripts/test-mobile-release-contract.sh
- name: Desktop instance environment contract
run: scripts/test-desktop-instance-detection.sh
- name: Mobile worktree identity contract
run: scripts/test-mobile-worktree-overrides.sh
- name: Codex security review contract
run: just security-review-check
- name: Rust cache contract
run: |
scripts/test-rust-cache-contract.sh
scripts/test-rust-cache-contract-regressions.sh
- name: SQLx dependency pin
run: |
python3 scripts/check-sqlx-pin.py
python3 scripts/test-sqlx-pin.py
- name: CI required-context isolation contract
run: scripts/test-ci-required-context-isolation.sh
- name: CI path selection regression tests
run: node --test scripts/ci-selection.test.mjs scripts/ci-required-gates.test.mjs scripts/ci-apt-retry.test.mjs scripts/ci-hermit-retry.test.mjs
- name: File size policy
run: just file-size-check
dead-token-guard:
name: Dead Token Reference Guard
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Check for dead API token references in client code
run: |
# Fail if dead API token patterns reappear in desktop, mobile, docs, or config.
# Relay crates are excluded — they still use token auth internally.
PATTERNS='TokenScope|MintTokenResponse|hasApiToken|spr_tok_'
PATHS='desktop/src/ desktop/tests/ mobile/test/ mobile/lib/ .env.example'
EXCLUDES='--exclude-dir=node_modules --exclude-dir=.dart_tool'
if grep -rn $EXCLUDES -E "$PATTERNS" $PATHS 2>/dev/null; then
echo "::error::Dead API token references found in client code. See above."
exit 1
fi
echo "No dead token references found."
rust:
name: Rust
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true'
uses: ./.github/workflows/_ci-rust.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
lane: required
rust-cross-compile-domain:
name: Rust Cross-Compile
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-rust.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
lane: cross-compile
desktop-domain:
name: Desktop Domain
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-desktop.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop: ${{ needs.changes.outputs.desktop == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
relay-artifacts-domain:
name: Relay Artifact Producer
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-relay.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop: ${{ needs.changes.outputs.desktop == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
lane: artifacts
postgres-domain:
name: PostgreSQL Domain
needs: [changes, relay-artifacts-domain]
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-relay.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop: ${{ needs.changes.outputs.desktop == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
lane: postgres
desktop-macos-domain:
name: Desktop macOS Domain
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-desktop-macos.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop: ${{ needs.changes.outputs.desktop == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
relay-domain:
name: Relay and PostgreSQL
needs: [changes, relay-artifacts-domain]
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-relay.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
desktop: ${{ needs.changes.outputs.desktop == 'true' }}
desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }}
lane: required
clients:
name: Clients
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.web == 'true' || needs.changes.outputs.mobile == 'true'
uses: ./.github/workflows/_ci-clients.yml
with:
web: ${{ needs.changes.outputs.web == 'true' }}
mobile: ${{ needs.changes.outputs.mobile == 'true' }}
lane: required
mobile-swift-domain:
name: Mobile Swift Domain
needs: [changes]
if: needs.changes.outputs.mobile == 'true'
uses: ./.github/workflows/_ci-clients.yml
with:
web: ${{ needs.changes.outputs.web == 'true' }}
mobile: ${{ needs.changes.outputs.mobile == 'true' }}
lane: mobile-swift
# Advisory flake check: repeats the tests a pull request changes. Its lanes
# skip when no test changed and none of the required gates below reads them;
# their own statuses and step summaries carry the result.
flake-check:
name: Flake Check
if: github.event_name == 'pull_request'
uses: ./.github/workflows/_ci-flake.yml
security-domain:
name: Security Domain
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
uses: ./.github/workflows/_ci-security.yml
with:
rust: ${{ needs.changes.outputs.rust == 'true' }}
rust-lint:
name: Rust Lint
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true')))
needs: [changes, rust]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Rust Lint result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.rust.outputs.rust_lint_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
unit-tests:
name: Unit Tests
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, rust]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Unit Tests result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.rust.outputs.unit_tests_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
windows-rust:
name: Windows Rust (x86_64-pc-windows-msvc)
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true')))
needs: [changes, rust]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Windows Rust result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.rust.outputs.windows_rust_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
desktop:
name: Desktop
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, desktop-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.desktop-domain.outputs.desktop_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
desktop-build-macos:
name: Desktop Build (macOS)
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, desktop-macos-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop Build (macOS) result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.desktop-macos-domain.outputs.desktop_macos_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
desktop-e2e-relay:
name: Desktop E2E Relay
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop E2E Relay result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-artifacts-domain.outputs.desktop_e2e_relay_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
desktop-e2e-integration:
name: Desktop E2E Integration
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, relay-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop E2E Integration result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-domain.outputs.desktop_e2e_integration_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
backend-integration:
name: Backend Integration (relay e2e)
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, relay-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Backend Integration result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-domain.outputs.backend_integration_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
postgres-tests:
name: PostgreSQL Tests
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, postgres-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check PostgreSQL Tests result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.postgres-domain.outputs.postgres_tests_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
relay-e2e:
name: Relay E2E
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, relay-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Relay E2E result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-domain.outputs.relay_e2e_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
web:
name: Web
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.web == 'true')))
needs: [changes, clients]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Web result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.clients.outputs.web_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
mobile:
name: Mobile
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.mobile == 'true')))
needs: [changes, clients]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Mobile result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.clients.outputs.mobile_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
security:
name: Security
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, security-domain]
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check Security result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.security-domain.outputs.security_result }}
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success