# yad-managed: yad-checks
# SDLC check gates (Phase 3 build plan §C). The gates below run on every PR and must pass before
# merge. They are CI-agnostic bash in checks/ — this workflow just invokes them with the PR base.
# This workflow file is OWNED by yad-checks (the marker on line 1 identifies it). It runs
# independently of any other workflow in .github/workflows/, so wiring never edits a foreign workflow.
name: yad-checks
on:
  pull_request:
    # `edited` (beyond the opened/synchronize/reopened defaults) so a PR title/body correction
    # re-runs the pattern gates without a close/reopen — e.g. fixing a body the pr-template gate held.
    # The commit-range jobs below skip a bare `edited` (only pr-title/pr-template need to re-check).
    types: [opened, synchronize, reopened, edited]
    branches: ["**"]

# Read-only by default: no gate here writes to the repo. A job that needs more asks for it itself.
permissions:
  contents: read

jobs:
  spec-link:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      - run: bash checks/spec-link.sh "origin/${{ github.base_ref }}"

  contract-check:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      # E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
      # secret is set; without it this prints a note and the gate defers, as before.
      - run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
        env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
      - run: bash checks/contract-check.sh "origin/${{ github.base_ref }}"

  # E65/E66 — the risk map (.sdlc/risk-map: a risk level per directory, no names). Warnings, and the
  # approver count read from the base branch's map. The script always exits 0: the count is reported,
  # not enforced, and a stale map never blocks a merge.
  risk-map:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      - run: bash checks/risk-map-check.sh "origin/${{ github.base_ref }}"

  build-test-lint:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    env:
      YAD_TEST_MAX_WORKERS: "2" # cap jest/vitest test workers in CI; ignored by other runners
      YAD_NODE_VERSION: ${{ vars.YAD_NODE_VERSION || '22' }} # repo variable; a current 20, 22 or 24 line (Node 25+ has no Corepack)
      NX_BASE: ${{ github.event.pull_request.base.sha }}
      NX_HEAD: ${{ github.event.pull_request.head.sha }}
      COREPACK_HOME: ${{ github.workspace }}/.corepack-cache # pinned-manager downloads, cached below
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with:
          fetch-depth: 0
          filter: blob:none
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: ${{ env.YAD_NODE_VERSION }}
      # Dependency cache for either manager: npm's ~/.npm, pnpm's content-addressable store, and the
      # Corepack home where `corepack prepare` keeps the pinned manager (otherwise re-downloaded from
      # the registry on every job, even when the dependency cache hits). Keyed on the lockfiles AND
      # package.json, since a packageManager bump changes no lockfile. setup-node's own `cache:` must
      # name the manager up front (and needs pnpm on PATH before it runs), which package.json only
      # decides later — so cache the directories directly.
      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: |
            ~/.npm
            ~/.local/share/pnpm/store
            ${{ env.COREPACK_HOME }}
          key: yad-deps-${{ runner.os }}-node${{ env.YAD_NODE_VERSION }}-${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'package.json') }}
          restore-keys: yad-deps-${{ runner.os }}-node${{ env.YAD_NODE_VERSION }}-
      - run: bash checks/install-deps.sh
      - run: bash checks/build-test-lint.sh

  # Phase 6 — feature-thread gates. lineage-check: the change links a real threaded epic. epic-open:
  # a sealed epic (all stories shipped) refuses new behaviour, forcing a change-epic. reconcile-debt:
  # a thread with open hotfix debt is frozen for new changes until paid. All build on spec-link's
  # story->epic resolution and degrade to a note when the product repo is not reachable from CI.
  lineage-check:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      # E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
      # secret is set; without it this prints a note and the gate defers, as before.
      - run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
        env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
      - run: bash checks/lineage-check.sh "origin/${{ github.base_ref }}"

  epic-open:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      # E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
      # secret is set; without it this prints a note and the gate defers, as before.
      - run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
        env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
      - run: bash checks/epic-open.sh "origin/${{ github.base_ref }}"

  reconcile-debt:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      # E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
      # secret is set; without it this prints a note and the gate defers, as before.
      - run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
        env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
      - run: bash checks/reconcile-debt-check.sh "origin/${{ github.base_ref }}"

  # Pattern gates: commit subject + PR title + PR body all follow the convention (profile: code).
  # commit-message reads the commit range, not the title/body — skip it on a bare `edited` event.
  commit-message:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      - run: bash checks/commit-message.sh --profile code "origin/${{ github.base_ref }}"

  # Pass the title via env (never interpolate untrusted ${{ }} into a run line — injection-safe).
  pr-title:
    runs-on: ubuntu-latest
    env:
      PR_TITLE: ${{ github.event.pull_request.title }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
      - run: bash checks/pr-title.sh --profile code "$PR_TITLE"

  pr-template:
    runs-on: ubuntu-latest
    env:
      PR_BODY: ${{ github.event.pull_request.body }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
      - run: |
          body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
          bash checks/pr-template.sh --profile code "$body"

  # No unverified commits: a platform-Verified signature on every commit (write access decides who authors).
  # Reads the commit range, not the title/body — skip it on a bare `edited` event.
  verified-commits:
    runs-on: ubuntu-latest
    if: github.event.action != 'edited'
    permissions:
      contents: read
    env:
      GH_TOKEN: ${{ github.token }} # read-only: gh api commits/<sha> for the Verified badge
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
        with: { fetch-depth: 0 }
      - run: bash checks/verified-commits.sh "origin/${{ github.base_ref }}"
