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
          load: true
          tags: owncord-smoke:candidate
          build-args: VERSION=ci
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Same script release.yml runs before it signs or pushes anything. A
      # boot regression — or a bug in the smoke harness itself, as happened on
      # the first v1.2.0-alpha.3 release run — must fail here, not at tag time.
      - name: Boot-smoke Docker image
        run: bash Server/scripts/docker-smoke.sh owncord-smoke:candidate

  # 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
