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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.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 ./... # Tag-gated tests (DC-06 / T-2026-07-25-16). The build-tag matrix above # only COMPILES the otel/wazero variants; the tests behind those tags # (plugin/sandbox_wazero_test.go, telemetry/telemetry_otel_test.go) ran # nowhere until this step. Scoped to the two packages that carry tagged # files — every other package is tag-invariant and already covered by the # race run above. One leg is enough; no -race (the runtime under the tag # is the concern, not new concurrency). - name: Run tag-gated tests (-tags wazero, -tags otel) if: matrix.os == 'ubuntu-latest' run: | go test -tags wazero -count=1 ./plugin/... go test -tags otel -count=1 ./telemetry/... - 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 # verify: false — the action's default `config verify` pass fetches # golangci-lint.run's JSONSchema over HTTPS before linting anything, so a # timeout on that host fails a required job having run zero linters (it # took main red on d352696). `golangci-lint run` rejects a bad config on # its own; the schema pass only bought a prettier error message, priced # at a third-party site inside the gate. - name: Lint uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: version: v2.11.3 working-directory: Server/ verify: false # ubuntu-latest deliberately: the client TS code has zero win32-conditional # paths (no process.platform / path.sep branches in src or the unit suites), # prettier pins endOfLine: lf and .gitattributes forces eol=lf, so a Windows # runner adds queue time without adding coverage. Windows-specific behavior # is covered where it exists: rust-tests and the tauri-build matrix. client-check: name: Client Static Checks runs-on: ubuntu-latest defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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 # 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: TypeScript check (Playwright specs) # The main tsconfig excludes tests/e2e from the app graph; this # project typechecks the 47 spec files + fixtures + the three # playwright configs so type rot cannot hide there. run: npx tsc -p tsconfig.e2e.json --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) # Blocking since the 2026-08-04 remediation: the '|| true' era let a # real unused-export finding sit invisible in every green run. run: npx knip # 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. # ubuntu-latest for the same reason as client-check above: jsdom-only vitest # with no platform-conditional code under test. client-tests: name: Client Unit Tests runs-on: ubuntu-latest defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 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. 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. # # BLOCKING since 2026-08-05 (DC-07): the post-repair soak recorded green # full-suite runs at 270, 276 and 291 tests across the 08-04/08-05 audit # branches, and the one hard CI failure in that window was a real spec bug # (updater install-settle race), which a non-blocking job would have let # rot. retries: 2 absorbs the known rare flake class (see E2E-ISSUES.md's # flake accounting). # 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) runs-on: ubuntu-latest timeout-minutes: 25 defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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 # Admin-panel journey against a REAL server (no mocks): start-server.sh # builds the Go binary and boots it with a fresh temp data dir, and the # suite drives the embedded SPA through the first-run wizard, dashboard, # channel CRUD, audit log and re-login — the one DC-04 surface the mocked # suites cannot reach. Non-blocking while it earns its soak, same # graduation convention client-e2e followed. # GRADUATION CRITERION (recorded 2026-08-15): flip continue-on-error to # false once the job has ~30 consecutive green runs on main with no # infra-flake reruns — the same evidence bar client-e2e cleared (270+ green # runs cited in docs/audit-2026-08-04-docs-and-coverage.md) scaled to this # job's lower traffic. Check with: gh run list -w CI -b main --json # conclusion | jq '[.[] | .conclusion] | index("failure")'. admin-e2e: name: Admin Panel E2E (real server, non-blocking) runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 20 defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version: "1.26" cache-dependency-path: Server/go.sum - 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 admin-panel journey run: npx playwright test --config=playwright.config.admin.ts - name: Upload Playwright report if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: admin-e2e-report path: | Client/tauri-client/playwright-report/ Client/tauri-client/test-results/ retention-days: 7 # Blocking e2e subset: the parity-feature specs (tagged "@parity"), covering # the wire paths added in v1.2.0 (mentions/badges, per-channel mute, NSFW # gate, group DMs, role change, custom-emoji autocomplete, voice moderation). # These are new and authored green, so unlike the full legacy suite above they # gate PRs: a regression on one of these features must fail CI. Kept as its own # job (not folded into the non-blocking suite) so the legacy suite can keep # earning its "few green pushes" before it too graduates to blocking. client-e2e-parity: name: Client E2E (parity subset, blocking) runs-on: ubuntu-latest timeout-minutes: 15 defaults: run: working-directory: Client/tauri-client/ steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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 parity e2e specs run: npx playwright test --config=playwright.config.ts --grep "@parity" - name: Upload Playwright report if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: playwright-report-parity 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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build image (no push) uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 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). # # Skipped for Dependabot: its PRs run under the separate `dependabot` secrets # scope, so TAURI_SIGNING_PRIVATE_KEY arrives empty and `npm run tauri build` # always aborts with "failed to decode secret key" while signing the updater # artifact — after a successful compile and bundle. That burned ~50 min of # runner time per dependency PR to produce a red check that never carried any # signal. Granting Dependabot the signing key would fix the symptom but hands # a release key to workflows triggered by third-party dependency updates. # # What still covers Dependabot PRs: the required `rust-tests` job compiles the # crate (cargo clippy --all-targets + cargo test --lib), so a dependency bump # that breaks the Rust build is still caught. # What this gives up on those PRs: bundling (NSIS/AppImage/deb), Windows and # ARM-specific compilation, and the `cargo audit` step below — that last one # overlaps with Dependabot's own cargo scanning, which is what opens these PRs # in the first place. tauri-build: name: Tauri Full Build (${{ matrix.os }}) needs: client-check if: >- github.event_name == 'pull_request' && github.base_ref == 'main' && github.actor != 'dependabot[bot]' 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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: Client/tauri-client/src-tauri - name: Install npm dependencies run: npm ci - 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