name: CI

on:
  push:
    branches: [main]
  pull_request:

# Least-privilege default for every job; widen per-job only when needed.
permissions:
  contents: read

# Cancel superseded runs on the same ref (e.g. rapid pushes to a PR) so CI
# only spends minutes on the newest commit. Pushes to main run to completion.
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  lint:
    name: Lint CSS
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint:css

  commitlint:
    name: Lint commit messages
    runs-on: ubuntu-latest
    # Commit-message linting only makes sense against a PR's commit range.
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          # Full history so commitlint can walk base..head.
          fetch-depth: 0
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - name: Validate PR commits against Conventional Commits
        run: >-
          npx --no -- commitlint
          --from ${{ github.event.pull_request.base.sha }}
          --to ${{ github.event.pull_request.head.sha }}
          --verbose

  build:
    name: Build bundle
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: dist
          path: dist/

  artifacts-freshness:
    name: Verify all generated artifacts
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          # Full history so check-token-registry.js can diff the registry
          # against the base branch (id-permanence gate) rather than HEAD.
          fetch-depth: 0
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run audit:check
      - run: node scripts/check-artifacts.js --check
      - run: node scripts/check-version-sync.js
      - run: node scripts/check-token-registry.js
      - run: node scripts/check-llm-guide.js
      # Every hand-written doc must reference only live token/class names (or
      # names explicitly allowlisted in docs/ref-allowlist.json).
      - run: node scripts/check-doc-refs.js
      # The release workflow's git-add must stage every file version-sync writes.
      - run: node scripts/check-release-add-list.js
      # architecture.md's layer order must match core/layers.css (the cascade contract).
      - run: node scripts/check-layer-order.js
      - run: node scripts/check-macro-catalog.js
      # #582 source-CSS audit gates: false wiring claims, annotation/value
      # drift, fallback-only hook tokens, hand-maintained mirrors, and
      # per-bundle token definitions.
      - run: node scripts/check-dead-knobs.js
      - run: node scripts/check-annotations.js
      - run: node scripts/check-hook-tokens.js
      - run: node scripts/check-mirrors.js
      - run: node scripts/check-bundle-defs.js

  dependency-audit:
    name: Dependency vulnerability audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      # Fail only on high/critical advisories so routine low-severity noise
      # doesn't block merges. Tune the threshold as the project matures.
      - run: npm audit --audit-level=high

  breaking-change-docs:
    name: Check migration docs for breaking changes
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Fail if breaking change is missing migration docs
        env:
          BASE: ${{ github.event.pull_request.base.sha }}
          HEAD: ${{ github.event.pull_request.head.sha }}
        run: |
          if git log "$BASE..$HEAD" --no-merges --pretty=format:"%s" \
               | grep -qE '^[[:alnum:]_-]+(\([^)]+\))?!:'; then
            if ! git diff --name-only "$BASE" "$HEAD" | grep -q '^docs/migration\.md$'; then
              echo "::error::Breaking change detected but docs/migration.md was not updated." \
                   "Add a migration section or use a non-breaking commit type."
              exit 1
            fi
            echo "Breaking changes found — docs/migration.md was updated."
          else
            echo "No breaking changes detected."
          fi

  test:
    name: Regression tests
    runs-on: ubuntu-latest
    needs: [build]
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      # Reuse the dist/ artifact from the build job — no need to rebuild.
      - uses: actions/download-artifact@v8
        with:
          name: dist
          path: dist/
      # Run unit tests directly (skipping the pretest build hook).
      - run: npm run test:unit
      - run: npx playwright install --with-deps chromium firefox webkit
      - run: npx playwright test

  configurator:
    name: Configurator tests
    runs-on: ubuntu-latest
    needs: [build]
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020  # v7.0.0
        with:
          node-version: 22
          cache: npm
      # Root deps first: the configurator's sync script reads docs/api-index.json
      # via tooling generated at the repo root.
      - run: npm ci
      - run: npm ci
        working-directory: configurator
      # Reuse the dist/ artifact from the build job so the configurator Vite
      # build can resolve PreviewPanel.svelte's ?raw CSS import without a
      # redundant root rebuild.
      - uses: actions/download-artifact@v8
        with:
          name: dist
          path: dist/
      # Unit suite — the sync tripwire: curated Basic controls, presets, fluid
      # engine scalars and knob defaults all pinned to the live catalogue —
      # plus the Vitest component suite (test runs both via test:unit/components).
      - run: npm test
        working-directory: configurator
      # Future-proofing tripwire: every public knob must map to a real domain.
      - run: npm run check:curation
        working-directory: configurator
      # Type/diagnostics gate (kept at 0 errors / 0 warnings).
      - run: npm run check
        working-directory: configurator
      # Browser regression suite (test:e2e prebuilds the configurator via Vite;
      # Playwright manages the preview server via its webServer option).
      # Cross-engine: the full suite runs on Chromium; core behaviour + the
      # dogfood/isolation contract also run on Firefox and WebKit.
      - run: npx playwright install --with-deps chromium firefox webkit
        working-directory: configurator
      - run: npm run test:e2e
        working-directory: configurator
