Skip to content

feat: CLI backend auth and same-account iroh auto-trust #5421

feat: CLI backend auth and same-account iroh auto-trust

feat: CLI backend auth and same-account iroh auto-trust #5421

Workflow file for this run

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