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 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-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"