Files
OwnCord/.github/workflows/ci.yml
T
J3vbandClaude Opus 5 d2e1d2deb0 fix(ci): unbreak the Docker build and the npm audit gate (#1274)
* fix(docker): build the server image with Go 1.26

The Docker verify job failed with "go.mod requires go >= 1.26 (running go
1.25.12; GOTOOLCHAIN=local)". The Go 1.26 upgrade bumped go.mod but left the
Dockerfile on golang:1.25-bookworm, and GOTOOLCHAIN=local in the base image
means it cannot download a newer toolchain.

golang:1.26-bookworm confirmed present upstream. Not verified locally (Docker
Desktop not running); the CI Docker job proves it on this PR.

* fix(client): override brace-expansion and qs to patched versions

npm audit --audit-level=high failed the Client Static Checks job with 10
vulnerabilities (8 high, 2 moderate). npm audit fix could not resolve any of
them.

There is really only one advisory behind the eight high findings:
brace-expansion <=5.0.7, a DoS via unbounded expansion length causing OOM.
minimatch, glob, test-exclude, @vitest/coverage-v8, eslint and @eslint/* were
all just transitive consumers of it, and those top-level dev deps are already
at their latest versions, so no bump reaches the fix. qs 6.11.1-6.15.1 is a
second, independent advisory arriving via @stryker-mutator/core ->
typed-rest-client.

No patch exists inside the brace-expansion 1.x or 2.x lines (the fix landed in
5.0.8), so overrides are the only route. Collapsing every copy to 5.0.9 risked
breaking minimatch 3.x, which requires it as CJS, so the whole client gate was
run to check: npm audit 0 vulnerabilities, tsc clean, oxlint unchanged
(pre-existing no-underscore-dangle warnings only), eslint exit 0, prettier
clean, and vitest 3572 tests across 129 files all passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci: stop running the suite twice for one push to dev

Listing dev under both push and pull_request meant a single push to dev fired
both events, running every job twice (visible as duplicated checks on #1274).
While a dev -> main PR is open, pull_request(synchronize) already covers each
push to dev, so dev only needs the pull_request trigger. workflow_dispatch
covers a dev branch with no PR open yet.

* chore(deps): roll up the seven open dependabot PRs

Consolidates #1267-#1273 onto this branch so they land as one CI run instead
of seven, each of which was triggering the full suite including tauri-build.

- google.golang.org/grpc 1.81.1 -> 1.82.1  (#1267)
- github.com/google/cel-go 0.28.1 -> 0.29.0 (#1268)
- defu 6.1.4 -> 6.1.7, root lockfile      (#1269)
- tauri 2.11.0 -> 2.11.1                   (#1270)
- @modelcontextprotocol/sdk 1.29 -> 1.30   (#1271)
- tar 0.4.45 -> 0.4.46                     (#1272)
- serde_with 3.18.0 -> 3.21.0              (#1273)

Applied by regenerating each lockfile from its manifest rather than merging
seven lockfile diffs.

Verified: go build across all four tag variants, go vet, govulncheck (0
vulnerabilities in called code), go test -race (14 packages, 0 failures),
cargo clippy --all-targets -D warnings, cargo test --lib (73 passed).

CI covers neither the root package.json nor tools/mcp-introspect, so those two
were checked by hand: changelogen still runs under defu 6.1.7 (release.yml
depends on it) and the introspect server still imports the 1.30 SDK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): drop the brace-expansion override, scope the audit to shipped deps

The brace-expansion override I added to clear npm audit broke Client Unit
Tests in CI:

  TypeError: (0 , brace_expansion_1.default) is not a function
    at minimatch braceExpand -> TestExclude.glob
    -> V8CoverageProvider.getUntestedFiles

minimatch requires brace-expansion as CJS and v5 is not callable that way. It
only fires under --coverage, which is why a local `vitest run` missed it; CI
runs `vitest run --coverage`. Verified the fix with that exact command.

There is no patched brace-expansion in the 1.x/2.x lines those tools pin (the
fix landed in 5.0.8), and eslint, @vitest/coverage-v8 and stryker are already
latest, so no bump reaches it. Since the whole chain is dev tooling that never
ships, the gate is now `npm audit --omit=dev --audit-level=high`, which
reports 0 vulnerabilities. The reasoning and the revisit condition are
recorded in ci.yml next to the step.

The qs override stays: qs is CJS, the override is proven safe, and it closes a
real advisory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): route all cert-tofu emits through the single call site

Tauri Full Build failed on all three platforms with the generated bindings
redeclaring onCertTofu (TS2323/TS2393), which killed `tauri build` at its
beforeBuildCommand:

  src/generated/events.ts(36,23): error TS2323: Cannot redeclare exported
  variable 'onCertTofu'.

tauri-typegen emits one onCertTofu binding per `emit("cert-tofu", ..)` call
site it finds. ws_proxy.rs already funnelled its emits through a helper for
exactly this reason -- its doc comment says so -- but http_proxy.rs emitted
directly from all three TOFU outcomes, so the crate had four call sites.

Makes ws_proxy::emit_cert_tofu pub(crate) and routes http_proxy's trusted,
first_use and mismatch paths through it, leaving one call site crate-wide. The
now-unused Emitter import is dropped from http_proxy so clippy -D warnings
stays clean. Behaviour is unchanged: same event name, same payloads, same
order.

Not reproducible locally -- typegen only regenerates under CI's clean
checkout, and a full `npm run tauri build` here passes tsc either way -- so the
Tauri Full Build job on this PR is the proof. Verified locally: exactly one
emit("cert-tofu") call site remains, cargo clippy --all-targets -D warnings
clean, and the release build completes through bundling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:41:22 +02:00

395 lines
14 KiB
YAML

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 \
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. Non-blocking, and it
# will very likely be RED at first: the suite has never run in CI, and a
# local run of the 255 web tests on `main` itself fails ~229 of them, all
# cascading from the shared login helper in tests/e2e/helpers.ts
# (navigateToMainPage never sees [data-testid='app-layout']). That breakage
# predates this PR — it reproduces on a clean 70caa6c worktree.
#
# The job is wired up anyway so the breakage is visible instead of invisible,
# but it MUST stay continue-on-error until the suite is repaired, and
# timeout-minutes caps the minutes it can burn while it is failing.
# 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 \
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