Chore(deps-dev): bump js-yaml and markdownlint-cli2 #3895
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
| name: Validate | |
| on: | |
| pull_request: | |
| branches: [main, develop] | |
| push: | |
| branches: [main] | |
| merge_group: | |
| branches: [main, develop] | |
| workflow_dispatch: | |
| inputs: | |
| summary-verbose: | |
| description: 'Emit verbose fixture summary (sets SUMMARY_VERBOSE=true)' | |
| required: false | |
| default: 'false' | |
| sample_id: | |
| description: 'Sampling correlation id (prevents cancels)' | |
| required: false | |
| type: string | |
| history_scenario_set: | |
| description: 'VI history scenario set for Validate VI history lanes (none|smoke|history-core)' | |
| required: false | |
| default: 'smoke' | |
| allow_noncanonical_vi_history: | |
| description: 'Allow vi-history-scenarios to run on non-canonical repos (manual override)' | |
| required: false | |
| type: boolean | |
| default: false | |
| allow_noncanonical_history_core: | |
| description: 'Allow history-core scenario set on non-canonical repos (requires allow_noncanonical_vi_history=true)' | |
| required: false | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ (github.event_name == 'pull_request' && github.event.pull_request.number) || github.event.inputs.sample_id || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # Controlled opt-in for the June 2026 JavaScript action runtime migration. | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' | |
| jobs: | |
| smoke-gate: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| skip: ${{ steps.determine.outputs.skip }} | |
| steps: | |
| - name: Determine skip for smoke branches | |
| id: determine | |
| shell: pwsh | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| HEAD_REF: ${{ github.head_ref }} | |
| run: | | |
| $isSmoke = $false | |
| if ($env:EVENT_NAME -eq 'pull_request' -and $env:HEAD_REF -and $env:HEAD_REF.StartsWith('smoke/')) { | |
| $isSmoke = $true | |
| } | |
| $value = if ($isSmoke) { 'true' } else { 'false' } | |
| "skip=$value" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| lint: | |
| needs: smoke-gate | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Install actionlint (retry) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ./bin | |
| ver="${ACTIONLINT_VERSION:-1.7.8}" | |
| for i in 1 2 3; do | |
| if curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash -s -- "$ver" ./bin; then | |
| break | |
| else | |
| echo "retry $i"; sleep 2 | |
| fi | |
| done | |
| - name: Run actionlint | |
| run: | | |
| ./bin/actionlint -color | |
| - name: Check PR mergeability | |
| if: ${{ github.event_name == 'pull_request' }} | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ./tools/Check-PRMergeable.ps1 -Number ${{ github.event.pull_request.number }} -FailOnConflict | |
| - name: Setup Python for workflow enclave | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.12' | |
| - name: Workflow drift check | |
| shell: pwsh | |
| run: | | |
| pwsh -NoLogo -NoProfile -File tools/Check-WorkflowDrift.ps1 -FailOnDrift | |
| - name: Setup Node with cache | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| - name: CLI lints | |
| uses: ./.github/actions/cli-lints | |
| with: | |
| enforce: ${{ github.ref_name == 'main' || github.base_ref == 'main' }} | |
| linkcheck: 'true' | |
| actionlint-version: '1.7.8' | |
| relaxed-path: 'docs/releases/**/*.md' | |
| - name: Guard tracked build artifacts (enforced) | |
| shell: pwsh | |
| run: | | |
| # Optional allowlist via env ALLOWLIST_TRACKED_ARTIFACTS (semicolon-separated globs) | |
| # and/or file-based allowlist at .ci/build-artifacts-allow.txt | |
| pwsh -File tools/Check-TrackedBuildArtifacts.ps1 -AllowListPath '.ci/build-artifacts-allow.txt' | |
| - name: Surface build artifacts allowlist (if any) | |
| shell: pwsh | |
| run: | | |
| $path = '.ci/build-artifacts-allow.txt' | |
| if (Test-Path -LiteralPath $path -PathType Leaf) { | |
| $lines = Get-Content -LiteralPath $path | Where-Object { $_ -and -not ($_.Trim().StartsWith('#')) } | ForEach-Object { $_.Trim() } | |
| if ($lines.Count -gt 0 -and $env:GITHUB_STEP_SUMMARY) { | |
| $out = @('### Build Artifacts Allowlist','') | |
| foreach($l in $lines){ $out += ('- ' + $l) } | |
| $out -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| } | |
| - name: Build artifacts guard — ad-hoc allowlist usage | |
| shell: pwsh | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $msg = @( | |
| '### Build Artifacts Guard — Ad-hoc Allowlist', | |
| '', | |
| '- To permit specific tracked paths temporarily, set ALLOWLIST_TRACKED_ARTIFACTS to a semicolon-separated list of globs.', | |
| "- Example: ``ALLOWLIST_TRACKED_ARTIFACTS='src/Legacy/**/bin/**;src/Legacy/**/obj/**'``", | |
| '- Prefer using .ci/build-artifacts-allow.txt for committed, reviewable exceptions.' | |
| ) -join "`n" | |
| $msg | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Derive environment snapshot | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node tools/npm/run-script.mjs --silent derive:env > derived-env.json | |
| mkdir -p tests/results/_agent | |
| cp derived-env.json tests/results/_agent/derived-env.json | |
| if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then | |
| { | |
| echo '### Derived Environment'; | |
| echo '```json'; | |
| cat derived-env.json; | |
| echo '```'; | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: PrePush local gates (includes watcher schema validation) | |
| shell: pwsh | |
| env: | |
| PREPUSH_SKIP_LEGACY_FIXTURE_CHECKS: '1' | |
| run: | | |
| pwsh -File tools/PrePush-Checks.ps1 | |
| - name: Release conductor contract tests | |
| shell: bash | |
| run: | | |
| node tools/npm/run-script.mjs priority:release:conductor:test | |
| - name: Policy guard (branch protection) | |
| if: ${{ github.event.repository.fork == false }} | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| pwsh -NoLogo -NoProfile -File tools/priority/Sync-BranchProtectionPolicy.ps1 -ResultsDir tests/results/_agent/policy | |
| - name: Upload policy drift artifact | |
| if: ${{ always() && github.event.repository.fork == false }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: policy-drift-validate-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: tests/results/_agent/policy/policy-drift-report.json | |
| if-no-files-found: error | |
| - name: Lint unanchored dot-sourcing (non-blocking) | |
| shell: pwsh | |
| continue-on-error: true | |
| run: | | |
| pwsh -File tools/Lint-DotSourcing.ps1 -WarnOnly | |
| - name: Lint inline-if in format (-f) | |
| shell: pwsh | |
| run: pwsh -File tools/Lint-InlineIfInFormat.ps1 | |
| - name: Emit tool versions | |
| if: always() | |
| shell: bash | |
| run: | | |
| { | |
| echo '### Tool Versions' | |
| echo "- actionlint: $(./bin/actionlint -version || echo 'n/a')" | |
| echo "- node: $(node -v || echo 'n/a')" | |
| echo "- npm: $(npm -v || echo 'n/a')" | |
| echo "- markdownlint: $(markdownlint --version || echo 'n/a')" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Lint loop determinism (notice only on non-main) | |
| if: github.ref_name != 'main' | |
| shell: pwsh | |
| run: | | |
| $paths = Get-ChildItem -Path .github/workflows -Filter *.yml | ForEach-Object { $_.FullName } | |
| if ($paths) { pwsh -File tools/Lint-LoopDeterminism.Shim.ps1 -PathsList ($paths -join ';') } else { Write-Host '::notice::No workflow files to lint.' } | |
| - name: Lint loop determinism (enforced on main) | |
| if: github.ref_name == 'main' | |
| shell: pwsh | |
| run: | | |
| $paths = Get-ChildItem -Path .github/workflows -Filter *.yml | ForEach-Object { $_.FullName } | |
| if ($paths) { pwsh -File tools/Lint-LoopDeterminism.Shim.ps1 -PathsList ($paths -join ';') -FailOnViolation } else { Write-Host '::notice::No workflow files to lint.' } | |
| - name: Local markdown link check (intra-repo) | |
| shell: pwsh | |
| run: | | |
| # Simple intra-repo link checker: scans markdown for (./...) links and verifies files exist. | |
| $ErrorActionPreference = 'Stop' | |
| $mdFiles = Get-ChildItem -Path . -Recurse -Include *.md -File | | |
| Where-Object { | |
| $path = $_.FullName -replace '\\','/' | |
| return $path -notmatch '/node_modules/' ` | |
| -and $path -notmatch '/vendor/' ` | |
| -and $path -notmatch '/tmp/' | |
| } | |
| $errors = @() | |
| foreach ($file in $mdFiles) { | |
| $text = Get-Content -LiteralPath $file.FullName -Raw | |
| $matches = [regex]::Matches($text, '\]\((\.\/?[^)#\s]+)\)') | |
| foreach ($m in $matches) { | |
| $rel = $m.Groups[1].Value | |
| if ($rel -like 'http*' -or $rel -like '#*') { continue } | |
| # Strip anchors like file.md#section | |
| $pathOnly = $rel.Split('#')[0] | |
| $target = Join-Path $file.DirectoryName $pathOnly | |
| if (-not (Test-Path -LiteralPath $target)) { | |
| $errors += "[$($file.FullName)] broken link -> $rel (resolved: $target)" | |
| } | |
| } | |
| } | |
| if ($errors.Count -gt 0) { | |
| Write-Host 'Broken intra-repo links detected:' | |
| $errors | ForEach-Object { Write-Host " - $_" } | |
| exit 2 | |
| } else { | |
| Write-Host 'Intra-repo markdown links OK.' | |
| } | |
| - name: Append docs pointers | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @('### Docs Pointers','') | |
| $lines += '- Fixture Drift: ./docs/FIXTURE_DRIFT.md' | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Labels sync summary (develop only) | |
| if: github.ref_name == 'develop' | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $labelsFile = '.github/labels.yml' | |
| if (-not (Test-Path -LiteralPath $labelsFile)) { Write-Host '::notice::.github/labels.yml not found'; exit 0 } | |
| try { | |
| $yaml = Get-Content -LiteralPath $labelsFile -Raw | |
| # crude parse: entries like '- name: xyz' on their own line | |
| $names = @([regex]::Matches($yaml,'(?m)^\s*-\s*name:\s*(.+?)\s*$') | ForEach-Object { $_.Groups[1].Value.Trim() }) | |
| } catch { $names = @() } | |
| $api = "https://api.github.com/repos/${{ github.repository }}/labels?per_page=100" | |
| $hdr = @{ Authorization = "token $env:GITHUB_TOKEN"; Accept='application/vnd.github+json'; 'X-GitHub-Api-Version'='2022-11-28' } | |
| $existing = @() | |
| try { $resp = Invoke-RestMethod -Method Get -Uri $api -Headers $hdr; $existing = @($resp | ForEach-Object { $_.name }) } catch {} | |
| $missing = @($names | Where-Object { $_ -and ($existing -notcontains $_) }) | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @('### Labels Sync (notice)','') | |
| $lines += ('- Defined in labels.yml: {0}' -f ($names.Count)) | |
| $lines += ('- Repo labels: {0}' -f ($existing.Count)) | |
| if ($missing.Count -gt 0) { | |
| $lines += '- Missing:' | |
| foreach ($m in $missing) { $lines += (' - ' + $m) } | |
| } else { | |
| $lines += '- Missing: none' | |
| } | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Labels sync enforcement (main) | |
| if: github.ref_name == 'main' | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $labelsFile = '.github/labels.yml' | |
| if (-not (Test-Path -LiteralPath $labelsFile)) { Write-Error '.github/labels.yml not found'; exit 2 } | |
| $yaml = Get-Content -LiteralPath $labelsFile -Raw | |
| $names = @([regex]::Matches($yaml,'(?m)^\s*-\s*name:\s*(.+?)\s*$') | ForEach-Object { $_.Groups[1].Value.Trim() }) | |
| $api = "https://api.github.com/repos/${{ github.repository }}/labels?per_page=100" | |
| $hdr = @{ Authorization = "token $env:GITHUB_TOKEN"; Accept='application/vnd.github+json'; 'X-GitHub-Api-Version'='2022-11-28' } | |
| $resp = Invoke-RestMethod -Method Get -Uri $api -Headers $hdr | |
| $existing = @($resp | ForEach-Object { $_.name }) | |
| $missing = @($names | Where-Object { $_ -and ($existing -notcontains $_) }) | |
| if ($missing.Count -gt 0) { | |
| Write-Host 'Missing labels:' | |
| $missing | ForEach-Object { Write-Host (' - ' + $_) } | |
| Write-Error ('Labels sync check failed on main: {0} missing' -f $missing.Count) | |
| exit 2 | |
| } else { | |
| Write-Host 'Labels OK on main.' | |
| } | |
| - name: Markdown lint determinism regression (Pester) | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| if (-not (Get-Module -ListAvailable -Name Pester)) { | |
| Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck | |
| } | |
| Invoke-Pester -Path 'tests/Lint-Markdown.Tests.ps1' -CI | |
| - name: Markdown lint determinism regression (Node) | |
| shell: bash | |
| run: node --test tools/__tests__/lint-markdown.test.mjs | |
| - name: Line ending drift guard | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| pwsh -NoLogo -NoProfile -File tools/Assert-LineEndingDeterminism.ps1 ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Append markdown determinism summary | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @( | |
| '### Markdown Lint Determinism Regression', | |
| '', | |
| '- `Invoke-Pester -Path tests/Lint-Markdown.Tests.ps1 -CI`', | |
| '- `node --test tools/__tests__/lint-markdown.test.mjs`', | |
| '- `tools/Assert-LineEndingDeterminism.ps1`' | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Run markdownlint | |
| run: | | |
| node tools/npm/run-script.mjs lint:md:changed | |
| env: | |
| ACTIONLINT_VERSION: '${{ vars.ACTIONLINT_VERSION || ''1.7.8'' }}' | |
| validate-scope-plan: | |
| needs: smoke-gate | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| scope_mode: ${{ steps.plan.outputs.scope_mode }} | |
| scope_category: ${{ steps.plan.outputs.scope_category }} | |
| run_fixtures: ${{ steps.plan.outputs.run_fixtures }} | |
| fixtures_reason: ${{ steps.plan.outputs.fixtures_reason }} | |
| run_bundle_certification: ${{ steps.plan.outputs.run_bundle_certification }} | |
| bundle_certification_reason: ${{ steps.plan.outputs.bundle_certification_reason }} | |
| run_vi_history: ${{ steps.plan.outputs.run_vi_history }} | |
| vi_history_reason: ${{ steps.plan.outputs.vi_history_reason }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Resolve Validate scope plan | |
| id: plan | |
| shell: pwsh | |
| env: | |
| VALIDATE_EVENT_NAME: ${{ github.event_name }} | |
| VALIDATE_REPOSITORY: ${{ github.repository }} | |
| VALIDATE_PR_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| VALIDATE_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }} | |
| VALIDATE_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }} | |
| VALIDATE_BASE_REF: ${{ github.base_ref || github.event.merge_group.base_ref || '' }} | |
| VALIDATE_HEAD_REF: ${{ github.head_ref || '' }} | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/validate-scope-plan' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $planPath = Join-Path $resultsRoot 'validate-scope-plan.json' | |
| pwsh -NoLogo -NoProfile -File tools/Resolve-ValidateScopePlan.ps1 ` | |
| -EventName $env:VALIDATE_EVENT_NAME ` | |
| -Repository $env:VALIDATE_REPOSITORY ` | |
| -PullRequestNumber $env:VALIDATE_PR_NUMBER ` | |
| -BaseSha $env:VALIDATE_BASE_SHA ` | |
| -HeadSha $env:VALIDATE_HEAD_SHA ` | |
| -BaseRef $env:VALIDATE_BASE_REF ` | |
| -HeadRef $env:VALIDATE_HEAD_REF ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY ` | |
| -JsonPath $planPath | |
| - name: Upload Validate scope plan artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-scope-plan | |
| path: tests/results/_agent/validate-scope-plan/validate-scope-plan.json | |
| if-no-files-found: error | |
| fixtures: | |
| needs: [smoke-gate, lint, validate-scope-plan] | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| FAIL_ON_NEW_STRUCTURAL: 'true' | |
| SUMMARY_VERBOSE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.summary-verbose || 'false' }} | |
| VALIDATE_SCOPE_RUN_FIXTURES: ${{ needs.validate-scope-plan.outputs.run_fixtures }} | |
| VALIDATE_SCOPE_FIXTURES_REASON: ${{ needs.validate-scope-plan.outputs.fixtures_reason }} | |
| VALIDATE_SCOPE_CATEGORY: ${{ needs.validate-scope-plan.outputs.scope_category }} | |
| steps: | |
| - name: Append fixture lane plan | |
| shell: pwsh | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @( | |
| '### Fixtures', | |
| '', | |
| ('- scope_category: `{0}`' -f $env:VALIDATE_SCOPE_CATEGORY), | |
| ('- run_fixtures: `{0}`' -f $env:VALIDATE_SCOPE_RUN_FIXTURES), | |
| ('- fixtures_reason: `{0}`' -f $env:VALIDATE_SCOPE_FIXTURES_REASON) | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Run fixture validator (JSON) | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| pwsh -File tools/Validate-Fixtures.ps1 -Json -MinBytes 32 > fixture-validation.json | |
| $exit = $LASTEXITCODE | |
| if ($exit -ne 0) { | |
| Write-Error "Fixture validator failed with exit code: $exit" | |
| exit $exit | |
| } | |
| $data = Get-Content fixture-validation.json -Raw | ConvertFrom-Json | |
| if (-not $data.ok) { | |
| Write-Error 'Fixture validation reported non-ok status.' | |
| exit 1 | |
| } | |
| - name: Restore previous fixture validation snapshot (cache) | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| id: restore_prev_fixture_validation | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: fixture-validation-prev.json | |
| key: fixture-validation-${{ github.sha }} | |
| restore-keys: | | |
| fixture-validation- | |
| - name: Compute delta vs previous snapshot | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' && steps.restore_prev_fixture_validation.outputs.cache-hit == 'true' | |
| shell: pwsh | |
| run: | | |
| Write-Host 'Previous snapshot restored. Computing delta.' | |
| pwsh -File tools/Diff-FixtureValidationJson.ps1 -Baseline fixture-validation-prev.json -Current fixture-validation.json -FailOnNewStructuralIssue > fixture-validation-delta.json | |
| if ($LASTEXITCODE -eq 3) { | |
| Write-Host 'New structural fixture issues detected (delta willFail=true).' | |
| if ($env:FAIL_ON_NEW_STRUCTURAL -eq 'true') { | |
| Write-Error 'Failing job due to FAIL_ON_NEW_STRUCTURAL=true' | |
| exit 3 | |
| } else { | |
| Write-Host 'FAIL_ON_NEW_STRUCTURAL=false -> continuing without failing.' | |
| } | |
| } | |
| - name: Validate delta JSON schema (basic) | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' && steps.restore_prev_fixture_validation.outputs.cache-hit == 'true' && hashFiles('fixture-validation-delta.json') != '' | |
| shell: pwsh | |
| run: | | |
| pwsh -File tools/Test-FixtureValidationDeltaSchema.ps1 -DeltaJsonPath fixture-validation-delta.json | |
| - name: Lite schema validate (delta) | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' && steps.restore_prev_fixture_validation.outputs.cache-hit == 'true' && hashFiles('fixture-validation-delta.json') != '' | |
| shell: pwsh | |
| run: | | |
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath fixture-validation-delta.json -SchemaPath docs/schemas/fixture-validation-delta-v1.schema.json | |
| - name: Upload fixture validation delta JSON | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' && steps.restore_prev_fixture_validation.outputs.cache-hit == 'true' && hashFiles('fixture-validation-delta.json') != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-fixture-validation-delta-json | |
| path: fixture-validation-delta.json | |
| - name: Upload fixture validation JSON | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-fixture-validation-json | |
| path: fixture-validation.json | |
| - name: Lite schema validate (snapshot) | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| shell: pwsh | |
| run: | | |
| if (-not (Test-Path fixture-validation.json)) { | |
| Write-Error 'fixture-validation.json was not produced.' | |
| exit 1 | |
| } | |
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath fixture-validation.json -SchemaPath docs/schemas/fixture-manifest-v1.schema.json | |
| - name: Append fixture summary | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| shell: pwsh | |
| run: pwsh -File tools/Write-FixtureValidationSummary.ps1 -ValidationJson fixture-validation.json -DeltaJson fixture-validation-delta.json | |
| - name: Write fixture summary file | |
| if: always() && env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| shell: pwsh | |
| run: pwsh -File tools/Write-FixtureValidationSummary.ps1 -ValidationJson fixture-validation.json -DeltaJson fixture-validation-delta.json -SummaryPath fixture-summary.md | |
| - name: Upload fixture summary artifact | |
| if: always() && env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' && hashFiles('fixture-summary.md') != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: fixture-validation-summary | |
| path: fixture-summary.md | |
| - name: Save current snapshot to cache | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: fixture-validation.json | |
| key: fixture-validation-${{ github.sha }} | |
| - name: Copy snapshot for next run reference | |
| if: env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| shell: pwsh | |
| run: Copy-Item -LiteralPath fixture-validation.json -Destination fixture-validation-prev.json -Force | |
| - name: Agent wait summary (notice-only) | |
| if: always() && env.VALIDATE_SCOPE_RUN_FIXTURES == 'true' | |
| uses: ./.github/actions/agent-wait-post | |
| with: | |
| results-dir: tests/results | |
| fail-on-outside: 'false' | |
| upload-artifact: 'false' | |
| hook-parity: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| env: | |
| PREPUSH_SKIP_LEGACY_FIXTURE_CHECKS: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Install dependencies (no scripts) | |
| run: npm ci --ignore-scripts | |
| - name: Clean hook results directory | |
| shell: bash | |
| run: | | |
| rm -rf tests/results/_hooks | |
| mkdir -p tests/results/_hooks | |
| - name: Prepare staged fixture | |
| shell: bash | |
| run: | | |
| rm -rf tmp/hooks | |
| mkdir -p tmp/hooks | |
| cat <<'PS' > tmp/hooks/sample.ps1 | |
| function Invoke-HookSample { | |
| param() | |
| Write-Output 'hook parity sample' | |
| } | |
| Invoke-HookSample | |
| PS | |
| git add -f tmp/hooks/sample.ps1 | |
| - name: Prime actionlint binary | |
| shell: pwsh | |
| run: ./tools/PrePush-Checks.ps1 -SkipLegacyFixtureChecks | |
| - name: Hooks plane info | |
| run: node tools/npm/run-script.mjs hooks:plane | |
| - name: Hooks preflight | |
| run: node tools/npm/run-script.mjs hooks:preflight | |
| - name: Run multi-plane hook diff | |
| run: node tools/npm/run-script.mjs hooks:multi | |
| - name: Validate hook summary schema | |
| run: node tools/npm/run-script.mjs hooks:schema | |
| - name: Upload hook parity summaries | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hook-parity-ubuntu-latest | |
| path: tests/results/_hooks/*.json | |
| - name: Reset staged files | |
| if: always() | |
| shell: bash | |
| run: | | |
| git reset --hard | |
| rm -rf tmp/hooks | |
| semver: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Install dependencies (no scripts) | |
| run: npm ci --ignore-scripts | |
| - name: SemVer check | |
| run: node tools/npm/run-script.mjs semver:check | |
| release-branch: | |
| if: startsWith(github.head_ref, 'release/') | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Install dependencies (no scripts) | |
| run: npm ci --ignore-scripts | |
| - name: Verify release branch | |
| env: | |
| GITHUB_HEAD_REF: ${{ github.head_ref }} | |
| RELEASE_VALIDATE_BASE: origin/develop | |
| run: node tools/priority/verify-release-branch.mjs | |
| feature-handoff: | |
| if: startsWith(github.head_ref, 'feature/') | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Install dependencies (no scripts) | |
| run: npm ci --ignore-scripts | |
| - name: Run priority handoff tests | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PRIORITY_HANDOFF_SKIP_POLICY: '1' | |
| run: node tools/npm/run-script.mjs priority:handoff-tests | |
| issue-snapshot: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Sync standing-priority snapshot | |
| env: | |
| AGENT_PRIORITY_UPSTREAM_REPOSITORY: LabVIEW-Community-CI-CD/compare-vi-cli-action | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| if [ -z "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]; then | |
| echo "::error::GH_TOKEN/GITHUB_TOKEN is required for priority:sync:lane" | |
| exit 1 | |
| fi | |
| node tools/npm/run-script.mjs priority:sync:lane | |
| - name: Validate snapshot schema (best-effort) | |
| continue-on-error: true | |
| run: | | |
| node tools/npm/run-script.mjs priority:schema | |
| - name: Upload issue snapshot artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: standing-priority-snapshot | |
| path: | | |
| tests/results/_agent/issue/*.json | |
| tests/results/_agent/issue/*.digest | |
| session-index: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Resolve Pester version | |
| id: resolve_pester | |
| shell: pwsh | |
| run: | | |
| $version = if ($env:PESTER_VERSION -and $env:PESTER_VERSION.Trim()) { $env:PESTER_VERSION } else { (./tools/Get-PesterVersion.ps1) } | |
| "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| - name: Install Pester ${{ steps.resolve_pester.outputs.version }} | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $pesterVersion = '${{ steps.resolve_pester.outputs.version }}' | |
| $installed = $false | |
| $galleryUri = 'https://www.powershellgallery.com/api/v2/' | |
| $installLegacy = { | |
| Install-Module -Name Pester -RequiredVersion $pesterVersion -Force -Scope CurrentUser | |
| } | |
| try { | |
| &$installLegacy | |
| $installed = $true | |
| } catch { | |
| Write-Host ("::notice::Install-Module first attempt failed: {0}" -f $_.Exception.Message) | |
| try { | |
| Register-PSRepository -Default -ErrorAction Stop | |
| } catch { | |
| Write-Host ("::warning::Register-PSRepository -Default failed: {0}" -f $_.Exception.Message) | |
| } | |
| try { | |
| &$installLegacy | |
| $installed = $true | |
| } catch { | |
| Write-Host ("::notice::Install-Module retry failed: {0}" -f $_.Exception.Message) | |
| Write-Host "::notice::Falling back to PSResourceGet for Pester installation." | |
| try { | |
| Register-PSResourceRepository -Name PSGallery -Uri $galleryUri -Trusted -ErrorAction SilentlyContinue | |
| } catch { | |
| Write-Host ("::warning::Register-PSResourceRepository fallback failed: {0}" -f $_.Exception.Message) | |
| } | |
| $attempts = 0 | |
| $maxAttempts = 5 | |
| while (-not $installed -and $attempts -lt $maxAttempts) { | |
| try { | |
| Install-PSResource -Name Pester -Version $pesterVersion -Scope CurrentUser -TrustRepository -Repository PSGallery | |
| $installed = $true | |
| } catch { | |
| $attempts++ | |
| if ($attempts -ge $maxAttempts) { | |
| Write-Host ("::warning::Install-PSResource exhausted attempts ({0}): {1}" -f $attempts, $_.Exception.Message) | |
| break | |
| } | |
| Write-Host ("::notice::Install-PSResource attempt {0} failed: {1}" -f $attempts, $_.Exception.Message) | |
| $delay = [math]::Min(30, [math]::Pow(2, $attempts)) | |
| Start-Sleep -Seconds $delay | |
| } | |
| } | |
| if (-not $installed) { | |
| Write-Host "::notice::Attempting manual PSGallery download fallback for Pester." | |
| $downloadRoot = Join-Path $env:RUNNER_TEMP 'pester-download' | |
| New-Item -ItemType Directory -Path $downloadRoot -Force | Out-Null | |
| $packagePath = Join-Path $downloadRoot ("Pester.{0}.nupkg" -f $pesterVersion) | |
| $downloadEndpoints = @( | |
| @{ Uri = ('{0}/package/Pester/{1}' -f $galleryUri.TrimEnd('/'), $pesterVersion); SkipCert = $false }, | |
| @{ Uri = ('https://psg-prod-eastus.azureedge.net/packages/pester.{0}.nupkg' -f $pesterVersion.ToLowerInvariant()); SkipCert = $true } | |
| ) | |
| $downloaded = $false | |
| foreach ($endpoint in $downloadEndpoints) { | |
| if ($downloaded) { break } | |
| for ($i = 1; $i -le 3 -and -not $downloaded; $i++) { | |
| try { | |
| $commonArgs = @{ | |
| Uri = $endpoint.Uri | |
| OutFile = $packagePath | |
| UseBasicParsing = $true | |
| ErrorAction = 'Stop' | |
| } | |
| if ($endpoint.SkipCert) { | |
| Invoke-WebRequest @commonArgs -SkipCertificateCheck | |
| } else { | |
| Invoke-WebRequest @commonArgs | |
| } | |
| $downloaded = $true | |
| } catch { | |
| if ($i -ge 3) { | |
| Write-Host ("::warning::Manual download attempt {0} ({1}) failed: {2}" -f $i, $endpoint.Uri, $_.Exception.Message) | |
| } else { | |
| Write-Host ("::notice::Manual download attempt {0} ({1}) failed: {2}" -f $i, $endpoint.Uri, $_.Exception.Message) | |
| Start-Sleep -Seconds ([math]::Min(30, [math]::Pow(2, $i))) | |
| } | |
| } | |
| } | |
| } | |
| if ($downloaded -and (Test-Path -LiteralPath $packagePath)) { | |
| $modulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator | |
| $destRoot = $modulePaths | Where-Object { $_ -and $_.Trim() } | Where-Object { $_ -like '*Modules*' } | Select-Object -First 1 | |
| if (-not $destRoot) { | |
| $destRoot = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.local/share/powershell/Modules' | |
| } | |
| $destPath = Join-Path $destRoot ('Pester/{0}' -f $pesterVersion) | |
| New-Item -ItemType Directory -Force -Path $destPath | Out-Null | |
| Expand-Archive -Path $packagePath -DestinationPath $destPath -Force | |
| $installed = $true | |
| } else { | |
| Write-Host "::warning::Manual PSGallery download fallback failed." | |
| } | |
| } | |
| } | |
| } | |
| if (-not $installed) { | |
| throw 'Failed to install Pester from PSGallery.' | |
| } | |
| $repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue | |
| if ($repo -and $repo.InstallationPolicy -ne 'Trusted') { | |
| try { | |
| Set-PSRepository -Name PSGallery -InstallationPolicy Trusted | |
| } catch { | |
| Write-Host ("::notice::Unable to set PSGallery installation policy to Trusted: {0}" -f $_.Exception.Message) | |
| } | |
| } | |
| - name: Run dispatcher smoke to produce session index | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $res = Join-Path $env:RUNNER_TEMP 'sessionindex' | |
| New-Item -ItemType Directory -Force -Path $res | Out-Null | |
| ./tools/Quick-DispatcherSmoke.ps1 -ResultsPath $res -PreferWorkspace | |
| $contexts = @( | |
| 'Validate / lint' | |
| 'Validate / fixtures' | |
| 'Validate / session-index' | |
| ) | |
| $bpScript = Join-Path (Get-Location) 'tools/Update-SessionIndexBranchProtection.ps1' | |
| $bpApiScript = Join-Path (Get-Location) 'tools/Get-BranchProtectionRequiredChecks.ps1' | |
| $branchName = '${{ github.base_ref || github.ref_name }}' | |
| $repoParts = '${{ github.repository }}'.Split('/') | |
| $bpResult = & $bpApiScript -Owner $repoParts[0] -Repository $repoParts[1] -Branch $branchName -Token $env:GITHUB_TOKEN | |
| $updateArgs = @{ | |
| ResultsDir = $res | |
| PolicyPath = 'tools/policy/branch-required-checks.json' | |
| Branch = $branchName | |
| ProducedContexts = $contexts | |
| } | |
| if ($bpResult.status) { $updateArgs['ActualStatus'] = $bpResult.status } | |
| if ($bpResult.contexts) { $updateArgs['ActualContexts'] = @($bpResult.contexts) } | |
| if ($bpResult.notes) { $updateArgs['AdditionalNotes'] = @($bpResult.notes) } | |
| & $bpScript @updateArgs | |
| - name: Attach origin/upstream parity telemetry | |
| shell: pwsh | |
| run: | | |
| $res = Join-Path $env:RUNNER_TEMP 'sessionindex' | |
| $parityPath = Join-Path $res 'origin-upstream-parity.json' | |
| $repoSlug = '${{ github.repository }}' | |
| try { | |
| git remote add upstream ("https://github.com/{0}.git" -f $repoSlug) 2>$null | Out-Null | |
| } catch { } | |
| try { git fetch --no-tags --prune --depth=1 origin develop | Out-Null } catch { Write-Host ("::notice::origin/develop fetch skipped: {0}" -f $_.Exception.Message) } | |
| try { git fetch --no-tags --prune --depth=1 upstream develop | Out-Null } catch { Write-Host ("::notice::upstream/develop fetch skipped: {0}" -f $_.Exception.Message) } | |
| node tools/priority/report-origin-upstream-parity.mjs --base-ref 'upstream/develop' --head-ref 'origin/develop' --sample-limit 20 --output-path "$parityPath" | |
| pwsh -File tools/Update-SessionIndexParity.ps1 -ResultsDir $res -ParityReportPath $parityPath -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Validate session index schemas (v1 + optional v2) | |
| shell: pwsh | |
| run: | | |
| $jsonV1 = Join-Path $env:RUNNER_TEMP 'sessionindex' 'session-index.json' | |
| $jsonV2 = Join-Path $env:RUNNER_TEMP 'sessionindex' 'session-index-v2.json' | |
| if (-not (Test-Path $jsonV1)) { Write-Error "session-index.json not found at $jsonV1"; exit 2 } | |
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath $jsonV1 -SchemaPath docs/schemas/session-index-v1.schema.json | |
| if (Test-Path $jsonV2) { | |
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath $jsonV2 -SchemaPath docs/schema/generated/session-index-v2.schema.json | |
| if ($LASTEXITCODE -ne 0) { Write-Host "::warning::V2 schema validation returned $LASTEXITCODE for session-index-v2.json" } | |
| } else { | |
| Write-Host '::notice::session-index-v2.json not found; running in v1 fallback mode.' | |
| } | |
| - name: Upload session index artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-session-index | |
| path: | | |
| ${{ runner.temp }}/sessionindex/session-index.json | |
| ${{ runner.temp }}/sessionindex/session-index-v2.json | |
| if-no-files-found: warn | |
| session-index-v2-contract: | |
| needs: [smoke-gate, session-index] | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| SESSION_INDEX_V2_CONTRACT_ENFORCE: ${{ vars.SESSION_INDEX_V2_CONTRACT_ENFORCE || 'false' }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Download session index artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: validate-session-index | |
| path: ${{ runner.temp }}/sessionindex-v2-contract | |
| - name: Evaluate session index v2 contract (burn-in aware) | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| $res = Join-Path $env:RUNNER_TEMP 'sessionindex-v2-contract' | |
| $enforceRaw = "$env:SESSION_INDEX_V2_CONTRACT_ENFORCE" | |
| $enforce = $false | |
| if (-not [string]::IsNullOrWhiteSpace($enforceRaw)) { | |
| $enforce = $enforceRaw.Trim().ToLowerInvariant() -eq 'true' | |
| } | |
| if ($enforce) { | |
| Write-Host '::notice::session-index-v2 contract is running in enforce mode.' | |
| pwsh -File tools/Test-SessionIndexV2Contract.ps1 -ResultsDir $res -Branch 'develop' -BurnInThreshold 10 -Enforce | |
| } else { | |
| Write-Host '::notice::session-index-v2 contract is running in burn-in mode (non-blocking).' | |
| pwsh -File tools/Test-SessionIndexV2Contract.ps1 -ResultsDir $res -Branch 'develop' -BurnInThreshold 10 | |
| } | |
| - name: Upload v2 contract report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-session-index-v2-contract | |
| path: | | |
| ${{ runner.temp }}/sessionindex-v2-contract/session-index-v2-contract.json | |
| ${{ runner.temp }}/sessionindex-v2-contract/session-index-v2-disposition.json | |
| ${{ runner.temp }}/sessionindex-v2-contract/session-index-v2-cutover-readiness.json | |
| if-no-files-found: warn | |
| comparevi-history-bundle-certification: | |
| needs: [smoke-gate, lint, validate-scope-plan, session-index, session-index-v2-contract] | |
| if: > | |
| needs.smoke-gate.outputs.skip != 'true' && | |
| needs.validate-scope-plan.outputs.run_bundle_certification == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Setup Node 20 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Install Node dependencies | |
| run: node tools/npm/cli.mjs ci | |
| - name: Publish CompareVI.Tools bundle for certification | |
| id: bundle | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/comparevi-history-bundle-certification' | |
| $bundleOutputRoot = Join-Path $resultsRoot 'bundle-artifacts' | |
| $metadataPath = Join-Path $resultsRoot 'comparevi-tools-artifact.json' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| pwsh -NoLogo -NoProfile -File tools/Publish-CompareVIToolsArtifact.ps1 ` | |
| -OutputRoot $bundleOutputRoot ` | |
| -MetadataReportPath $metadataPath | |
| $bundleMetadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json -Depth 12 | |
| $bundleArchivePath = Join-Path (Resolve-Path -LiteralPath $bundleOutputRoot).Path $bundleMetadata.bundle.archiveName | |
| "bundle_archive_path=$bundleArchivePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "bundle_metadata_path=$metadataPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Validate CompareVI.Tools metadata schema | |
| run: | | |
| node tools/npm/run-script.mjs schema:validate -- ` | |
| --schema docs/schemas/comparevi-tools-release-manifest-v1.schema.json ` | |
| --data '${{ steps.bundle.outputs.bundle_metadata_path }}' | |
| - name: Run CompareVI history bundle certification | |
| id: certification | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/comparevi-history-bundle-certification' | |
| $summaryPath = Join-Path $resultsRoot 'comparevi-history-bundle-certification.json' | |
| pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1 ` | |
| -ResultsDir $resultsRoot ` | |
| -SummaryJsonPath $summaryPath ` | |
| -BundleArchivePath '${{ steps.bundle.outputs.bundle_archive_path }}' ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Validate bundle certification schema | |
| run: | | |
| node tools/npm/run-script.mjs schema:validate -- ` | |
| --schema docs/schemas/comparevi-history-bundle-certification-v1.schema.json ` | |
| --data '${{ steps.certification.outputs.summary-json-path }}' | |
| - name: Render bundle certification markdown | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/comparevi-history-bundle-certification' | |
| $markdownPath = Join-Path $resultsRoot 'comparevi-history-bundle-certification.md' | |
| node tools/npm/run-script.mjs history:bundle:verify -- ` | |
| --summary '${{ steps.certification.outputs.summary-json-path }}' ` | |
| --markdown $markdownPath ` | |
| --step-summary $env:GITHUB_STEP_SUMMARY | |
| - name: Upload bundle certification artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-comparevi-history-bundle-certification | |
| path: | | |
| tests/results/_agent/comparevi-history-bundle-certification/** | |
| if-no-files-found: error | |
| vi-history-scenarios-plan: | |
| needs: [smoke-gate, lint, validate-scope-plan, session-index, session-index-v2-contract] | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| execute_lanes: ${{ steps.plan.outputs.execute_lanes }} | |
| skip_reason: ${{ steps.plan.outputs.skip_reason }} | |
| history_scenario_set: ${{ steps.plan.outputs.history_scenario_set }} | |
| requested_history_scenario_set: ${{ steps.plan.outputs.requested_history_scenario_set }} | |
| downgraded_history_core: ${{ steps.plan.outputs.downgraded_history_core }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Resolve VI history dispatch plan | |
| id: plan | |
| shell: pwsh | |
| env: | |
| VALIDATE_EVENT_NAME: ${{ github.event_name }} | |
| VALIDATE_REPOSITORY: ${{ github.repository }} | |
| VALIDATE_IS_FORK: ${{ github.event.repository.fork }} | |
| VALIDATE_HISTORY_SCENARIO_SET: ${{ github.event.inputs.history_scenario_set }} | |
| VALIDATE_ALLOW_NONCANONICAL: ${{ github.event.inputs.allow_noncanonical_vi_history }} | |
| VALIDATE_ALLOW_NONCANONICAL_HISTORY_CORE: ${{ github.event.inputs.allow_noncanonical_history_core }} | |
| VALIDATE_SCOPE_RUN_VI_HISTORY: ${{ needs.validate-scope-plan.outputs.run_vi_history }} | |
| VALIDATE_SCOPE_VI_HISTORY_REASON: ${{ needs.validate-scope-plan.outputs.vi_history_reason }} | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/vi-history-dispatch' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $planPath = Join-Path $resultsRoot 'validate-vi-history-dispatch-plan.json' | |
| $isForkRepository = "$env:VALIDATE_IS_FORK" -eq 'true' | |
| $allowNonCanonical = "$env:VALIDATE_ALLOW_NONCANONICAL" -eq 'true' | |
| $allowNonCanonicalHistoryCore = "$env:VALIDATE_ALLOW_NONCANONICAL_HISTORY_CORE" -eq 'true' | |
| $enableScopedExecution = "$env:VALIDATE_SCOPE_RUN_VI_HISTORY" -eq 'true' | |
| pwsh -NoLogo -NoProfile -File tools/Resolve-ValidateVIHistoryDispatchPlan.ps1 ` | |
| -EventName $env:VALIDATE_EVENT_NAME ` | |
| -Repository $env:VALIDATE_REPOSITORY ` | |
| -IsForkRepository:$isForkRepository ` | |
| -HistoryScenarioSet $env:VALIDATE_HISTORY_SCENARIO_SET ` | |
| -AllowNonCanonical:$allowNonCanonical ` | |
| -AllowNonCanonicalHistoryCore:$allowNonCanonicalHistoryCore ` | |
| -EnableScopedExecution:$enableScopedExecution ` | |
| -ScopedSkipReason $env:VALIDATE_SCOPE_VI_HISTORY_REASON ` | |
| -ScopedHistoryScenarioSet 'smoke' ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY ` | |
| -JsonPath $planPath | |
| - name: Upload VI history dispatch plan artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-vi-history-dispatch-plan | |
| path: tests/results/_agent/vi-history-dispatch/validate-vi-history-dispatch-plan.json | |
| if-no-files-found: error | |
| vi-history-scenarios-linux: | |
| needs: [smoke-gate, lint, session-index, session-index-v2-contract, vi-history-scenarios-plan] | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| NI_LINUX_IMAGE: nationalinstruments/labview:2026q1-linux | |
| NI_LINUX_LABVIEW_PATH: /usr/local/natinst/LabVIEW-2026-64/labview | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Append VI history Linux lane plan | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @( | |
| '### VI History Scenarios (Linux lane)', | |
| '', | |
| ('- execute_lanes: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.execute_lanes }}'), | |
| ('- skip_reason: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.skip_reason }}'), | |
| ('- requested_history_scenario_set: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.requested_history_scenario_set }}'), | |
| ('- history_scenario_set: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.history_scenario_set }}'), | |
| ('- downgraded_history_core: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.downgraded_history_core }}') | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append | |
| } | |
| - if: needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Pull NI Linux image (2026q1-linux) | |
| if: needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: docker pull $env:NI_LINUX_IMAGE | |
| - name: Manage Docker runtime (Linux preflight) | |
| if: needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/linux' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $snapshotPath = Join-Path $resultsRoot 'runtime-manager-preflight-linux.json' | |
| pwsh -NoLogo -NoProfile -File tools/Assert-DockerRuntimeDeterminism.ps1 ` | |
| -ExpectedOsType linux ` | |
| -ExpectedContext desktop-linux ` | |
| -AutoRepair:$true ` | |
| -ManageDockerEngine:$false ` | |
| -EngineReadyTimeoutSeconds 300 ` | |
| -EngineReadyPollSeconds 3 ` | |
| -SnapshotPath $snapshotPath ` | |
| -GitHubOutputPath '' | |
| - name: Print VI history Linux runtime alignment | |
| if: needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/linux' | |
| pwsh -NoLogo -NoProfile -File tools/Write-VIHistoryLaneEvidence.ps1 ` | |
| -LaneName 'vi-history-scenarios-linux-preflight' ` | |
| -RuntimeSnapshotPath (Join-Path $resultsRoot 'runtime-manager-preflight-linux.json') ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Run NI Linux create comparison report (fail-fast) | |
| id: linux-compare | |
| if: needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/linux' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $reportPath = Join-Path $resultsRoot 'linux-compare-report.html' | |
| "linux_compare_report=$reportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| $runtimeSnapshot = Join-Path $resultsRoot 'runtime-manager-compare-linux.json' | |
| pwsh -NoLogo -NoProfile -File tools/Run-NILinuxContainerCompare.ps1 ` | |
| -BaseVi 'fixtures/vi-stage/control-rename/Base.vi' ` | |
| -HeadVi 'fixtures/vi-stage/control-rename/Head.vi' ` | |
| -Image $env:NI_LINUX_IMAGE ` | |
| -LabVIEWPath $env:NI_LINUX_LABVIEW_PATH ` | |
| -ReportPath $reportPath ` | |
| -TimeoutSeconds 300 ` | |
| -AutoRepairRuntime:$true ` | |
| -RuntimeEngineReadyTimeoutSeconds 120 ` | |
| -RuntimeEngineReadyPollSeconds 3 ` | |
| -RuntimeSnapshotPath $runtimeSnapshot | |
| $compareExit = $LASTEXITCODE | |
| $capturePath = Join-Path $resultsRoot 'ni-linux-container-capture.json' | |
| if (Test-Path -LiteralPath $capturePath -PathType Leaf) { | |
| "linux_compare_capture=$capturePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| $capture = Get-Content -LiteralPath $capturePath -Raw | ConvertFrom-Json -Depth 12 | |
| $gateOutcome = if ($capture.PSObject.Properties['gateOutcome']) { [string]$capture.gateOutcome } else { '' } | |
| $resultClass = if ($capture.PSObject.Properties['resultClass']) { [string]$capture.resultClass } else { '' } | |
| $observedDockerHost = if ($capture.PSObject.Properties['observedDockerHost']) { [string]$capture.observedDockerHost } else { '' } | |
| $dockerContext = if ($capture.PSObject.Properties['dockerContext']) { [string]$capture.dockerContext } else { '' } | |
| $dockerServerOs = if ($capture.PSObject.Properties['dockerServerOs']) { [string]$capture.dockerServerOs } else { '' } | |
| Write-Host ("[linux-compare] exit={0} gateOutcome={1} resultClass={2}" -f $compareExit, $gateOutcome, $resultClass) | |
| Write-Host ("[linux-compare] observedDockerHost={0} dockerContext={1} dockerServerOs={2} capture={3}" -f ($observedDockerHost ?? '<null>'), ($dockerContext ?? '<null>'), ($dockerServerOs ?? '<null>'), $capturePath) | |
| if ($compareExit -ne 0 -and $gateOutcome -ne 'pass') { | |
| exit $compareExit | |
| } | |
| } elseif ($compareExit -ne 0) { | |
| throw ("Linux compare failed (exit={0}) and capture file was not found at {1}" -f $compareExit, $capturePath) | |
| } | |
| - name: Project VI history Linux Docker-side evidence | |
| if: always() && needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/linux' | |
| pwsh -NoLogo -NoProfile -File tools/Write-VIHistoryLaneEvidence.ps1 ` | |
| -LaneName 'vi-history-scenarios-linux' ` | |
| -RuntimeSnapshotPath (Join-Path $resultsRoot 'runtime-manager-compare-linux.json') ` | |
| -CapturePath (Join-Path $resultsRoot 'ni-linux-container-capture.json') ` | |
| -StdOutPath (Join-Path $resultsRoot 'ni-linux-container-stdout.txt') ` | |
| -StdErrPath (Join-Path $resultsRoot 'ni-linux-container-stderr.txt') ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Show Linux live Docker evidence | |
| if: always() && needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/linux' | |
| $capturePath = Join-Path $resultsRoot 'ni-linux-container-capture.json' | |
| if (Test-Path -LiteralPath $capturePath -PathType Leaf) { | |
| pwsh -NoLogo -NoProfile -File tools/Show-NIContainerCaptureEvidence.ps1 ` | |
| -CapturePath $capturePath ` | |
| -BasePath $resultsRoot ` | |
| -TailLineCount 20 | |
| } else { | |
| Write-Host ("::warning::Linux compare capture not found at {0}" -f $capturePath) | |
| } | |
| - name: Validate Linux comparison report artifact contract | |
| if: always() && needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/linux' | |
| $reportPath = '${{ steps.linux-compare.outputs.linux_compare_report }}' | |
| if ([string]::IsNullOrWhiteSpace($reportPath)) { | |
| $reportPath = Join-Path $resultsRoot 'linux-compare-report.html' | |
| } | |
| $capturePath = Join-Path $resultsRoot 'ni-linux-container-capture.json' | |
| $summaryPath = Join-Path $resultsRoot 'linux-compare-artifact-summary.json' | |
| $captureExists = Test-Path -LiteralPath $capturePath -PathType Leaf | |
| $recoveredFromCapture = $false | |
| $recoveredReportPath = '' | |
| $captureResultClass = '' | |
| $captureGateOutcome = '' | |
| if (-not (Test-Path -LiteralPath $reportPath -PathType Leaf) -and $captureExists) { | |
| try { | |
| $capture = Get-Content -LiteralPath $capturePath -Raw | ConvertFrom-Json -Depth 12 | |
| $captureResultClass = if ($capture.PSObject.Properties['resultClass']) { [string]$capture.resultClass } else { '' } | |
| $captureGateOutcome = if ($capture.PSObject.Properties['gateOutcome']) { [string]$capture.gateOutcome } else { '' } | |
| if ($capture.PSObject.Properties['reportAnalysis'] -and $capture.reportAnalysis) { | |
| $candidate = if ($capture.reportAnalysis.PSObject.Properties['reportPathExtracted']) { [string]$capture.reportAnalysis.reportPathExtracted } else { '' } | |
| if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { | |
| Copy-Item -LiteralPath $candidate -Destination $reportPath -Force | |
| $recoveredFromCapture = $true | |
| $recoveredReportPath = $candidate | |
| } | |
| } | |
| } catch { | |
| Write-Host ("::warning::Unable to parse Linux compare capture for report recovery: {0}" -f $_.Exception.Message) | |
| } | |
| } elseif ($captureExists) { | |
| try { | |
| $capture = Get-Content -LiteralPath $capturePath -Raw | ConvertFrom-Json -Depth 12 | |
| $captureResultClass = if ($capture.PSObject.Properties['resultClass']) { [string]$capture.resultClass } else { '' } | |
| $captureGateOutcome = if ($capture.PSObject.Properties['gateOutcome']) { [string]$capture.gateOutcome } else { '' } | |
| } catch {} | |
| } | |
| $reportExists = Test-Path -LiteralPath $reportPath -PathType Leaf | |
| $compareSucceeded = '${{ steps.linux-compare.conclusion }}' -eq 'success' | |
| $payload = [ordered]@{ | |
| schema = 'vi-history/linux-compare-artifact-summary@v1' | |
| generatedAt = (Get-Date).ToUniversalTime().ToString('o') | |
| compareStepConclusion = '${{ steps.linux-compare.conclusion }}' | |
| reportPath = $reportPath | |
| reportExists = [bool]$reportExists | |
| reportRecoveredFromCapture = [bool]$recoveredFromCapture | |
| recoveredReportPath = $recoveredReportPath | |
| capturePath = $capturePath | |
| captureExists = [bool]$captureExists | |
| captureResultClass = $captureResultClass | |
| captureGateOutcome = $captureGateOutcome | |
| } | |
| $payload | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $summaryPath -Encoding utf8 | |
| $requireReport = $compareSucceeded -and [string]::Equals($captureResultClass, 'success-diff', [System.StringComparison]::OrdinalIgnoreCase) | |
| if ($requireReport -and -not $reportExists) { | |
| throw ("Linux compare classified success-diff but report file was not found: {0}" -f $reportPath) | |
| } | |
| if ($compareSucceeded -and -not $reportExists -and -not $requireReport) { | |
| Write-Host ("::warning::Linux compare succeeded without diff classification ({0}); no report file was generated at {1}." -f $captureResultClass, $reportPath) | |
| } | |
| if (-not $reportExists) { | |
| Write-Host ("::warning::Linux comparison report not found at {0}" -f $reportPath) | |
| } | |
| - name: Upload VI history scenario artifacts (Linux lane) | |
| if: always() && needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-vi-history-scenarios-linux | |
| path: | | |
| tests/results/local-parity/linux/linux-compare-report.html | |
| tests/results/local-parity/linux/linux-compare-artifact-summary.json | |
| tests/results/local-parity/linux/ni-linux-container-capture.json | |
| tests/results/local-parity/linux/ni-linux-container-stdout.txt | |
| tests/results/local-parity/linux/ni-linux-container-stderr.txt | |
| tests/results/local-parity/linux/container-export/** | |
| tests/results/local-parity/linux/runtime-manager-preflight-linux.json | |
| tests/results/local-parity/linux/runtime-manager-compare-linux.json | |
| if-no-files-found: warn | |
| vi-history-scenarios-windows-plan: | |
| needs: [smoke-gate, lint, session-index, session-index-v2-contract, vi-history-scenarios-plan] | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| available: ${{ steps.plan.outputs.available }} | |
| status: ${{ steps.plan.outputs.status }} | |
| skip_reason: ${{ steps.plan.outputs.skip_reason }} | |
| summary_path: ${{ steps.plan.outputs.summary_path }} | |
| execution_model: ${{ steps.plan.outputs.execution_model }} | |
| runner_image: ${{ steps.plan.outputs.runner_image }} | |
| expected_context: ${{ steps.plan.outputs.expected_context }} | |
| expected_os: ${{ steps.plan.outputs.expected_os }} | |
| required_labels: ${{ steps.plan.outputs.required_labels }} | |
| matching_runner_count: ${{ steps.plan.outputs.matching_runner_count }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Resolve self-hosted Windows Docker lane | |
| id: plan | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/vi-history-dispatch' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $planPath = Join-Path $resultsRoot 'validate-vi-history-windows-docker-plan.json' | |
| $requiredLabels = @( | |
| 'self-hosted', | |
| 'Windows', | |
| 'X64', | |
| 'comparevi', | |
| 'capability-ingress', | |
| 'docker-lane' | |
| ) | |
| $requiredHealthReceipts = @() | |
| $plannerArgs = @{ | |
| Repository = '${{ github.repository }}' | |
| RequiredLabels = $requiredLabels | |
| ExecutionModel = 'self-hosted-windows-docker-lane' | |
| RunnerImage = 'self-hosted-windows-docker-lane' | |
| ExpectedContext = 'desktop-windows' | |
| ExpectedOs = 'windows' | |
| Notes = @( | |
| 'Availability means an online, idle repository runner advertises the ingress plus docker-lane labels.', | |
| 'The lane may mutate Docker Desktop into the Windows engine and then restores the starting context after proof capture.' | |
| ) | |
| Token = $env:GITHUB_TOKEN | |
| OutputJsonPath = $planPath | |
| GitHubOutputPath = $env:GITHUB_OUTPUT | |
| StepSummaryPath = $env:GITHUB_STEP_SUMMARY | |
| } | |
| if ($requiredHealthReceipts.Count -gt 0) { | |
| $plannerArgs.RequiredHealthReceipts = $requiredHealthReceipts | |
| } | |
| & tools/Resolve-SelfHostedWindowsLanePlan.ps1 @plannerArgs | |
| - name: Append VI history Windows runner plan | |
| shell: pwsh | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @( | |
| '### VI History Scenarios (Windows self-hosted Docker plan)', | |
| '', | |
| ('- execute_lanes: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.execute_lanes }}'), | |
| ('- lane_available: `{0}`' -f '${{ steps.plan.outputs.available }}'), | |
| ('- lane_status: `{0}`' -f '${{ steps.plan.outputs.status }}'), | |
| ('- skip_reason: `{0}`' -f '${{ steps.plan.outputs.skip_reason }}'), | |
| ('- execution_model: `{0}`' -f '${{ steps.plan.outputs.execution_model }}'), | |
| ('- runner_image: `{0}`' -f '${{ steps.plan.outputs.runner_image }}'), | |
| ('- required_labels: `{0}`' -f '${{ steps.plan.outputs.required_labels }}'), | |
| '- self-hosted docker model: validate 64-bit LabVIEW in the Windows container while the LV32 lane runs in parallel as the native host-plane reference.' | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append | |
| } | |
| - name: Upload VI history Windows runner plan artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-vi-history-windows-docker-plan | |
| path: tests/results/_agent/vi-history-dispatch/validate-vi-history-windows-docker-plan.json | |
| if-no-files-found: error | |
| vi-history-scenarios-windows: | |
| needs: [smoke-gate, lint, session-index, session-index-v2-contract, vi-history-scenarios-plan, vi-history-scenarios-windows-plan] | |
| if: needs.smoke-gate.outputs.skip != 'true' && needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' && needs.vi-history-scenarios-windows-plan.outputs.available == 'true' | |
| runs-on: [self-hosted, Windows, X64, comparevi, capability-ingress, docker-lane] | |
| continue-on-error: true | |
| timeout-minutes: 50 | |
| permissions: | |
| actions: read | |
| contents: read | |
| env: | |
| NI_WINDOWS_IMAGE: nationalinstruments/labview:2026q1-windows | |
| NI_WINDOWS_LABVIEW_PATH: C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Append VI history Windows lane plan | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @( | |
| '### VI History Scenarios (Windows self-hosted Docker lane)', | |
| '', | |
| ('- execute_lanes: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.execute_lanes }}'), | |
| ('- plan_status: `{0}`' -f '${{ needs.vi-history-scenarios-windows-plan.outputs.status }}'), | |
| ('- runner_image: `{0}`' -f '${{ needs.vi-history-scenarios-windows-plan.outputs.runner_image }}'), | |
| ('- expected_context: `{0}`' -f '${{ needs.vi-history-scenarios-windows-plan.outputs.expected_context }}'), | |
| ('- required_labels: `{0}`' -f '${{ needs.vi-history-scenarios-windows-plan.outputs.required_labels }}'), | |
| '- self-hosted 64-bit proof runs on the ingress host and pairs with the parallel LV32 lane so 32-bit host-plane drift has an immediate native reference.' | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append | |
| } | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Validate self-hosted runner label contract | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = Join-Path $env:GITHUB_WORKSPACE 'tests/results/local-parity/windows' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $requiredLabels = @( | |
| 'self-hosted', | |
| 'Windows', | |
| 'X64', | |
| 'comparevi', | |
| 'capability-ingress', | |
| 'docker-lane' | |
| ) | |
| foreach ($label in $requiredLabels) { | |
| $contractPath = Join-Path $resultsRoot ("runner-label-contract-{0}.json" -f $label) | |
| pwsh -NoLogo -NoProfile -File tools/Assert-RunnerLabelContract.ps1 ` | |
| -Repository '${{ github.repository }}' ` | |
| -RunnerName $env:RUNNER_NAME ` | |
| -RequiredLabel $label ` | |
| -Token $env:GITHUB_TOKEN ` | |
| -OutputJsonPath $contractPath ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| } | |
| - name: Collect self-hosted Windows runner health | |
| shell: pwsh | |
| run: | | |
| pwsh -NoLogo -NoProfile -File tools/Collect-RunnerHealth.ps1 ` | |
| -ResultsDir 'tests/results/local-parity/windows' ` | |
| -AppendSummary ` | |
| -EmitJson | |
| - name: Prepare NI Windows image and runtime | |
| id: windows-preflight | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/windows' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $summaryPath = Join-Path $resultsRoot 'windows-ni-2026q1-host-preflight.json' | |
| pwsh -NoLogo -NoProfile -File tools/Test-WindowsNI2026q1HostPreflight.ps1 ` | |
| -Image $env:NI_WINDOWS_IMAGE ` | |
| -ResultsDir $resultsRoot ` | |
| -ExecutionSurface 'desktop-local' ` | |
| -ManageDockerEngine:$true ` | |
| -AllowHostEngineMutation:$true ` | |
| -OutputJsonPath $summaryPath ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Print VI history Windows runtime alignment | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/windows' | |
| pwsh -NoLogo -NoProfile -File tools/Write-VIHistoryLaneEvidence.ps1 ` | |
| -LaneName 'vi-history-scenarios-windows-preflight' ` | |
| -PreflightPath (Join-Path $resultsRoot 'windows-ni-2026q1-host-preflight.json') ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Run NI Windows create comparison report (fail-fast) | |
| id: windows-compare | |
| if: steps.windows-preflight.outputs.windows_host_preflight_status == 'ready' | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/windows' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $reportPath = Join-Path $resultsRoot 'windows-compare-report.html' | |
| "windows_compare_report=$reportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| $runtimeSnapshot = Join-Path $resultsRoot 'runtime-manager-compare-windows.json' | |
| pwsh -NoLogo -NoProfile -File tools/Run-NIWindowsContainerCompare.ps1 ` | |
| -BaseVi 'fixtures/vi-stage/control-rename/Base.vi' ` | |
| -HeadVi 'fixtures/vi-stage/control-rename/Head.vi' ` | |
| -Image $env:NI_WINDOWS_IMAGE ` | |
| -LabVIEWPath $env:NI_WINDOWS_LABVIEW_PATH ` | |
| -ReportPath $reportPath ` | |
| -TimeoutSeconds 600 ` | |
| -ManageDockerEngine:$false ` | |
| -AllowHostEngineMutation:$false ` | |
| -RuntimeEngineReadyTimeoutSeconds 180 ` | |
| -RuntimeEngineReadyPollSeconds 5 ` | |
| -RuntimeSnapshotPath $runtimeSnapshot | |
| $compareExit = $LASTEXITCODE | |
| $capturePath = Join-Path $resultsRoot 'ni-windows-container-capture.json' | |
| if (Test-Path -LiteralPath $capturePath -PathType Leaf) { | |
| "windows_compare_capture=$capturePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| $capture = Get-Content -LiteralPath $capturePath -Raw | ConvertFrom-Json -Depth 12 | |
| $gateOutcome = if ($capture.PSObject.Properties['gateOutcome']) { [string]$capture.gateOutcome } else { '' } | |
| $resultClass = if ($capture.PSObject.Properties['resultClass']) { [string]$capture.resultClass } else { '' } | |
| $observedDockerHost = if ($capture.PSObject.Properties['observedDockerHost']) { [string]$capture.observedDockerHost } else { '' } | |
| $dockerContext = if ($capture.PSObject.Properties['dockerContext']) { [string]$capture.dockerContext } else { '' } | |
| $dockerServerOs = if ($capture.PSObject.Properties['dockerServerOs']) { [string]$capture.dockerServerOs } else { '' } | |
| Write-Host ("[windows-compare] exit={0} gateOutcome={1} resultClass={2}" -f $compareExit, $gateOutcome, $resultClass) | |
| Write-Host ("[windows-compare] observedDockerHost={0} dockerContext={1} dockerServerOs={2} capture={3}" -f ($observedDockerHost ?? '<null>'), ($dockerContext ?? '<null>'), ($dockerServerOs ?? '<null>'), $capturePath) | |
| if ($compareExit -ne 0 -and $gateOutcome -ne 'pass') { | |
| exit $compareExit | |
| } | |
| } elseif ($compareExit -ne 0) { | |
| throw ("Windows compare failed (exit={0}) and capture file was not found at {1}" -f $compareExit, $capturePath) | |
| } | |
| - name: Project VI history Windows Docker-side evidence | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/windows' | |
| pwsh -NoLogo -NoProfile -File tools/Write-VIHistoryLaneEvidence.ps1 ` | |
| -LaneName 'vi-history-scenarios-windows' ` | |
| -RuntimeSnapshotPath (Join-Path $resultsRoot 'runtime-manager-compare-windows.json') ` | |
| -PreflightPath (Join-Path $resultsRoot 'windows-ni-2026q1-host-preflight.json') ` | |
| -CapturePath (Join-Path $resultsRoot 'ni-windows-container-capture.json') ` | |
| -StdOutPath (Join-Path $resultsRoot 'ni-windows-container-stdout.txt') ` | |
| -StdErrPath (Join-Path $resultsRoot 'ni-windows-container-stderr.txt') ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| - name: Validate Windows comparison report artifact contract | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/windows' | |
| $reportPath = '${{ steps.windows-compare.outputs.windows_compare_report }}' | |
| if ([string]::IsNullOrWhiteSpace($reportPath)) { | |
| $reportPath = Join-Path $resultsRoot 'windows-compare-report.html' | |
| } | |
| $capturePath = Join-Path $resultsRoot 'ni-windows-container-capture.json' | |
| $summaryPath = Join-Path $resultsRoot 'windows-compare-artifact-summary.json' | |
| $captureExists = Test-Path -LiteralPath $capturePath -PathType Leaf | |
| $captureResultClass = '' | |
| $captureGateOutcome = '' | |
| if ($captureExists) { | |
| try { | |
| $capture = Get-Content -LiteralPath $capturePath -Raw | ConvertFrom-Json -Depth 12 | |
| $captureResultClass = if ($capture.PSObject.Properties['resultClass']) { [string]$capture.resultClass } else { '' } | |
| $captureGateOutcome = if ($capture.PSObject.Properties['gateOutcome']) { [string]$capture.gateOutcome } else { '' } | |
| } catch { | |
| Write-Host ("::warning::Unable to parse Windows compare capture for artifact validation: {0}" -f $_.Exception.Message) | |
| } | |
| } | |
| $reportExists = Test-Path -LiteralPath $reportPath -PathType Leaf | |
| $compareSucceeded = '${{ steps.windows-compare.conclusion }}' -eq 'success' | |
| $payload = [ordered]@{ | |
| schema = 'vi-history/windows-compare-artifact-summary@v1' | |
| generatedAt = (Get-Date).ToUniversalTime().ToString('o') | |
| compareStepConclusion = '${{ steps.windows-compare.conclusion }}' | |
| reportPath = $reportPath | |
| reportExists = [bool]$reportExists | |
| capturePath = $capturePath | |
| captureExists = [bool]$captureExists | |
| captureResultClass = $captureResultClass | |
| captureGateOutcome = $captureGateOutcome | |
| } | |
| $payload | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $summaryPath -Encoding utf8 | |
| $requireReport = $compareSucceeded -and [string]::Equals($captureResultClass, 'success-diff', [System.StringComparison]::OrdinalIgnoreCase) | |
| if ($requireReport -and -not $reportExists) { | |
| throw ("Windows compare classified success-diff but report file was not found: {0}" -f $reportPath) | |
| } | |
| if ($compareSucceeded -and -not $reportExists -and -not $requireReport) { | |
| Write-Host ("::warning::Windows compare succeeded without diff classification ({0}); no report file was generated at {1}." -f $captureResultClass, $reportPath) | |
| } | |
| if (-not $reportExists) { | |
| Write-Host ("::warning::Windows comparison report not found at {0}" -f $reportPath) | |
| } | |
| - name: Summarize skipped hosted Windows compare | |
| if: always() && steps.windows-preflight.outputs.windows_host_preflight_status != 'ready' | |
| shell: pwsh | |
| run: | | |
| Write-Host ("::notice::Skipping self-hosted Windows Docker compare because preflight status was {0} ({1})." -f '${{ steps.windows-preflight.outputs.windows_host_preflight_status }}', '${{ steps.windows-preflight.outputs.windows_host_preflight_failure_class }}') | |
| - name: Restore Docker Desktop context after Windows proof | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = 'tests/results/local-parity/windows' | |
| $preflightPath = Join-Path $resultsRoot 'windows-ni-2026q1-host-preflight.json' | |
| if (-not (Test-Path -LiteralPath $preflightPath -PathType Leaf)) { | |
| Write-Host ("::notice::Skipping Docker Desktop restore because preflight artifact was not found at {0}" -f $preflightPath) | |
| exit 0 | |
| } | |
| $preflight = Get-Content -LiteralPath $preflightPath -Raw | ConvertFrom-Json -Depth 20 | |
| $restoreContext = if ($preflight.contexts.PSObject.Properties['start'] -and -not [string]::IsNullOrWhiteSpace([string]$preflight.contexts.start)) { | |
| [string]$preflight.contexts.start | |
| } else { | |
| 'desktop-windows' | |
| } | |
| $restorePath = Join-Path $resultsRoot 'runtime-manager-restore-windows.json' | |
| pwsh -NoLogo -NoProfile -File tools/Invoke-DockerRuntimeManager.ps1 ` | |
| -ProbeScope 'windows' ` | |
| -BootstrapWindowsImage:$false ` | |
| -BootstrapLinuxImage:$false ` | |
| -RestoreContext $restoreContext ` | |
| -OutputJsonPath $restorePath ` | |
| -GitHubOutputPath '' ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | Out-Null | |
| - name: Upload VI history scenario artifacts (Windows lane) | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-vi-history-scenarios-windows | |
| path: | | |
| tests/results/local-parity/windows/windows-compare-report.html | |
| tests/results/local-parity/windows/windows-compare-artifact-summary.json | |
| tests/results/local-parity/windows/ni-windows-container-capture.json | |
| tests/results/local-parity/windows/ni-windows-container-stdout.txt | |
| tests/results/local-parity/windows/ni-windows-container-stderr.txt | |
| tests/results/local-parity/windows/container-export/** | |
| tests/results/local-parity/windows/windows-ni-2026q1-host-preflight.json | |
| tests/results/local-parity/windows/runtime-manager-compare-windows.json | |
| tests/results/local-parity/windows/runtime-manager-restore-windows.json | |
| tests/results/local-parity/windows/runner-label-contract-*.json | |
| tests/results/local-parity/windows/_agent/runner-health.json | |
| if-no-files-found: warn | |
| vi-history-scenarios-windows-lv32-plan: | |
| needs: [smoke-gate, lint, session-index, session-index-v2-contract, vi-history-scenarios-plan] | |
| if: needs.smoke-gate.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| outputs: | |
| available: ${{ steps.plan.outputs.available }} | |
| status: ${{ steps.plan.outputs.status }} | |
| skip_reason: ${{ steps.plan.outputs.skip_reason }} | |
| summary_path: ${{ steps.plan.outputs.summary_path }} | |
| execution_model: ${{ steps.plan.outputs.execution_model }} | |
| runner_image: ${{ steps.plan.outputs.runner_image }} | |
| expected_context: ${{ steps.plan.outputs.expected_context }} | |
| expected_os: ${{ steps.plan.outputs.expected_os }} | |
| required_labels: ${{ steps.plan.outputs.required_labels }} | |
| matching_runner_count: ${{ steps.plan.outputs.matching_runner_count }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Resolve self-hosted Windows LV32 lane | |
| id: plan | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| $resultsRoot = 'tests/results/_agent/vi-history-dispatch' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $planPath = Join-Path $resultsRoot 'validate-vi-history-windows-lv32-plan.json' | |
| pwsh -NoLogo -NoProfile -File tools/Resolve-SelfHostedWindowsLanePlan.ps1 ` | |
| -Repository '${{ github.repository }}' ` | |
| -OutputJsonPath $planPath ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY ` | |
| -Token $env:GITHUB_TOKEN | |
| - name: Upload self-hosted Windows LV32 lane plan artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-vi-history-windows-lv32-plan | |
| path: tests/results/_agent/vi-history-dispatch/validate-vi-history-windows-lv32-plan.json | |
| if-no-files-found: error | |
| vi-history-scenarios-windows-lv32: | |
| needs: [smoke-gate, lint, session-index, session-index-v2-contract, vi-history-scenarios-plan, vi-history-scenarios-windows-lv32-plan] | |
| if: needs.smoke-gate.outputs.skip != 'true' && needs.vi-history-scenarios-plan.outputs.execute_lanes == 'true' && needs.vi-history-scenarios-windows-lv32-plan.outputs.available == 'true' | |
| runs-on: [self-hosted, Windows, X64, comparevi, capability-ingress, labview-2026, lv32] | |
| continue-on-error: true | |
| timeout-minutes: 60 | |
| permissions: | |
| actions: read | |
| contents: read | |
| env: | |
| LVCI_COMPARE_MODE: labview-cli | |
| LVCI_COMPARE_POLICY: cli-only | |
| LV_SUPPRESS_UI: '1' | |
| LV_NO_ACTIVATE: '1' | |
| LV_CURSOR_RESTORE: '1' | |
| UNBLOCK_GUARD: '1' | |
| VI_HISTORY_LV32_RESULTS_ROOT: tests/results/local-parity/windows/lv32-shadow-proof | |
| VI_HISTORY_LV32_RECEIPT_PATH: tests/results/_agent/promotion/vi-history-lv32-shadow-proof-receipt.json | |
| VI_HISTORY_LV32_PLAN_PATH: tests/results/_agent/vi-history-dispatch/validate-vi-history-windows-lv32-plan.json | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Append VI history self-hosted LV32 lane plan | |
| run: | | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @( | |
| '### VI History Scenarios (Windows self-hosted LV32 shadow proof)', | |
| '', | |
| ('- execute_lanes: `{0}`' -f '${{ needs.vi-history-scenarios-plan.outputs.execute_lanes }}'), | |
| ('- plan_status: `{0}`' -f '${{ needs.vi-history-scenarios-windows-lv32-plan.outputs.status }}'), | |
| ('- runner_image: `{0}`' -f '${{ needs.vi-history-scenarios-windows-lv32-plan.outputs.runner_image }}'), | |
| ('- expected_context: `{0}`' -f '${{ needs.vi-history-scenarios-windows-lv32-plan.outputs.expected_context }}'), | |
| ('- required_labels: `{0}`' -f '${{ needs.vi-history-scenarios-windows-lv32-plan.outputs.required_labels }}'), | |
| '- shadow proof is non-required and records promotion-ready evidence only when the self-hosted LV32 runner and host-plane are both ready.' | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append | |
| } | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results | |
| - name: Validate self-hosted runner label contract | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| $resultsRoot = Join-Path $env:GITHUB_WORKSPACE 'tests/results/local-parity/windows/lv32-shadow-proof' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $requiredLabels = @( | |
| 'self-hosted', | |
| 'Windows', | |
| 'X64', | |
| 'comparevi', | |
| 'capability-ingress', | |
| 'labview-2026', | |
| 'lv32' | |
| ) | |
| foreach ($label in $requiredLabels) { | |
| $contractPath = Join-Path $resultsRoot ("runner-label-contract-{0}.json" -f $label) | |
| pwsh -NoLogo -NoProfile -File tools/Assert-RunnerLabelContract.ps1 ` | |
| -Repository '${{ github.repository }}' ` | |
| -RunnerName $env:RUNNER_NAME ` | |
| -RequiredLabel $label ` | |
| -Token $env:GITHUB_TOKEN ` | |
| -OutputJsonPath $contractPath ` | |
| -GitHubOutputPath $env:GITHUB_OUTPUT ` | |
| -StepSummaryPath $env:GITHUB_STEP_SUMMARY | |
| } | |
| - name: Capture LabVIEW 2026 host-plane diagnostics | |
| id: host-plane | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = Join-Path $env:GITHUB_WORKSPACE 'tests/results/local-parity/windows/lv32-shadow-proof' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $reportPath = Join-Path $env:GITHUB_WORKSPACE 'tests/results/_agent/host-planes/labview-2026-host-plane-report.json' | |
| pwsh -NoLogo -NoProfile -File tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1 -OutputPath $reportPath | Out-Null | |
| if (-not (Test-Path -LiteralPath $reportPath -PathType Leaf)) { | |
| throw ("LabVIEW 2026 host-plane diagnostics report was not written to {0}" -f $reportPath) | |
| } | |
| $report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json -Depth 12 | |
| if ($report.native.planes.x32.status -ne 'ready') { | |
| throw ("Native LabVIEW 32-bit host plane is not ready. Observed status: {0}" -f $report.native.planes.x32.status) | |
| } | |
| $labviewPath = [string]$report.native.planes.x32.labviewPath | |
| if ([string]::IsNullOrWhiteSpace($labviewPath)) { | |
| throw 'Native LabVIEW 32-bit host-plane report did not include LabVIEW path.' | |
| } | |
| if (-not (Test-Path -LiteralPath $labviewPath -PathType Leaf)) { | |
| throw ("Native LabVIEW 32-bit LabVIEW.exe was not found at {0}" -f $labviewPath) | |
| } | |
| "host_plane_report_path=$reportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "labview_path=$labviewPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "labview_cli_path=$([string]$report.native.planes.x32.cliPath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "compare_path=$([string]$report.native.planes.x32.comparePath)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "labview_32_status=$([string]$report.native.planes.x32.status)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| $env:LABVIEW_PATH = $labviewPath | |
| $env:LABVIEW_CLI_PATH = [string]$report.native.planes.x32.cliPath | |
| $env:LABVIEWCLI_PATH = [string]$report.native.planes.x32.cliPath | |
| $reportPath | Out-File -FilePath (Join-Path $resultsRoot 'host-plane-report-path.txt') -Encoding utf8 | |
| - name: Run VI history shadow proof on the self-hosted LV32 runner | |
| id: history-shadow | |
| shell: pwsh | |
| env: | |
| LABVIEW_PATH: ${{ steps.host-plane.outputs.labview_path }} | |
| LABVIEW_CLI_PATH: ${{ steps.host-plane.outputs.labview_cli_path }} | |
| LABVIEWCLI_PATH: ${{ steps.host-plane.outputs.labview_cli_path }} | |
| run: | | |
| $resultsRoot = Join-Path $env:GITHUB_WORKSPACE 'tests/results/local-parity/windows/lv32-shadow-proof' | |
| New-Item -ItemType Directory -Path $resultsRoot -Force | Out-Null | |
| $historyResultsRoot = Join-Path $resultsRoot 'history' | |
| New-Item -ItemType Directory -Path $historyResultsRoot -Force | Out-Null | |
| $invokeScriptPath = Join-Path $env:GITHUB_WORKSPACE 'tools/Invoke-LVCompare.ps1' | |
| pwsh -NoLogo -NoProfile -File tools/Compare-VIHistory.ps1 ` | |
| -TargetPath 'VI1.vi' ` | |
| -StartRef HEAD ` | |
| -MaxPairs 1 ` | |
| -Mode default ` | |
| -RenderReport ` | |
| -FailOnDiff:$false ` | |
| -ResultsDir $historyResultsRoot ` | |
| -InvokeScriptPath $invokeScriptPath | |
| $summaryPath = Join-Path $historyResultsRoot 'history-summary.json' | |
| $reportPath = Join-Path $historyResultsRoot 'history-report.html' | |
| if (-not (Test-Path -LiteralPath $summaryPath -PathType Leaf)) { | |
| throw ("VI history compare did not emit a summary at {0}" -f $summaryPath) | |
| } | |
| "history_summary_path=$summaryPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| "history_report_path=$reportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Write VI history LV32 shadow proof receipt | |
| shell: pwsh | |
| run: | | |
| $resultsRoot = Join-Path $env:GITHUB_WORKSPACE 'tests/results/local-parity/windows/lv32-shadow-proof' | |
| $runnerContractPaths = @( | |
| 'runner-label-contract-self-hosted.json', | |
| 'runner-label-contract-Windows.json', | |
| 'runner-label-contract-X64.json', | |
| 'runner-label-contract-comparevi.json', | |
| 'runner-label-contract-capability-ingress.json', | |
| 'runner-label-contract-labview-2026.json', | |
| 'runner-label-contract-lv32.json' | |
| ) | ForEach-Object { Join-Path $resultsRoot $_ } | |
| $receiptScriptPath = Join-Path $env:GITHUB_WORKSPACE 'tools/Write-VIHistoryLV32ShadowProofReceipt.ps1' | |
| $receiptArgs = @{ | |
| LaneName = 'vi-history-scenarios-windows-lv32' | |
| RunnerLabelContractPaths = $runnerContractPaths | |
| HostPlaneReportPath = '${{ steps.host-plane.outputs.host_plane_report_path }}' | |
| CompareSummaryPath = '${{ steps.history-shadow.outputs.history_summary_path }}' | |
| CompareReportPath = '${{ steps.history-shadow.outputs.history_report_path }}' | |
| OutputJsonPath = $env:VI_HISTORY_LV32_RECEIPT_PATH | |
| GitHubOutputPath = $env:GITHUB_OUTPUT | |
| StepSummaryPath = $env:GITHUB_STEP_SUMMARY | |
| } | |
| & $receiptScriptPath @receiptArgs | |
| - name: Runner Unblock Guard | |
| if: always() | |
| uses: ./.github/actions/runner-unblock-guard | |
| with: | |
| snapshot-path: tests/results/local-parity/windows/lv32-shadow-proof/runner-unblock-snapshot.json | |
| cleanup: ${{ env.UNBLOCK_GUARD == '1' }} | |
| process-names: 'conhost,pwsh,LabVIEW,LVCompare,LabVIEWCLI' | |
| - name: Upload VI history LV32 shadow proof artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validate-vi-history-scenarios-windows-lv32 | |
| path: | | |
| tests/results/local-parity/windows/lv32-shadow-proof/** | |
| tests/results/_agent/promotion/vi-history-lv32-shadow-proof-receipt.json | |
| tests/results/_agent/host-planes/labview-2026-host-plane-report.json | |
| if-no-files-found: warn |