name: Sencho CI

on:
  pull_request:
    branches: [main]

# Cancel previous runs on the same branch/PR
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

# Least-privilege default for every job.
permissions:
  contents: read

# ---------------------------------------------------------------------------
# Build & Validation (PRs only, skipped for release-please PRs)
# ---------------------------------------------------------------------------
jobs:
  backend:
    name: Backend (Build, Test, Lint)
    runs-on: ubuntu-latest
    timeout-minutes: 10
    # Skip on the release-please bot PR. It only touches version metadata,
    # so the full build+test+lint+audit stack is pure action-minute waste.
    if: >-
      github.event_name == 'pull_request'
      && github.head_ref != 'release-please--branches--main--components--sencho'
    steps:
      - name: Checkout Code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
        with:
          node-version-file: '.node-version'
          cache: 'npm'
          cache-dependency-path: backend/package-lock.json

      - name: Install Dependencies
        working-directory: ./backend
        run: npm ci

      - name: Build (TypeScript)
        working-directory: ./backend
        run: npm run build

      - name: Unit Tests (Vitest)
        working-directory: ./backend
        run: npm test

      - name: Lint (ESLint)
        working-directory: ./backend
        run: npm run lint

      - name: Audit Dependencies
        working-directory: ./backend
        run: npm audit --audit-level=high

      # Hand the freshly-verified dist/ to the E2E job so it does not have to
      # re-run `tsc` on the same source. retention-days is the minimum; this
      # artifact is only needed for the downstream e2e job in the same run.
      - name: Upload backend dist
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: backend-dist
          path: backend/dist
          retention-days: 1
          if-no-files-found: error

  frontend:
    name: Frontend (Build, Lint)
    runs-on: ubuntu-latest
    timeout-minutes: 10
    # See backend job rationale: skip bot PRs that cannot affect the frontend.
    if: >-
      github.event_name == 'pull_request'
      && github.head_ref != 'release-please--branches--main--components--sencho'
    steps:
      - name: Checkout Code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
        with:
          node-version-file: '.node-version'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install Dependencies
        working-directory: ./frontend
        run: npm ci

      - name: Build (Vite/React)
        working-directory: ./frontend
        run: npm run build

      - name: Unit Tests (Vitest)
        working-directory: ./frontend
        run: npm test

      - name: Lint (ESLint)
        working-directory: ./frontend
        run: npm run lint

      - name: Audit Dependencies
        working-directory: ./frontend
        run: npm audit --audit-level=high

  docker-validate:
    name: Docker Build & Scan
    runs-on: ubuntu-latest
    timeout-minutes: 15
    # See backend job rationale.
    if: >-
      github.event_name == 'pull_request'
      && github.head_ref != 'release-please--branches--main--components--sencho'
    steps:
      - name: Checkout Code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4

      # Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so
      # the `apk upgrade` layer rebuilds at least once per calendar day, even
      # when every other input to the layer is cached. See Dockerfile:115-122
      # for the rationale.
      - name: Compute daily apk cache bust value
        id: apk-bust
        run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"

      - name: Build Docker image (validation only)
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
        with:
          context: .
          push: false
          load: true
          tags: sencho:pr-test
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}

      - name: Scan image for vulnerabilities (Trivy)
        # Hard-fails the PR on any HIGH or CRITICAL finding not suppressed by
        # the OpenVEX document at security/vex/sencho.openvex.json (loaded via
        # trivy.yaml). The VEX document is the canonical triage record and is
        # also attached as a cosign attestation on the published image.
        # If a new CVE must be triaged, add a VEX statement with a
        # justification — do NOT recreate .trivyignore.
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: sencho:pr-test
          exit-code: '1'
          severity: 'CRITICAL,HIGH'
          format: 'table'
          trivy-config: trivy.yaml

      # Mirror of the release-time pre-publish smoke gate in docker-publish.yml.
      # Catches the class of failure where the source-built Docker CLI or
      # Compose plugin compiles cleanly but does not exec at runtime (e.g. -o
      # pointed at a non-main Go package, ABI breakage in a base image bump).
      # The release workflow additionally polls /api/health; PR CI keeps just
      # the binary check to bound job duration without losing the gate.
      - name: Smoke test built image (binaries)
        run: |
          set -euo pipefail
          docker run --rm --entrypoint sh sencho:pr-test -c \
            'docker --version && docker compose version'

  # ---------------------------------------------------------------------------
  # E2E Tests (PRs only, skipped for release-please PRs)
  # ---------------------------------------------------------------------------
  e2e:
    name: E2E Tests (Playwright)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs: [backend, frontend]
    # See backend job rationale.
    if: >-
      github.event_name == 'pull_request'
      && github.head_ref != 'release-please--branches--main--components--sencho'
    steps:
      - name: Checkout Code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      # Reuse the dist/ that the `backend` job produced and verified, instead
      # of running `tsc` a second time against the same source tree. The
      # composite action's `skip-backend-build` input short-circuits its build
      # step when this artifact is already in place.
      - name: Download backend dist
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: backend-dist
          path: backend/dist

      - name: Start app & install Playwright
        uses: ./.github/actions/start-app
        with:
          skip-backend-build: 'true'

      - name: Run E2E tests
        # `--project=chromium` is explicit because playwright.config.ts also
        # defines a `screenshots` project that captures docs/images/ for
        # manual review. That project must not run in CI: its output would
        # be discarded after the run, wasting minutes per PR. Keep this
        # flag; do not "simplify" back to `npx playwright test`.
        run: npx playwright test --project=chromium

      - name: Upload E2E report and service logs
        if: failure()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: playwright-report
          path: |
            e2e/report/
            test-results/
            ci-logs/
          retention-days: 7
