name: CI on: # dev is deliberately not a push trigger: while a dev -> main PR is open every # push to dev already fires pull_request(synchronize), so listing it here ran # the whole suite twice for one push. Use workflow_dispatch for a dev branch # with no PR open yet. push: branches: [main] pull_request: branches: [main, dev] workflow_dispatch: # Cancel in-progress runs for the same branch/PR concurrency: group: ci-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: server-build-test: name: Server Build & Test (${{ matrix.os }}) strategy: fail-fast: false matrix: include: - os: windows-latest binary: chatserver.exe - os: ubuntu-latest binary: chatserver runs-on: ${{ matrix.os }} defaults: run: working-directory: Server/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: "1.26" cache-dependency-path: Server/go.sum - name: Build server run: go build -o ${{ matrix.binary }} -ldflags "-s -w" . # Phase B + C build-tag matrix. Each tag variant must compile so the # tag boundaries don't drift. - name: Build with -tags otel (Phase B Step 8) run: go build -tags otel ./... - name: Build with -tags wazero (Phase C Step 9) run: go build -tags wazero ./... - name: Build with -tags otel,wazero (full community-hub build) run: go build -tags otel,wazero ./... - name: Go vulnerability check run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 && govulncheck ./... # Generated sqlc output must never drift from db/queries/. One leg of # the matrix is enough; make is not guaranteed on the Windows runner. - name: Verify generated sqlc output (make sqlc-verify) if: matrix.os == 'ubuntu-latest' run: make sqlc-install sqlc-verify # Protocol message-type constants (Go + TS) must never drift from # docs/protocol-schema.json — the single source of truth. - name: Verify generated protocol constants (make protocol-verify) if: matrix.os == 'ubuntu-latest' run: make protocol-verify - name: Run tests with race detection and coverage run: go test -race -timeout 20m ./... -coverprofile=coverage.out -cover - name: Run tests with deadlock detection run: go test -tags deadlock -count=1 ./... - name: Upload Go coverage if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: go-coverage-${{ matrix.os }} path: Server/coverage.out retention-days: 7 - name: Lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: v2.11.3 working-directory: Server/ client-check: name: Client Static Checks runs-on: windows-latest defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: npm cache-dependency-path: Client/tauri-client/package-lock.json - name: Install npm dependencies run: npm ci - name: Patch auto-generated Tauri TypeScript bindings working-directory: Client/tauri-client/ # tauri-typegen generates an Event type that is intentionally unused in app code. # Rename it to _Event so @typescript-eslint/no-unused-vars does not fail. run: | node -e " const fs = require('fs'); const p = 'src/generated/events.ts'; if (fs.existsSync(p)) { let c = fs.readFileSync(p, 'utf8'); c = c.replace(/^type Event\b/gm, 'type _Event').replace(/^interface Event\b/gm, 'interface _Event'); c = c.replace(/,\s*type Event\s*(?=\})/g, ' '); // unused named import from @tauri-apps/api/event fs.writeFileSync(p, c); console.log('Patched: renamed Event -> _Event in generated/events.ts'); } else { console.log('src/generated/events.ts not found, skipping patch.'); } " # Scoped to shipped dependencies. The remaining high findings are all one # advisory, brace-expansion <=5.0.7, reaching us only through dev tooling # (eslint, @vitest/coverage-v8, stryker). Those are already on their # latest versions, so no bump reaches the fix, and there is no patched # release in the 1.x/2.x lines they pin. Forcing every copy to 5.0.9 via # overrides was tried and broke the build: minimatch requires # brace-expansion as CJS and v5 is not callable that way, which took out # vitest's coverage provider. Nothing here ships to users; revisit when # eslint and @vitest/coverage-v8 widen their minimatch ranges. - name: Security audit (npm, shipped deps) run: npm audit --omit=dev --audit-level=high - name: Oxlint (fast correctness checks) run: npx oxlint src/ - name: TypeScript check run: npx tsc --noEmit - name: ESLint (type-aware rules) run: npx eslint src/ - name: Prettier format check run: npx prettier --check "src/**/*.ts" "tests/**/*.ts" - name: Knip (unused code & deps) run: npx knip || true # Unit tests live in their own job so a suite failure is visible as exactly one # failing check instead of masking the static gates above. The suite is GREEN # and must stay green — never "fix" a failing test by editing its assertions. client-tests: name: Client Unit Tests runs-on: windows-latest defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: npm cache-dependency-path: Client/tauri-client/package-lock.json - name: Install npm dependencies run: npm ci - name: Run unit tests with coverage run: npx vitest run --coverage --reporter=default - name: Upload client coverage if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: client-coverage path: Client/tauri-client/coverage/ retention-days: 7 # Rust unit tests used to live inside tauri-build, which only runs on PRs to # main — so #[cfg(test)] code never ran on pushes or on PRs to dev, and could # rot for a whole release cycle. This job runs them on every event. Clippy is # run with --all-targets here (tauri-build's lib-only clippy skips test code). rust-tests: name: Rust Unit Tests runs-on: ubuntu-22.04 timeout-minutes: 30 defaults: run: working-directory: Client/tauri-client/src-tauri/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Linux system dependencies run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ libsecret-1-dev \ libdbus-1-dev \ libasound2-dev \ libssl-dev \ librsvg2-dev - name: Install Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: components: clippy - name: Rust cache uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 with: workspaces: Client/tauri-client/src-tauri - name: Clippy lint (including test targets) run: cargo clippy --all-targets -- -D warnings - name: Rust unit tests run: cargo test --lib # Playwright e2e against the mocked-Tauri dev server. The suite is green # since the mock repair (start_http_proxy stub + voice-premise rewrite): # a full 255-test run passes locally in ~7.5 min at 1 worker. Runaway # protection lives in playwright.config.ts (maxFailures: 20 aborts a # systemic cascade early; globalTimeout: 20 min self-terminates with a # usable report) with timeout-minutes below as the outer backstop. # # Still continue-on-error for now: a newly-revived 255-test browser suite # may harbor rare flakes (retries: 2 covers them, but confidence needs a # few green pushes first). Flip this job to blocking once it has been # stably green across several pushes. # See docs/audit-test-coverage-2026-07-25.md T-2026-07-25-21. # The native config (playwright.config.native.ts) is deliberately not wired # up — it needs a real server and a built desktop binary. client-e2e: name: Client E2E (Playwright, non-blocking) runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 25 defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: npm cache-dependency-path: Client/tauri-client/package-lock.json - name: Install npm dependencies run: npm ci - name: Install Playwright browser run: npx playwright install --with-deps chromium - name: Run Playwright tests run: npx playwright test --config=playwright.config.ts - name: Upload Playwright report if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: playwright-report path: | Client/tauri-client/playwright-report/ Client/tauri-client/test-results/ retention-days: 7 # Image build is verification only, so it is skipped on dev to keep day-to-day # work on the fast check suite. Runs for main pushes and PRs targeting main. server-docker-build: name: Server Docker Build (verify) if: github.ref_name == 'main' || github.base_ref == 'main' runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Build image (no push) uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 with: context: Server/ push: false build-args: VERSION=ci cache-from: type=gha cache-to: type=gha,mode=max # Full Tauri build only on PRs to main (expensive: ~15 min x2 multiplier) tauri-build: name: Tauri Full Build (${{ matrix.os }}) needs: client-check if: github.event_name == 'pull_request' && github.base_ref == 'main' strategy: fail-fast: false matrix: include: - os: windows-latest - os: ubuntu-22.04 - os: ubuntu-22.04-arm runs-on: ${{ matrix.os }} defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: npm cache-dependency-path: Client/tauri-client/package-lock.json - name: Install Linux system dependencies if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ libsecret-1-dev \ libdbus-1-dev \ libasound2-dev \ libssl-dev \ patchelf \ librsvg2-dev \ xdg-utils - name: Install Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: components: clippy - name: Rust cache uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 with: workspaces: Client/tauri-client/src-tauri - name: Install npm dependencies run: npm ci - name: Install tauri-typegen run: cargo install tauri-typegen@0.5.0 --quiet - name: Generate TypeScript IPC bindings working-directory: Client/tauri-client/ run: cargo tauri-typegen generate - name: Fix generated TypeScript bindings (tauri-typegen 0.5.0 workaround) working-directory: Client/tauri-client/ # tauri-typegen 0.5.0 cannot map serde_json::Value to a TS type — patch post-generation. # Duplicate events are avoided at source by using one emit() call site per event name. run: | node -e " const fs = require('fs'); const tp = fs.readFileSync('src/generated/types.ts', 'utf8'); if (!tp.includes('export type Value')) { fs.writeFileSync('src/generated/types.ts', tp.replace( 'export interface CredentialData', 'export type Value = unknown;\n\nexport interface CredentialData' )); } console.log('Generated bindings patched.'); " - name: Clippy lint (Rust) working-directory: Client/tauri-client/src-tauri/ run: cargo clippy -- -D warnings # Rust unit tests moved to the standalone `rust-tests` job so they run on # every event, not just PRs to main. - name: Security audit (Rust dependencies) working-directory: Client/tauri-client/src-tauri/ run: | cargo install cargo-audit@0.22.1 --quiet cargo audit - name: Build Tauri app env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: npm run tauri build