refactor(scripts): unify asset-doc section contract #6098
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 | |
| - release/prerelease | |
| - release/stable | |
| 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| 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) | |
| # Reject repository-derived paths that could alter command structure downstream. | |
| . ./scripts/security/Assert-WorkflowProjectDirectory.ps1 | |
| $projectList = @(Assert-WorkflowProjectDirectory -Path $projectList) | |
| $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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| 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) | |
| # Reject repository-derived paths that could alter command structure downstream. | |
| . ./scripts/security/Assert-WorkflowProjectDirectory.ps1 | |
| $skills = @(Assert-WorkflowProjectDirectory -Path $skills) | |
| $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) }} | |
| accessibility-browser-smoke: | |
| name: Accessibility Browser Smoke | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Node | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "24" | |
| - name: Install accessibility skill Node dependencies | |
| shell: bash | |
| env: | |
| SEARCH_ROOT: .github/skills/accessibility/accessibility | |
| run: ./scripts/ci/install-skill-node-deps.sh | |
| - name: Verify system Chrome | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| chrome_bin="$(command -v google-chrome || command -v google-chrome-stable || true)" | |
| if [ -z "$chrome_bin" ]; then | |
| echo "::error::No system Google Chrome found on the runner; accessibility smoke tests require it" | |
| exit 1 | |
| fi | |
| echo "Using system Chrome at: $chrome_bin" | |
| "$chrome_bin" --version | |
| - name: Run accessibility browser smoke tests | |
| run: npm run ci:test:a11y:smoke | |
| 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 | |
| asset-docs-validation: | |
| name: Asset Docs Validation | |
| uses: ./.github/workflows/asset-docs-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) }} | |
| 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 | |
| workflow-runner-check: | |
| name: Workflow Runner Check | |
| uses: ./.github/workflows/workflow-runner-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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "24" | |
| cache: "npm" | |
| - name: Validate public dependency feeds | |
| run: npm run lint:public-dependency-feeds | |
| - name: Install dependencies | |
| run: npm ci | |
| # Uses audit-ci with an allowlist for advisories that have no installable | |
| # upstream fix. See audit-ci.json for the current allowlist. | |
| - name: Audit all npm projects | |
| 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| 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 | |
| # The promotion head is the last reviewable state before release-please | |
| # can consume its exact release intent, so grammar, parity, advancement, | |
| # and the retained candidate record are proved while the pull request is | |
| # still open. Branch rulesets decide whether this failing check blocks the | |
| # merge; release-prerelease.yml independently revalidates the same intent | |
| # before any tag can be created. | |
| - name: Validate PreRelease promotion intent advances release/prerelease | |
| if: > | |
| github.event_name == 'pull_request' | |
| && github.event.pull_request.head.repo.full_name == github.repository | |
| && github.event.pull_request.base.ref == 'release/prerelease' | |
| && github.event.pull_request.head.ref == 'release-promotion--main--to--release-prerelease' | |
| env: | |
| HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PROMOTION_HEAD: release-promotion--main--to--release-prerelease | |
| run: | | |
| set -euo pipefail | |
| if [ "$HEAD_BRANCH" != "$PROMOTION_HEAD" ]; then | |
| echo "::error::PreRelease promotion head $HEAD_BRANCH is not the canonical promotion head" | |
| exit 1 | |
| fi | |
| if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then | |
| echo "::error::PreRelease promotion event carries an invalid pull request number" | |
| exit 1 | |
| fi | |
| if [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "::error::PreRelease promotion event carries an invalid head SHA" | |
| exit 1 | |
| fi | |
| # hve-core is public, so this origin access is intentionally | |
| # credential free; the fetch and ls-remote below stop working if the | |
| # repository ever becomes private. | |
| # The verified commit is the exact event head, so a head that moved | |
| # after the event fails rather than being proved at a later tip. | |
| git fetch --no-tags origin \ | |
| "+refs/pull/$PR_NUMBER/head:refs/remotes/pull/$PR_NUMBER/head" \ | |
| "+refs/heads/release/prerelease:refs/remotes/origin/release/prerelease" | |
| if [ "$(git rev-parse "refs/remotes/pull/$PR_NUMBER/head")" != "$HEAD_SHA" ]; then | |
| echo "::error::Fetched PreRelease promotion head does not match event head $HEAD_SHA" | |
| exit 1 | |
| fi | |
| CANDIDATE=$(git show "refs/remotes/pull/$PR_NUMBER/head:release-please-prerelease-config.json" | jq -r '.packages["."]["release-as"] // ""') | |
| BASELINE=$(git show 'refs/remotes/origin/release/prerelease:.release-please-prerelease-manifest.json' | jq -r '.["."] // ""') | |
| for entry in "candidate:$CANDIDATE" "baseline:$BASELINE"; do | |
| value=${entry#*:} | |
| if [[ "$value" == *$'\n'* || "$value" == *$'\r'* || ! "$value" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::${entry%%:*} PreRelease version is not canonical MAJOR.MINOR.PATCH" | |
| exit 1 | |
| fi | |
| done | |
| CANDIDATE_MINOR=${CANDIDATE#*.} | |
| CANDIDATE_MINOR=${CANDIDATE_MINOR%%.*} | |
| if (( 10#$CANDIDATE_MINOR % 2 == 0 )); then | |
| echo "::error::PreRelease candidate $CANDIDATE has an even minor" | |
| exit 1 | |
| fi | |
| if [ "$CANDIDATE" = "$BASELINE" ] || [ "$(printf '%s\n%s\n' "$CANDIDATE" "$BASELINE" | sort -V | tail -1)" = "$BASELINE" ]; then | |
| echo "::error::$HEAD_BRANCH proposes $CANDIDATE but release/prerelease already carries $BASELINE" | |
| exit 1 | |
| fi | |
| # A tag is immutable, so a transport failure aborts under set -e | |
| # instead of reporting the identity as available. | |
| CANDIDATE_REF="refs/tags/prerelease-v$CANDIDATE" | |
| OCCUPIED=$(git ls-remote origin "$CANDIDATE_REF") | |
| if [ -n "$OCCUPIED" ]; then | |
| echo "::error::$CANDIDATE_REF already exists; the PreRelease release identity is occupied" | |
| exit 1 | |
| fi | |
| echo "$HEAD_BRANCH advances release/prerelease from $BASELINE to $CANDIDATE" | |
| - name: Validate Stable promotion intent advances release/stable | |
| if: > | |
| github.event_name == 'pull_request' | |
| && github.event.pull_request.base.ref == 'release/stable' | |
| && startsWith(github.head_ref, 'release-promotion--release-prerelease--to--release-stable--prerelease-v') | |
| env: | |
| HEAD_BRANCH: ${{ github.head_ref }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$HEAD_BRANCH" =~ ^release-promotion--release-prerelease--to--release-stable--(prerelease-v[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then | |
| echo "::error::Stable promotion head $HEAD_BRANCH does not match the canonical selected-tag grammar" | |
| exit 1 | |
| fi | |
| SOURCE_TAG=${BASH_REMATCH[1]} | |
| SOURCE_VERSION=${SOURCE_TAG#prerelease-v} | |
| SOURCE_MINOR=${SOURCE_VERSION#*.} | |
| SOURCE_MINOR=${SOURCE_MINOR%%.*} | |
| if (( 10#$SOURCE_MINOR % 2 == 0 )); then | |
| echo "::error::Stable promotion source $SOURCE_TAG has an even minor" | |
| exit 1 | |
| fi | |
| if [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "::error::Stable promotion event carries an invalid head SHA" | |
| exit 1 | |
| fi | |
| git fetch --no-tags origin \ | |
| "+refs/pull/$PR_NUMBER/head:refs/remotes/pull/$PR_NUMBER/head" \ | |
| "+refs/heads/release/stable:refs/remotes/origin/release/stable" \ | |
| "+refs/tags/$SOURCE_TAG:refs/tags/$SOURCE_TAG" | |
| if [ "$(git rev-parse "refs/remotes/pull/$PR_NUMBER/head")" != "$HEAD_SHA" ]; then | |
| echo "::error::Fetched Stable promotion head does not match event head $HEAD_SHA" | |
| exit 1 | |
| fi | |
| CANDIDATE=$(git show "refs/remotes/pull/$PR_NUMBER/head:release-please-config.json" | jq -r '.packages["."]["release-as"] // ""') | |
| BASELINE=$(git show 'refs/remotes/origin/release/stable:.release-please-manifest.json' | jq -r '.["."] // ""') | |
| for entry in "candidate:$CANDIDATE" "baseline:$BASELINE"; do | |
| value=${entry#*:} | |
| if [[ "$value" == *$'\n'* || "$value" == *$'\r'* || ! "$value" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::${entry%%:*} Stable version is not canonical MAJOR.MINOR.PATCH" | |
| exit 1 | |
| fi | |
| done | |
| CANDIDATE_MINOR=${CANDIDATE#*.} | |
| CANDIDATE_MINOR=${CANDIDATE_MINOR%%.*} | |
| if (( 10#$CANDIDATE_MINOR % 2 != 0 )); then | |
| echo "::error::Stable candidate $CANDIDATE has an odd minor" | |
| exit 1 | |
| fi | |
| if [ "$CANDIDATE" = "$BASELINE" ] || [ "$(printf '%s\n%s\n' "$CANDIDATE" "$BASELINE" | sort -V | tail -1)" = "$BASELINE" ]; then | |
| echo "::error::$HEAD_BRANCH proposes $CANDIDATE but release/stable already carries $BASELINE" | |
| exit 1 | |
| fi | |
| SOURCE_SHA=$(git rev-parse --verify --end-of-options "refs/tags/$SOURCE_TAG^{commit}") | |
| if git merge-base --is-ancestor "$SOURCE_SHA" refs/remotes/origin/release/stable; then | |
| echo "::error::$SOURCE_TAG at $SOURCE_SHA is already contained in release/stable" | |
| exit 1 | |
| fi | |
| echo "$HEAD_BRANCH advances release/stable from $BASELINE to $CANDIDATE using $SOURCE_TAG" | |
| 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 | |
| - accessibility-browser-smoke | |
| - fuzz-tests | |
| - pip-audit | |
| - docusaurus-tests | |
| - frontmatter-validation | |
| - adr-consistency-validation | |
| - ai-artifact-validation | |
| - asset-docs-validation | |
| - msdate-freshness | |
| - plugin-validation | |
| - skill-validation | |
| - eval-validation | |
| - link-lang-check | |
| - markdown-link-check | |
| - dependency-pinning-check | |
| - devcontainer-lockfile-check | |
| - workflow-permissions-check | |
| - workflow-runner-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." |