name: CI
description: Runs Go tests, race detection, vulnerability scanning, and frontend quality checks.
on:
  push:
    branches:
      - main
      - develop
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  POSTGRES_USER: myuser
  POSTGRES_PASSWORD: mypassword
  POSTGRES_DB: mydb

jobs:
  secrets-scan:
    name: Secrets scan
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Install git (required by gitleaks)
        run: apk add --no-cache git

      - name: Install gitleaks
        run: go install github.com/zricethezav/gitleaks/v8@latest

      - name: Detect secrets
        run: gitleaks detect --source . --verbose --no-banner

  env-docs-check:
    name: Env docs check
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Install python3
        run: apk add --no-cache python3

      - name: Check env var documentation
        run: python3 scripts/check-env-docs.py

  go-build:
    name: Go build
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-build
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Build
        run: go build ./...
        working-directory: backend

  go-vet:
    name: Go vet
    needs: [go-build]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-vet
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Vet (dev tags)
        run: go vet -tags "test,dev" ./...
        working-directory: backend

      - name: Vet (prod tags)
        run: go vet -tags "test,!dev" ./...
        working-directory: backend

  go-lint:
    name: golangci-lint
    needs: [go-build]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-lint
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: golangci-lint
        run: |
          go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
          golangci-lint run ./...
        working-directory: backend

  go-staticcheck:
    name: Staticcheck
    needs: [go-build]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-staticcheck
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Staticcheck (dev tags)
        run: |
          go install honnef.co/go/tools/cmd/staticcheck@latest
          staticcheck -tags "test,dev" ./...
        working-directory: backend

      - name: Staticcheck (prod tags)
        run: |
          staticcheck -tags "test,!dev" ./...
        working-directory: backend

  go-gosec:
    name: Security scan (gosec)
    needs: [go-build]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-gosec
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: gosec (dev tags)
        run: |
          go install github.com/securego/gosec/v2/cmd/gosec@latest
          gosec -severity medium -tags "test,dev" ./...
        working-directory: backend

      - name: gosec (prod tags)
        run: |
          gosec -severity medium -tags "test,!dev" ./...
        working-directory: backend

  go-mod-tidy:
    name: go mod tidy
    needs: [go-build]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-tidy
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: go mod tidy check
        run: |
          cp go.sum go.sum.bak
          cp go.mod go.mod.bak
          go mod tidy
          diff -q go.sum go.sum.bak && diff -q go.mod go.mod.bak && echo "go.mod and go.sum up to date" || { echo "go.mod or go.sum out of date — run 'go mod tidy' and commit"; exit 1; }
          rm -f go.sum.bak go.mod.bak
        working-directory: backend

  test:
    name: Tests (${{ matrix.label }})
    needs: [go-vet, go-lint, go-staticcheck, go-gosec, go-mod-tidy, vulns]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: myuser
          POSTGRES_PASSWORD: mypassword
          POSTGRES_DB: mydb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    strategy:
      matrix:
        include:
          - label: dev
            gotags: test,dev
            verbose: -v
            coverflags: -coverprofile=coverage.out -covermode=atomic
          - label: prod
            gotags: test,!dev
            verbose: ""
            coverflags: ""

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-test-${{ matrix.label }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Install psql client
        run: apk add --no-cache postgresql-client

      - name: Wait for Postgres
        run: |
          for i in $(seq 1 30); do
            pg_isready -h postgres -U myuser && break
            sleep 1
          done

      - name: Create test database
        run: PGPASSWORD=mypassword psql -h postgres -U myuser -d mydb -c "CREATE DATABASE crussell_test_db;" 2>/dev/null || true

      - name: Run tests
        working-directory: backend
        run: go test -tags "${{ matrix.gotags }}" -count=1 ${{ matrix.verbose }} ${{ matrix.coverflags }} ./...
        env:
          POSTGRES_HOST: postgres
          TEST_DB_HOST: postgres

      - name: Check coverage threshold
        working-directory: backend
        run: |
          if [ -f coverage.out ]; then
            COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
            echo "Total coverage: $COVERAGE%"
            if [ "$(echo "$COVERAGE < 100" | bc)" -eq 1 ]; then
              echo "FAIL: Coverage $COVERAGE% is below 100% threshold"
              exit 1
            fi
            echo "PASS: Coverage $COVERAGE% meets 100% threshold"
          else
            echo "No coverage file generated (prod build)"
          fi

  race:
    name: Race (${{ matrix.label }})
    needs: [go-vet, go-lint, go-staticcheck, go-gosec, go-mod-tidy, vulns]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: myuser
          POSTGRES_PASSWORD: mypassword
          POSTGRES_DB: mydb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    strategy:
      matrix:
        include:
          - label: dev
            gotags: test,dev
          - label: prod
            gotags: test,!dev

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-race-${{ matrix.label }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Install psql + build-base
        run: apk add --no-cache postgresql-client build-base

      - name: Wait for Postgres
        run: |
          for i in $(seq 1 30); do
            pg_isready -h postgres -U myuser && break
            sleep 1
          done

      - name: Create test database
        run: PGPASSWORD=mypassword psql -h postgres -U myuser -d mydb -c "CREATE DATABASE crussell_test_db;" 2>/dev/null || true

      - name: Run race detector
        working-directory: backend
        run: go test -tags "${{ matrix.gotags }}" -race -count=1 ./...
        env:
          POSTGRES_HOST: postgres
          TEST_DB_HOST: postgres
          CGO_ENABLED: "1"

  vulns:
    name: Go vulnerabilities
    needs: [go-build]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
          cache: false

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-vulns
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Go vulnerability scan
        working-directory: backend
        run: |
          go install golang.org/x/vuln/cmd/govulncheck@latest
          govulncheck ./...

  frontend-deps:
    name: Frontend deps check
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Cache npm dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            frontend/node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: cd frontend && npm ci

      - name: Check for in-range dependency updates
        run: |
          cd frontend && npm outdated --json 2>&1 | node -e "
          const chunks = [];
          process.stdin.on('data', c => chunks.push(c));
          process.stdin.on('end', () => {
            const raw = Buffer.concat(chunks).toString().trim();
            if (!raw) { console.log('No dependencies to check'); process.exit(0); }
            const data = JSON.parse(raw);
            const updates = Object.entries(data).filter(([_, v]) => v.current !== v.wanted);
            if (updates.length) {
              console.log('In-range updates available — run \"npm update\" locally and commit:');
              updates.forEach(([k, v]) => console.log('  ' + k + ': ' + v.current + ' -> ' + v.wanted));
              process.exit(1);
            }
            console.log('All dependencies up to date within semver range');
          });
          "

      - name: Check for stale overrides
        run: |
          cd frontend && node -e "
          const { execSync } = require('child_process');
          const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8'));
          const overrides = pkg.overrides || {};
          const keys = Object.keys(overrides);
          if (!keys.length) { console.log('No overrides configured'); process.exit(0); }

          function parseMin(range) {
            const v = range.replace(/^[\^~>=<]*/, '').split('.').map(Number);
            return { major: v[0]||0, minor: v[1]||0, patch: v[2]||0 };
          }
          function gte(a, b) {
            if (a.major !== b.major) return a.major > b.major;
            if (a.minor !== b.minor) return a.minor > b.minor;
            return a.patch >= b.patch;
          }

          let stale = [];
          for (const key of keys) {
            const explain = execSync('npm explain ' + key + ' 2>/dev/null || true').toString();
            const was = explain.match(/\(was \"([^\"]+)\"\)/);
            if (!was) {
              const found = explain.includes('node_modules/' + key);
              console.log(key + ': ' + (found ? 'override active' : 'not in tree'));
              continue;
            }
            const parentRange = was[1];
            const overrideTarget = overrides[key];
            if (gte(parseMin(parentRange), parseMin(overrideTarget))) {
              stale.push(key + ' (parent requires ' + parentRange + ', override is ' + overrideTarget + ')');
            }
          }

          if (stale.length) {
            console.log('Stale overrides detected — remove from package.json:');
            stale.forEach(s => console.log('  ' + s));
            process.exit(1);
          }
          console.log('All overrides appear necessary');
          "

  knip:
    name: Knip
    needs: [frontend-deps]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Restore npm dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            frontend/node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: cd frontend && npm ci

      - name: Run knip
        run: cd frontend && npx knip

  frontend-a11y:
    name: Frontend a11y check
    needs: [frontend-deps]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Restore npm dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            frontend/node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: cd frontend && npm ci

      - name: Check a11y
        run: cd frontend && npm run lint:a11y

  frontend-build:
    name: Frontend build
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Cache npm dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            frontend/node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: cd frontend && npm ci

      - name: Build
        run: cd frontend && npm run build

  frontend-svelte-strict:
    name: Svelte strict check
    needs: [frontend-qc]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Restore npm dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            frontend/node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: cd frontend && npm ci

      - name: svelte-kit sync
        run: cd frontend && npx svelte-kit sync

      - name: svelte-check (fail on warnings)
        run: cd frontend && npx svelte-check --tsconfig ./tsconfig.json --fail-on-warnings

  frontend-qc:
    name: Frontend QC (${{ matrix.task }})
    needs: [frontend-build, knip]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    strategy:
      matrix:
        include:
          - task: typecheck
            cmd: npm run check
          - task: lint
            cmd: npm run lint
          - task: audit
            cmd: npm audit --audit-level=info
    steps:
      - uses: actions/checkout@v4

      - name: Fix node toolcache path for Post-step cleanup
        run: |
          mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin
          ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node

      - name: Restore npm dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            frontend/node_modules
          key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: cd frontend && npm ci

      - name: Run QC task
        run: cd frontend && ${{ matrix.cmd }}

  docker-compose-check:
    name: Docker compose check
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Install docker CLI
        run: apk add --no-cache docker-cli docker-compose

      - name: Create env file for compose validation
        run: cp .env.example backend/.env

      - name: Validate compose.yml
        run: docker compose -f compose.yml config --quiet

  nginx-check:
    name: Nginx config check
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: sh
    steps:
      - uses: actions/checkout@v4

      - name: Install nginx
        run: apk add --no-cache nginx

      - name: Validate nginx config
        run: |
          mkdir -p /etc/nginx/conf.d
          cp $(pwd)/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
          nginx -t
