feat: CLI backend auth and same-account iroh auto-trust #5421
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| # Run full CI when code lands on main (or other long-lived branches) | |
| push: | |
| branches: | |
| - main | |
| # Run CI for all PR updates without duplication from push events | |
| pull_request: | |
| types: [opened, reopened, synchronize, ready_for_review] | |
| branches: ['**'] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Detect file changes to optimize CI performance (THU-28) | |
| # Skip Rust build step when no Rust-related files have changed | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| rust: ${{ steps.filter.outputs.rust }} | |
| agent-core: ${{ steps.filter.outputs.agent-core }} | |
| cli: ${{ steps.filter.outputs.cli }} | |
| wasm-artifact: ${{ steps.filter.outputs.wasm-artifact }} | |
| crate-src: ${{ steps.filter.outputs.crate-src }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Detect changed files | |
| id: filter | |
| uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 | |
| with: | |
| filters: | | |
| rust: | |
| - 'src-tauri/**/*.rs' | |
| - 'src-tauri/**/Cargo.toml' | |
| - 'src-tauri/**/Cargo.lock' | |
| - 'src-tauri/build.rs' | |
| - 'src-tauri/rust-toolchain.toml' | |
| - 'src-tauri/.cargo/**' | |
| - 'crates/**' | |
| agent-core: | |
| - 'shared/agent-core/**' | |
| cli: | |
| - 'cli/**' | |
| wasm-artifact: | |
| - 'src/acp/iroh/pkg/**' | |
| # Inputs that change the compiled wasm (README.md excluded — it doesn't). | |
| # Pairs with the wasm-artifact staleness gate: touching any of these MUST | |
| # ship a regenerated src/acp/iroh/pkg in the same PR. | |
| crate-src: | |
| - 'crates/thunderbolt-acp-client/src/**' | |
| - 'crates/thunderbolt-acp-client/Cargo.toml' | |
| - 'crates/thunderbolt-acp-client/Cargo.lock' | |
| - 'crates/thunderbolt-acp-client/rust-toolchain.toml' | |
| - 'crates/thunderbolt-acp-client/.cargo/**' | |
| - 'crates/thunderbolt-acp-client/build.sh' | |
| typescript: | |
| runs-on: ubuntu-latest | |
| # Happy path is a few minutes; the GitHub default is 360min (6h). A 20min cap | |
| # kills a CPU-starved runner ~18x sooner and frees the cancel-in-progress slot | |
| # instead of blocking the concurrency group for hours. | |
| timeout-minutes: 20 | |
| permissions: | |
| pull-requests: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '20' | |
| - name: Install Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.14 | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| key: bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Type check | |
| run: bun run tsc --noEmit | |
| - name: Lint | |
| run: bun run lint | |
| - name: Run tests | |
| run: bun run test:5x | |
| # Upload source maps to PostHog only for main branch | |
| - name: Build application with source maps | |
| if: github.ref == 'refs/heads/main' | |
| run: bun run build | |
| env: | |
| NODE_ENV: production | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| ENABLE_SOURCEMAP: 'true' | |
| - name: Inject & upload source maps to PostHog | |
| if: github.ref == 'refs/heads/main' | |
| uses: PostHog/upload-source-maps@e798a054427efc710af080354f8450d3c154c584 # v0.4.6 | |
| with: | |
| directory: dist | |
| env-id: ${{ secrets.POSTHOG_CLI_ENV_ID }} | |
| cli-token: ${{ secrets.POSTHOG_CLI_TOKEN }} | |
| - name: Delete source maps after upload | |
| if: github.ref == 'refs/heads/main' | |
| run: find dist -name '*.map' -delete | |
| - name: Compute and save metrics baseline | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| bun test --cwd=src --timeout 3600000 --coverage 2>&1 | tee /tmp/coverage.txt || true | |
| COVERAGE=$(grep -E "All files" /tmp/coverage.txt | grep -oE '[0-9]+\.[0-9]+' | head -1) | |
| bunx size-limit --json > /tmp/size-limit.json 2>/dev/null || true | |
| BUNDLE=$(jq -r '.[0].size // 0' /tmp/size-limit.json 2>/dev/null || echo 0) | |
| mkdir -p .metrics-baseline | |
| printf '{"bundleSize":%s,"coverage":"%s"}\n' "${BUNDLE:-0}" "${COVERAGE:-N/A}" > .metrics-baseline/metrics.json | |
| - name: Cache metrics baseline | |
| if: github.ref == 'refs/heads/main' | |
| uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: .metrics-baseline | |
| key: pr-metrics-main-${{ github.sha }} | |
| # shared/agent-core is an isolated module (also consumed by cli/). Its unit tests | |
| # are NOT part of the default `bun run test` — they only need to run when the | |
| # module itself changes, so gate them on the agent-core path filter. Mirrors the | |
| # `rust` job's detect-changes pattern and the `typescript` job's Bun setup. | |
| agent-core: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.agent-core == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '20' | |
| - name: Install Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.14 | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| key: bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Run agent-core tests (5x) | |
| run: bun run test:agent-core:5x | |
| # cli/ is a self-contained package (own bun.lock, no imports from src/ or shared/), | |
| # so its tests and typecheck aren't reached by the `typescript` job. Gate on the | |
| # `cli/**` path filter — same detect-changes mechanism as the `rust` and | |
| # `agent-core` jobs — so it runs only when cli/ changes. | |
| cli: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.cli == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.14 | |
| - name: Cache Bun dependencies (cli) | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| cli/node_modules | |
| key: cli-bun-${{ hashFiles('cli/bun.lock') }} | |
| restore-keys: | | |
| cli-bun- | |
| - name: Install cli dependencies | |
| run: cd cli && bun install --frozen-lockfile | |
| - name: Type check cli | |
| run: cd cli && bun run typecheck | |
| - name: Run cli tests (5x) | |
| run: cd cli && bun run test:5x | |
| # The iroh ACP client (P2P/QUIC crypto core) ships as a prebuilt wasm artifact at | |
| # src/acp/iroh/pkg so the web app imports it without a wasm toolchain in CI. The | |
| # build remaps absolute cargo/repo paths for local reproducibility; cross-platform | |
| # Linux/macOS reproducibility remains unproven. CI therefore enforces provenance | |
| # two ways without rebuilding or needing a wasm toolchain: | |
| # 1. Staleness gate — a change to the crate source MUST ship a regenerated pkg/ in | |
| # the same PR, so editing the crate can't silently leave the committed wasm stale | |
| # (the `rust` job host-compiles the crate but never touches the binary). | |
| # 2. Tamper-evidence — the committed pkg/ must match its CHECKSUMS.txt manifest. | |
| # Reproduce a bit-identical artifact locally on the pinned toolchain with | |
| # `crates/thunderbolt-acp-client/build.sh --verify` (see that crate's README). | |
| wasm-artifact: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.wasm-artifact == 'true' || needs.detect-changes.outputs.crate-src == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| # Crate source changed but the committed pkg/ didn't — the wasm wasn't rebuilt, | |
| # so it's now stale relative to its source. Fail with the fix instruction. | |
| - name: Guard against stale wasm artifact | |
| if: needs.detect-changes.outputs.crate-src == 'true' && needs.detect-changes.outputs.wasm-artifact != 'true' | |
| run: | | |
| echo "::error::crates/thunderbolt-acp-client changed but src/acp/iroh/pkg was not regenerated — the committed wasm is stale." | |
| echo "Rebuild and commit the artifact in the same change: crates/thunderbolt-acp-client/build.sh (then commit src/acp/iroh/pkg)." | |
| exit 1 | |
| - name: Verify committed wasm artifacts match checksums manifest | |
| if: needs.detect-changes.outputs.wasm-artifact == 'true' | |
| run: | | |
| cd src/acp/iroh/pkg | |
| diff -u \ | |
| <(awk '{ print $2 }' CHECKSUMS.txt | sort) \ | |
| <(find . -maxdepth 1 -type f ! -name CHECKSUMS.txt -exec basename {} \; | sort) | |
| shasum -a 256 -c CHECKSUMS.txt | |
| rust: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.rust == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install Rust toolchain | |
| uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 | |
| with: | |
| components: rustfmt, clippy | |
| cache: false | |
| - name: Cache cargo registry | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: | | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| key: ${{ runner.os }}-cargo-registry-${{ hashFiles('src-tauri/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-registry- | |
| # Security tripwire: fail if the unsound glib::VariantStrIter path | |
| # (GHSA-wrw7-89jp-8q8g) ever gains a caller in our code or any dependency. | |
| # Cheap (cargo metadata + grep, no build), so it runs before the build. | |
| - name: Guard glib VariantStrIter unsoundness (RUSTSEC-2024-0429) | |
| run: ./scripts/check-glib-variantstriter.sh | |
| - name: Cache cargo build | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: src-tauri/target/ | |
| key: ${{ runner.os }}-cargo-build-${{ hashFiles('src-tauri/Cargo.lock') }}-${{ hashFiles('src-tauri/src/**/*.rs') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-build-${{ hashFiles('src-tauri/Cargo.lock') }}- | |
| ${{ runner.os }}-cargo-build- | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| build-essential \ | |
| curl \ | |
| wget \ | |
| file \ | |
| libxdo-dev \ | |
| libssl-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev | |
| - name: Build and check Rust code | |
| env: | |
| CARGO_INCREMENTAL: 0 | |
| RUSTC_WRAPPER: '' | |
| run: | | |
| cd src-tauri | |
| cargo build --all-targets | |
| cargo clippy --all-targets -- -D warnings | |
| cargo test | |
| # crates/ holds the standalone thunderbolt-acp-client crate (own Cargo.toml | |
| # and Cargo.lock, not a src-tauri workspace member), so the src-tauri build | |
| # above never compiles it. Its wasm cdylib is the deployed artifact; the rlib | |
| # target lets these host-target checks run without a wasm toolchain — the | |
| # .cargo/config.toml rustflags are scoped to wasm32, so no --target override. | |
| - name: Build and check ACP client crate | |
| env: | |
| CARGO_INCREMENTAL: 0 | |
| RUSTC_WRAPPER: '' | |
| run: | | |
| cd crates/thunderbolt-acp-client | |
| cargo build --all-targets | |
| cargo clippy --all-targets -- -D warnings | |
| cargo test | |
| backend: | |
| runs-on: ubuntu-latest | |
| # Healthy run is ~130s; a CPU-starved one was observed at 4920s (~82min). A | |
| # 15min cap = >6x headroom over the worst legitimate run (even with the 5x | |
| # rerun gate) while killing a starved run ~24x sooner than the 360min default. | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install Bun | |
| # Pinned to 1.3.13 to match main (green). The 1.3.14 bump (a5bbd883) was for | |
| # the frontend --isolate work that Option A dropped; on the backend job 1.3.14 | |
| # correlated with intermittent hard hangs inside PGlite's WASM (the 5s test | |
| # timeout couldn't even fire — 94s/10min stalls), never seen on main's 1.3.13. | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.13 | |
| - name: Cache Bun dependencies (backend) | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| backend/node_modules | |
| key: backend-bun-${{ hashFiles('backend/bun.lock') }} | |
| restore-keys: | | |
| backend-bun- | |
| - name: Install backend dependencies | |
| run: | | |
| cd backend | |
| bun install --frozen-lockfile | |
| - name: Type check backend | |
| run: | | |
| cd backend | |
| bun run type-check | |
| - name: Lint backend | |
| run: | | |
| cd backend | |
| bun run lint | |
| - name: Test backend (5x, excludes same-process WS event-delivery flakes) | |
| timeout-minutes: 10 | |
| # No per-test timeout in CI (large value ≈ disabled — bun `--timeout 0` hangs async tests): | |
| # the 10-minute step cap above is the real ceiling, so tests that only get slow under | |
| # CI/PGlite contention don't spuriously fail. Local `test:backend` keeps the 5s timeout. | |
| run: cd backend && bun test --timeout 3600000 --randomize --rerun-each 5 $(find src -name '*.test.ts' -not -path '*proxy/ws-e2e.test.ts' -not -path '*haystack/routes.test.ts') | |
| # ws-e2e.test.ts + haystack/routes.test.ts wait on Bun same-process WS close/ | |
| # message events that Bun drops/delays under load. Systematic causes (shared | |
| # PGlite, missing event waits, strict close codes) are already fixed; the | |
| # residual is irreducible same-process WS event-drop. Run them ONCE (no | |
| # --rerun-each, which 5x'd the exposure) and retry up to 5x so an intermittent | |
| # drop self-heals while a genuine break (5 non-zero) still reds CI. | |
| - name: Test backend (segregated WS, 1x + retry) | |
| uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 | |
| with: | |
| timeout_minutes: 5 | |
| max_attempts: 5 | |
| retry_wait_seconds: 5 | |
| retry_on: error | |
| command: bun run test:backend:ws |