# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
name: PR Validation

on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main
      - develop
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
  cancel-in-progress: true

# Minimal permissions for security
permissions:
  contents: read

jobs:
  spell-check:
    name: Spell Check
    uses: ./.github/workflows/spell-check.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  markdown-lint:
    name: Markdown Lint
    uses: ./.github/workflows/markdown-lint.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  table-format:
    name: Table Format Check
    uses: ./.github/workflows/table-format.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  psscriptanalyzer:
    name: PowerShell Lint
    uses: ./.github/workflows/ps-script-analyzer.yml
    permissions:
      contents: read
    with:
      soft-fail: false
      changed-files-only: true

  discover-python-projects:
    name: Discover Python Projects
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      directories: ${{ steps.find.outputs.directories }}
      has-projects: ${{ steps.find.outputs.has-projects }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Find Python projects
        id: find
        shell: pwsh
        run: |
          # The moderation eval (scripts/evals/moderation) carries a heavy
          # torch/detoxify stack and runs weekly via weekly-validation.yml,
          # so it is excluded from per-PR Python matrix jobs here.
          $projectList = @(Get-ChildItem -Recurse -Force -Filter pyproject.toml |
            Where-Object { $_.FullName -notmatch 'node_modules' -and $_.FullName -notmatch 'evals[\\/]+moderation' } |
            ForEach-Object { Resolve-Path -Relative $_.DirectoryName } |
            ForEach-Object { $_ -replace '^\.[\\/]', '' } |
            Sort-Object)
          $jsonItems = $projectList | ForEach-Object { $_ | ConvertTo-Json -Compress }
          $dirs = '[' + (($jsonItems) -join ',') + ']'
          "directories=$dirs" >> $env:GITHUB_OUTPUT
          if ($projectList.Count -eq 0) {
            "has-projects=false" >> $env:GITHUB_OUTPUT
            Write-Output 'No Python projects found'
          } else {
            "has-projects=true" >> $env:GITHUB_OUTPUT
            Write-Output "Found Python projects: $dirs"
          }

  discover-node-projects:
    name: Discover Node Skills
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      directories: ${{ steps.find.outputs.directories }}
      has-projects: ${{ steps.find.outputs.has-projects }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Find Node skills
        id: find
        shell: pwsh
        run: |
          # A Node skill is any skill (dir containing SKILL.md) that ships at
          # least one *.test.* / *.spec.* (mjs/cjs/js). The skill root is the
          # node --test working dir.
          $skills = @(Get-ChildItem -Path .github/skills -Recurse -Force -Filter SKILL.md -ErrorAction SilentlyContinue |
            Where-Object { $_.FullName -notmatch 'node_modules' } |
            ForEach-Object { $_.DirectoryName } |
            Where-Object { @(Get-ChildItem -Path $_ -Recurse -Force -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '\.(test|spec)\.(mjs|cjs|js)$' -and $_.FullName -notmatch 'node_modules' }).Count -gt 0 } |
            ForEach-Object { (Resolve-Path -Relative $_) -replace '^\.[\\/]', '' -replace '\\', '/' } |
            Sort-Object -Unique)
          $jsonItems = $skills | ForEach-Object { $_ | ConvertTo-Json -Compress }
          $dirs = '[' + (($jsonItems) -join ',') + ']'
          "directories=$dirs" >> $env:GITHUB_OUTPUT
          if ($skills.Count -eq 0) {
            "has-projects=false" >> $env:GITHUB_OUTPUT
            Write-Output 'No Node skills found'
          } else {
            "has-projects=true" >> $env:GITHUB_OUTPUT
            Write-Output "Found Node skills: $dirs"
          }

  python-lint:
    name: "Python Lint (${{ matrix.directory }})"
    needs: discover-python-projects
    if: needs.discover-python-projects.outputs.has-projects == 'true'
    strategy:
      fail-fast: false
      matrix:
        directory: ${{ fromJson(needs.discover-python-projects.outputs.directories) }}
    uses: ./.github/workflows/python-lint.yml
    permissions:
      contents: read
    with:
      soft-fail: false
      changed-files-only: true
      working-directory: ${{ matrix.directory }}

  copyright-headers:
    name: Copyright Headers
    uses: ./.github/workflows/copyright-headers.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  yaml-lint:
    name: YAML Lint
    uses: ./.github/workflows/yaml-lint.yml
    permissions:
      contents: read
    with:
      soft-fail: false
      changed-files-only: true

  pester-tests:
    name: PowerShell Tests
    uses: ./.github/workflows/pester-tests.yml
    permissions:
      contents: read
      id-token: write
    with:
      soft-fail: false
      changed-files-only: false
      code-coverage: true

  pytest:
    name: "Python Tests (${{ matrix.directory }})"
    needs: discover-python-projects
    if: needs.discover-python-projects.outputs.has-projects == 'true'
    uses: ./.github/workflows/pytest-tests.yml
    permissions:
      contents: read
      id-token: write
    with:
      working-directory: ${{ matrix.directory }}
      soft-fail: false
      changed-files-only: true
    strategy:
      fail-fast: false
      matrix:
        directory: ${{ fromJson(needs.discover-python-projects.outputs.directories) }}

  node-tests:
    name: "Node Tests (${{ matrix.directory }})"
    needs: discover-node-projects
    if: needs.discover-node-projects.outputs.has-projects == 'true'
    uses: ./.github/workflows/node-tests.yml
    permissions:
      contents: read
      id-token: write
    with:
      working-directory: ${{ matrix.directory }}
      soft-fail: false
      changed-files-only: true
    strategy:
      fail-fast: false
      matrix:
        directory: ${{ fromJson(needs.discover-node-projects.outputs.directories) }}

  fuzz-tests:
    name: "Fuzz Tests (${{ matrix.directory }})"
    needs: discover-python-projects
    if: needs.discover-python-projects.outputs.has-projects == 'true'
    uses: ./.github/workflows/fuzz-tests.yml
    permissions:
      contents: read
    with:
      working-directory: ${{ matrix.directory }}
      soft-fail: false
      changed-files-only: true
    strategy:
      fail-fast: false
      matrix:
        directory: ${{ fromJson(needs.discover-python-projects.outputs.directories) }}

  pip-audit:
    name: "pip-audit (${{ matrix.directory }})"
    needs: discover-python-projects
    if: needs.discover-python-projects.outputs.has-projects == 'true'
    strategy:
      fail-fast: false
      matrix:
        directory: ${{ fromJson(needs.discover-python-projects.outputs.directories) }}
    uses: ./.github/workflows/pip-audit.yml
    permissions:
      contents: read
    with:
      working-directory: ${{ matrix.directory }}
      soft-fail: false
      changed-files-only: true

  docusaurus-tests:
    name: Docusaurus Tests
    uses: ./.github/workflows/docusaurus-tests.yml
    permissions:
      contents: read
      id-token: write # Required for Codecov OIDC in the reusable workflow
    with:
      soft-fail: false
      changed-files-only: true

  frontmatter-validation:
    name: Frontmatter Validation
    uses: ./.github/workflows/frontmatter-validation.yml
    permissions:
      contents: read
    with:
      soft-fail: false
      changed-files-only: true
      skip-footer-validation: false
      warnings-as-errors: true

  adr-consistency-validation:
    name: ADR Consistency Validation
    uses: ./.github/workflows/adr-consistency-validation.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
    with:
      soft-fail: false
      changed-files-only: true
      upload-sarif: true
      upload-artifact: false

  ai-artifact-validation:
    name: AI Artifact Validation
    uses: ./.github/workflows/ai-artifact-validation.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  msdate-freshness:
    name: ms.date Freshness Check
    uses: ./.github/workflows/msdate-freshness-check.yml
    permissions:
      contents: read
    with:
      staleness-threshold-days: 90
      changed-files-only: true
      soft-fail: false

  plugin-validation:
    name: Plugin Validation
    uses: ./.github/workflows/plugin-validation.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  skill-validation:
    name: Skill Validation
    uses: ./.github/workflows/skill-validation.yml
    permissions:
      contents: read
    with:
      soft-fail: false
      changed-files-only: true
      base-branch: ${{ github.event.pull_request.base.sha || format('origin/{0}', github.base_ref) }}

  eval-validation:
    name: Eval Validation
    uses: ./.github/workflows/eval-validation.yml
    permissions:
      contents: read
      pull-requests: write
    with:
      soft-fail: false
      changed-files-only: true
      base-branch: ${{ github.event.pull_request.base.sha || format('origin/{0}', github.base_ref) }}
    secrets:
      copilot-github-token: ${{ secrets.COPILOT_GITHUB_TOKEN }}

  link-lang-check:
    name: Link Language Check
    uses: ./.github/workflows/link-lang-check.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  markdown-link-check:
    name: Markdown Link Check
    uses: ./.github/workflows/markdown-link-check.yml
    permissions:
      contents: read
    with:
      soft-fail: true

  dependency-pinning-check:
    name: Validate Dependency Pinning
    uses: ./.github/workflows/dependency-pinning-scan.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
    with:
      soft-fail: false
      upload-sarif: true
      upload-artifact: false

  devcontainer-lockfile-check:
    name: Devcontainer Lockfile Integrity
    uses: ./.github/workflows/devcontainer-lockfile-check.yml
    permissions:
      contents: read
    with:
      soft-fail: false

  workflow-permissions-check:
    name: Workflow Permissions Check
    uses: ./.github/workflows/workflow-permissions-scan.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
    with:
      soft-fail: false
      upload-sarif: true
      upload-artifact: false

  dangerous-workflow-check:
    name: Dangerous Workflow Check
    uses: ./.github/workflows/dangerous-workflow-scan.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
    with:
      soft-fail: false
      upload-sarif: true
      upload-artifact: false
      poutine-soft-fail: true  # Poutine runs advisory; homegrown template-injection check is the hard gate

  action-version-consistency-scan:
    name: Action Version Consistency Scan
    uses: ./.github/workflows/action-version-consistency-scan.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
    with:
      soft-fail: false
      upload-sarif: true
      upload-artifact: false

  gitleaks-scan:
    name: Gitleaks Secret Scan
    uses: ./.github/workflows/gitleaks-scan.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
    with:
      soft-fail: false
      upload-sarif: true
      upload-artifact: false
      log-opts: '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}'

  npm-audit:
    name: npm Security Audit
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout code
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: "24"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      # Uses audit-ci with an allowlist for advisories that have no upstream
      # fix available. See audit-ci.json for the current allowlist and rationale.
      - name: Run security audit
        run: npm run audit:npm

  codeql:
    name: CodeQL Security Analysis
    uses: ./.github/workflows/codeql-analysis.yml
    permissions:
      contents: read
      security-events: write  # Required for SARIF upload to Security tab
      actions: read

  gate-completeness-check:
    name: PR Gate Completeness
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout code
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Setup PowerShell modules
        uses: ./.github/actions/setup-ps-modules

      - name: Validate PR gate completeness
        shell: pwsh
        run: ./scripts/security/Test-PrValidationGate.ps1 -FailOnViolation

  pr-validation-success:
    name: PR Validation Success
    runs-on: ubuntu-latest
    permissions:
      contents: read
    if: always()
    needs:
      - spell-check
      - markdown-lint
      - table-format
      - psscriptanalyzer
      - discover-python-projects
      - python-lint
      - copyright-headers
      - yaml-lint
      - pester-tests
      - pytest
      - discover-node-projects
      - node-tests
      - fuzz-tests
      - pip-audit
      - docusaurus-tests
      - frontmatter-validation
      - adr-consistency-validation
      - ai-artifact-validation
      - msdate-freshness
      - plugin-validation
      - skill-validation
      - eval-validation
      - link-lang-check
      - markdown-link-check
      - dependency-pinning-check
      - devcontainer-lockfile-check
      - workflow-permissions-check
      - dangerous-workflow-check
      - action-version-consistency-scan
      - gitleaks-scan
      - npm-audit
      - codeql
      - gate-completeness-check
    steps:
      - name: Verify all jobs succeeded
        env:
          NEEDS_JSON: ${{ toJSON(needs) }}
        shell: bash
        run: |
          set -euo pipefail
          failed=$(echo "$NEEDS_JSON" | jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "skipped") | .key')
          if [ -n "$failed" ]; then
            echo "The following jobs did not pass:"
            echo "$failed"
            exit 1
          fi
          echo "All PR validation jobs passed."

