name: CI on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: permissions: contents: read id-token: write env: # Bump this when Tauri/Rust target artifacts capture stale absolute paths. RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria jobs: test: name: Tests & Quality Checks runs-on: macos-15 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Full history for CodeScene - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'pnpm' - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy, llvm-tools-preview - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git src-tauri/target key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}- - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Install dependencies run: pnpm install --frozen-lockfile # ── 0. Build check (catches type errors and bundler failures) ───────── - name: TypeScript type check run: pnpm exec tsc --noEmit - name: Vite build check run: pnpm build # ── 1. Coverage-backed tests ────────────────────────────────────────── # The coverage commands run the same frontend and Rust test suites, so keep # them as the canonical test lane instead of running every suite twice. - name: Bundle MCP server resources (required by Tauri build) run: node scripts/bundle-mcp-server.mjs # ── 2. Tests + coverage (enforced — fails build if thresholds not met) ─ - name: Frontend tests + coverage (≥70% lines/functions/branches/statements) run: pnpm test:coverage # Thresholds configured in vite.config.ts — exits non-zero if coverage drops - name: Rust tests + coverage (≥85% lines) run: | cargo llvm-cov \ --manifest-path src-tauri/Cargo.toml \ --ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \ --lcov \ --output-path coverage/rust.lcov \ --fail-under-lines 85 # cargo-llvm-cov exits non-zero if line coverage drops below 85% # lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable. - name: Upload coverage to Codecov if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository uses: codecov/codecov-action@v5 with: use_oidc: true fail_ci_if_error: true disable_search: true files: ./coverage/lcov.info,./coverage/rust.lcov verbose: true # OIDC avoids long-lived CODECOV_TOKEN secrets. # ── 3. Code Health (CodeScene — Hotspot + Average Code Health gates) ── # Enforces minimum floors on BOTH hotspot and average code health. # Thresholds come from .codescene-thresholds so CI and local hooks match. - name: Code Health gates env: CODESCENE_PAT: ${{ secrets.CODESCENE_PAT }} CODESCENE_PROJECT_ID: ${{ secrets.CODESCENE_PROJECT_ID }} run: | HOTSPOT_THRESHOLD=$(grep '^HOTSPOT_THRESHOLD=' .codescene-thresholds | cut -d= -f2) AVERAGE_THRESHOLD=$(grep '^AVERAGE_THRESHOLD=' .codescene-thresholds | cut -d= -f2) API_RESPONSE=$(curl -sf \ -H "Authorization: Bearer $CODESCENE_PAT" \ -H "Accept: application/json" \ "https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID") HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])") AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])") echo "Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_THRESHOLD)" echo "Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_THRESHOLD)" python3 -c " hotspot = float('$HOTSPOT_SCORE') average = float('$AVERAGE_SCORE') ht = float('$HOTSPOT_THRESHOLD') at = float('$AVERAGE_THRESHOLD') failed = False if hotspot < ht: print(f'❌ Hotspot Code Health {hotspot:.2f} is below threshold {ht}') failed = True else: print(f'✅ Hotspot Code Health {hotspot:.2f} ≥ {ht}') if average < at: print(f'❌ Average Code Health {average:.2f} is below threshold {at}') failed = True else: print(f'✅ Average Code Health {average:.2f} ≥ {at}') if failed: exit(1) " # ── 4. Documentation check (warning only — does not fail build) ─────── - name: Check docs are updated continue-on-error: true run: | if git log -1 --pretty=%B | grep -i '\[skip docs\]' > /dev/null; then echo "⏭️ Documentation check skipped" exit 0 fi if git diff --name-only origin/main | grep -E '^(src/|src-tauri/)' > /dev/null; then if ! git diff --name-only origin/main | grep -E '^docs/' > /dev/null; then echo "⚠️ Code files changed but docs/ not updated" git diff --name-only origin/main | grep -E '^(src/|src-tauri/)' echo "If this change affects architecture/abstractions/theme documented in docs/, update them." echo "To suppress: include [skip docs] in your commit message." fi fi echo "✅ Documentation check passed" # ── 5. Lint & format ────────────────────────────────────────────────── - name: Lint frontend run: pnpm lint - name: Clippy (Rust) run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings - name: Format check (Rust) run: cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check linux-build: name: Linux build verification runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install Tauri Linux system dependencies run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libsoup-3.0-dev \ libxdo-dev \ libssl-dev \ libayatana-appindicator3-dev \ librsvg2-dev \ patchelf \ build-essential \ file - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'pnpm' - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: components: clippy - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git src-tauri/target key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}- - name: Install dependencies run: pnpm install --frozen-lockfile - name: Frontend build run: pnpm build - name: Cargo check run: cargo check --manifest-path=src-tauri/Cargo.toml - name: Clippy run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings