Implement dual-fork merge-queue delivery for origin and personal fork… #561
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: CI Orchestrated (deterministic chain) | |
| on: | |
| push: | |
| branches: [develop] | |
| workflow_dispatch: | |
| inputs: | |
| origin_kind: | |
| description: Trigger origin (e.g., issue_comment) | |
| required: false | |
| type: string | |
| origin_pr: | |
| description: PR number when triggered from a comment | |
| required: false | |
| type: string | |
| origin_comment_id: | |
| description: Comment id when triggered from a comment | |
| required: false | |
| type: string | |
| origin_comment_url: | |
| description: Comment URL when triggered from a comment | |
| required: false | |
| type: string | |
| origin_author: | |
| description: Comment author login | |
| required: false | |
| type: string | |
| strategy: | |
| description: Execution strategy (matrix or single) | |
| required: false | |
| default: single | |
| type: choice | |
| options: [matrix, single] | |
| include_integration: | |
| description: Include Integration-tagged tests | |
| required: false | |
| default: 'true' | |
| type: choice | |
| options: ['true', 'false'] | |
| sample_id: | |
| description: Sampling correlation id (prevents cancels) | |
| required: false | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.inputs.sample_id || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| normalize: | |
| runs-on: ubuntu-latest | |
| env: | |
| RESULTS_DIR: tests/results/normalize | |
| outputs: | |
| include_integration: ${{ steps.b.outputs.normalized }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - 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: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Wire Invoker (start) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-start | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Ensure Invoker (start) | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: start | |
| timeout-seconds: '60' | |
| retries: '1' | |
| retry-delay-seconds: '0' | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| require-invoker: 'false' | |
| - name: Normalize include_integration | |
| id: b | |
| uses: ./.github/actions/bool-normalize | |
| with: | |
| value: ${{ inputs.include_integration || 'true' }} | |
| - name: Write run provenance | |
| shell: pwsh | |
| env: | |
| EV_ORIGIN_KIND: ${{ inputs.origin_kind || '' }} | |
| EV_ORIGIN_PR: ${{ inputs.origin_pr || '' }} | |
| EV_ORIGIN_COMMENT_ID: ${{ inputs.origin_comment_id || '' }} | |
| EV_ORIGIN_COMMENT_URL: ${{ inputs.origin_comment_url || '' }} | |
| EV_ORIGIN_AUTHOR: ${{ inputs.origin_author || '' }} | |
| EV_SAMPLE_ID: ${{ inputs.sample_id || '' }} | |
| EV_INCLUDE_INTEGRATION: ${{ inputs.include_integration || 'true' }} | |
| EV_STRATEGY: ${{ inputs.strategy || 'matrix' }} | |
| run: | | |
| pwsh -NoLogo -NoProfile -File tools/Write-RunProvenance.ps1 -ResultsDir '${{ env.RESULTS_DIR }}' -AppendStepSummary | |
| - name: Upload run provenance | |
| if: always() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: orchestrated-provenance | |
| path: ${{ env.RESULTS_DIR }}/provenance.json | |
| lint: | |
| runs-on: ubuntu-latest | |
| env: | |
| RESULTS_DIR: tests/results/lint | |
| ACTIONLINT_VERSION: '${{ vars.ACTIONLINT_VERSION || ''1.7.7'' }}' | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} | |
| COMPAREVI_TOOLS_IMAGE: ghcr.io/labview-community-ci-cd/comparevi-tools:latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - 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.7}" | |
| 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: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Non-LabVIEW checks (Docker) | |
| shell: pwsh | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch || github.event.pull_request.base.repo.default_branch || github.event.workflow_run.repository.default_branch || '' }} | |
| run: | | |
| $params = @() | |
| $defaultBranch = $env:DEFAULT_BRANCH | |
| if ('${{ github.ref_name }}' -eq $defaultBranch -or '${{ github.base_ref }}' -eq $defaultBranch) { | |
| $params += '-FailOnWorkflowDrift' | |
| } | |
| $params += '-SkipDotnetCliBuild' | |
| # Skip linting this workflow while it is executing to avoid orchestration deadlocks. | |
| $params += '-ExcludeWorkflowPaths' | |
| $params += '.github/workflows/ci-orchestrated.yml' | |
| pwsh -File ./tools/Run-NonLVChecksInDocker.ps1 @params | |
| - name: Publish comparevi-cli (SDK container) | |
| if: ${{ hashFiles('src/CompareVi.Tools.Cli/CompareVi.Tools.Cli.csproj') != '' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| SHA="${GITHUB_SHA:-local}" | |
| docker run --rm -v "${PWD}:/work" -w /work -e BUILD_SHA="$SHA" mcr.microsoft.com/dotnet/sdk:8.0 bash -lc ' | |
| set -euo pipefail | |
| export DOTNET_CLI_HOME=/tmp/dotnet | |
| rm -rf dist/comparevi-cli src/CompareVi.Tools.Cli/obj src/CompareVi.Shared/obj || true | |
| IV="${BUILD_SHA:-local}" | |
| dotnet publish src/CompareVi.Tools.Cli/CompareVi.Tools.Cli.csproj -c Release -nologo -o dist/comparevi-cli -p:UseAppHost=false -p:InformationalVersion="$IV" | |
| ' | |
| - name: Validate comparevi-cli outputs | |
| if: ${{ hashFiles('dist/comparevi-cli/comparevi-cli.dll') != '' }} | |
| run: node tools/npm/run-script.mjs cli:validate | |
| - name: Watch run (REST sanity) | |
| if: ${{ hashFiles('dist/tools/watchers/orchestrated-watch.js') != '' }} | |
| run: node dist/tools/watchers/orchestrated-watch.js --run-id ${{ github.run_id }} --poll-ms 1000 --out "${{ env.RESULTS_DIR }}/watcher-rest.json" | |
| - name: Merge REST watcher into session index | |
| if: ${{ hashFiles(format('{0}/watcher-rest.json', env.RESULTS_DIR)) != '' }} | |
| shell: pwsh | |
| run: pwsh -File tools/Update-SessionIndexWatcher.ps1 -ResultsDir "${{ env.RESULTS_DIR }}" -WatcherJson "${{ env.RESULTS_DIR }}/watcher-rest.json" | |
| - name: Compute comparevi-cli checksums | |
| if: ${{ hashFiles('dist/comparevi-cli/**') != '' }} | |
| shell: pwsh | |
| run: | | |
| $dir = 'dist/comparevi-cli' | |
| $outDir = 'tests/results/_cli' | |
| New-Item -ItemType Directory -Force -Path $outDir | Out-Null | |
| $files = Get-ChildItem -LiteralPath $dir -File -Recurse | |
| $list = @() | |
| foreach($f in $files){ | |
| $sha = pwsh -File tools/Get-FileSha256.ps1 -Path $f.FullName | |
| $list += [pscustomobject]@{ path = (Resolve-Path $f.FullName).Path; sha256 = $sha; bytes = $f.Length } | |
| } | |
| $meta = [pscustomobject]@{ | |
| gitSha = '${{ github.sha }}' | |
| branch = '${{ github.ref_name }}' | |
| generatedAt = (Get-Date).ToString('o') | |
| files = $list | |
| } | |
| $json = $meta | ConvertTo-Json -Depth 6 | |
| $json | Out-File -FilePath (Join-Path $outDir 'meta.json') -Encoding utf8 | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @('### comparevi-cli artifact','',"- Files: $($files.Count)") | |
| $first = $list | Select-Object -First 1 | |
| if ($first) { $lines += ("- First file: {0} (sha256={1})" -f $first.path,$first.sha256) } | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Upload comparevi-cli artifact | |
| if: ${{ hashFiles('dist/comparevi-cli/**') != '' }} | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: comparevi-cli-${{ runner.os }} | |
| path: dist/comparevi-cli/**/* | |
| - name: Setup Node with cache | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| - name: Install CLI validation deps | |
| run: node tools/npm/cli.mjs ci | |
| - name: Install markdownlint-cli (retry) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| for i in 1 2 3; do | |
| if node tools/npm/cli.mjs install -g markdownlint-cli; then | |
| break | |
| else | |
| node tools/npm/cli.mjs cache clean --force || true | |
| echo "retry $i" | |
| sleep 2 | |
| fi | |
| done | |
| - name: Run markdownlint (non-blocking) | |
| run: | | |
| markdownlint "**/*.md" --ignore node_modules | |
| continue-on-error: true | |
| preflight: | |
| runs-on: windows-latest | |
| timeout-minutes: 3 | |
| needs: [normalize] | |
| env: | |
| RESULTS_DIR: tests/results/preflight | |
| # Hosted Windows health gate before self-hosted jobs | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - 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: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Runner health (notice-only, hosted) | |
| if: ${{ vars.RUNNER_HEALTH != '0' }} | |
| shell: pwsh | |
| run: pwsh -File tools/Collect-RunnerHealth.ps1 -AppendSummary -EmitJson | |
| - name: Verify Windows runner and idle LabVIEW (surface LVCompare notice) | |
| shell: pwsh | |
| run: |- | |
| Write-Host "Runner: $([System.Environment]::OSVersion.VersionString)" | |
| Write-Host "Pwsh: $($PSVersionTable.PSVersion)" | |
| $cli = 'C:\Program Files\National Instruments\Shared\LabVIEW Compare\LVCompare.exe' | |
| if (-not (Test-Path -LiteralPath $cli)) { | |
| Write-Host "::notice::LVCompare.exe not found at canonical path: $cli (hosted preflight)" | |
| } else { Write-Host "LVCompare present: $cli" } | |
| $lv = Get-Process -Name 'LabVIEW' -ErrorAction SilentlyContinue | |
| if ($lv) { $pids = ($lv | ForEach-Object Id); $msg = "::error::LabVIEW.exe is running (PID(s): {0})" -f ([string]::Join(",", $pids)); Write-Host $msg; exit 1 } | |
| Write-Host 'Preflight OK: Windows runner healthy; LabVIEW not running.' | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $note = @('Note:', '- This preflight runs on hosted Windows (windows-latest); LVCompare presence is not required here.', '- Self-hosted Windows steps later in this workflow enforce LVCompare at the canonical path.') -join "`n" | |
| $note | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| # Hosted gate is informational only; enforce LVCompare on self-hosted jobs. | |
| probe: | |
| # Quick interactivity probe on self-hosted Windows to decide single vs fallback | |
| if: ${{ inputs.strategy == 'single' || vars.ORCH_STRATEGY == 'single' }} | |
| runs-on: [self-hosted, Windows, X64] | |
| timeout-minutes: 2 | |
| needs: [normalize, preflight] | |
| env: | |
| RESULTS_DIR: tests/results/probe | |
| outputs: | |
| ok: ${{ steps.out.outputs.ok }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - 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: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Runner health (notice-only, self-hosted) | |
| if: ${{ vars.RUNNER_HEALTH != '0' }} | |
| shell: pwsh | |
| run: pwsh -File tools/Collect-RunnerHealth.ps1 -ResultsDir '${{ env.RESULTS_DIR }}' -AppendSummary -EmitJson | |
| - name: Run interactivity probe | |
| id: out | |
| shell: pwsh | |
| run: | | |
| pwsh -File tools/Write-InteractivityProbe.ps1 | |
| $ui = [System.Environment]::UserInteractive | |
| $in = $false; try { $in = [Console]::IsInputRedirected } catch {} | |
| $ok = ($ui -and -not $in) | |
| "ok=$ok" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| pester-category: | |
| if: ${{ inputs.strategy == 'matrix' || vars.ORCH_STRATEGY == 'matrix' || (inputs.strategy == '' && vars.ORCH_STRATEGY == '') || (inputs.strategy == 'single' && needs.probe.outputs.ok == 'false') }} | |
| runs-on: [self-hosted, Windows, X64] | |
| timeout-minutes: 3 | |
| needs: [normalize, preflight, probe] | |
| strategy: | |
| matrix: | |
| category: [dispatcher, fixtures, schema, comparevi, loop, psummary, workflow] | |
| max-parallel: 1 | |
| env: | |
| RESULTS_ROOT: tests/results | |
| RESULTS_DIR: ${{ format('tests/results/{0}', matrix.category) }} | |
| DETECT_LEAKS: '1' | |
| CLEAN_AFTER: '0' | |
| SCAN_ARTIFACTS: '1' | |
| UNBLOCK_GUARD: '0' | |
| # Recommended LabVIEW environment toggles for headless invocations | |
| LV_SUPPRESS_UI: '1' | |
| LV_NO_ACTIVATE: '1' | |
| LV_CURSOR_RESTORE: '1' | |
| LV_IDLE_WAIT_SECONDS: '2' | |
| LV_IDLE_MAX_WAIT_SECONDS: '5' | |
| WATCH_CONSOLE: '1' | |
| LABVIEW_EXE: C:\\Program Files\\National Instruments\\LabVIEW 2025\\LabVIEW.exe | |
| EMIT_LV_CLOSURE_CRUMBS: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: tests/results/${{ matrix.category }} | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: tests/results/${{ matrix.category }} | |
| - name: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Append interactivity probe | |
| shell: pwsh | |
| run: pwsh -File tools/Write-InteractivityProbe.ps1 | |
| - name: Snapshot processes (pre-invoker) | |
| shell: pwsh | |
| run: | | |
| $procs = Get-Process | Where-Object { $_.Name -in 'pwsh','LVCompare','LabVIEW' } | Select-Object Name,Id,MainWindowTitle | |
| $lines = @('### Pre-Invoker Process Snapshot','') | |
| foreach ($p in $procs) { $lines += ('- {0} (PID {1}) {2}' -f $p.Name,$p.Id,$p.MainWindowTitle) } | |
| if ($env:GITHUB_STEP_SUMMARY) { $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'pre-invoker' | |
| - name: Wire Invoker (start) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-start | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Ensure Invoker (start) | |
| id: inv_start_matrix | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: start | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| category: ${{ matrix.category }} | |
| # Give the invoker more time and an extra retry while diagnosing READY | |
| timeout-seconds: '120' | |
| retries: '4' | |
| retry-delay-seconds: '7' | |
| - name: Debug invoker markers | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $dir = "${{ env.RESULTS_DIR }}/_invoker" | |
| if (Test-Path -LiteralPath $dir -PathType Container) { | |
| Write-Host "Invoker marker directory: $dir" | |
| try { Get-ChildItem -LiteralPath $dir | Format-Table -AutoSize | Out-String | Write-Host } catch {} | |
| $boot = Join-Path $dir 'boot.log' | |
| if (Test-Path -LiteralPath $boot -PathType Leaf) { | |
| Write-Host '--- boot.log ---' | |
| Get-Content -LiteralPath $boot -Raw | Write-Host | |
| } else { | |
| Write-Host 'boot.log not found.' | |
| } | |
| } else { | |
| Write-Host '::notice::Invoker marker directory missing.' | |
| } | |
| - name: LabVIEW warmup (preflight; leave LVCompare running) | |
| if: runner.os == 'Windows' | |
| env: | |
| WARMUP_SKIP_ARGS: '1' | |
| shell: pwsh | |
| run: pwsh -File tools/Warmup-LabVIEW.ps1 | |
| - name: Snapshot processes (post-invoker) | |
| shell: pwsh | |
| run: | | |
| $procs = Get-Process | Where-Object { $_.Name -in 'pwsh','LVCompare','LabVIEW' } | Select-Object Name,Id,MainWindowTitle | |
| $lines = @('### Post-Invoker Process Snapshot','') | |
| foreach ($p in $procs) { $lines += ('- {0} (PID {1}) {2}' -f $p.Name,$p.Id,$p.MainWindowTitle) } | |
| if ($env:GITHUB_STEP_SUMMARY) { $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'post-invoker' | |
| - name: Prepare fixture copies (base/head) | |
| if: ${{ steps.inv_start_matrix.outputs.ready == 'true' }} | |
| id: fixtures | |
| uses: ./.github/actions/prepare-fixtures | |
| - name: Export fixture env for tests | |
| shell: pwsh | |
| run: | | |
| "LV_BASE_VI=${{ steps.fixtures.outputs.base }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | |
| "LV_HEAD_VI=${{ steps.fixtures.outputs.head }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | |
| - name: Apply determinism profile | |
| uses: ./.github/actions/determinism-profile | |
| with: | |
| strict: 'true' | |
| - name: Apply dispatcher profile | |
| id: dprofile | |
| uses: ./.github/actions/dispatcher-profile | |
| with: | |
| emit-failures-json-always: 'true' | |
| detect-leaks: 'true' | |
| fail-on-leaks: 'false' | |
| kill-leaks: 'false' | |
| leak-grace-seconds: '3' | |
| clean-labview-before: 'false' | |
| clean-after: 'false' | |
| track-artifacts: 'true' | |
| - name: Wire Probe (T1) | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: T1 | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Run Compare CLI (category) | |
| if: ${{ steps.inv_start_matrix.outputs.ready == 'true' }} | |
| shell: pwsh | |
| run: | | |
| pwsh -File tools/Invoke-CompareCli.ps1 ` | |
| -Category '${{ matrix.category }}' ` | |
| -IntegrationMode "${{ needs.normalize.outputs.include_integration == 'true' && 'include' || 'exclude' }}" ` | |
| -ResultsRoot '${{ env.RESULTS_ROOT }}' | |
| - name: Summarize CLI output | |
| if: ${{ steps.inv_start_matrix.outputs.ready == 'true' }} | |
| shell: pwsh | |
| run: | | |
| $category = '${{ matrix.category }}' | |
| $cliPath = Join-Path '${{ env.RESULTS_DIR }}' 'cli-run.json' | |
| if (-not (Test-Path -LiteralPath $cliPath -PathType Leaf)) { | |
| "Compare CLI summary not found for category '$category'." | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| return | |
| } | |
| try { | |
| $cli = Get-Content -LiteralPath $cliPath -Raw | ConvertFrom-Json -ErrorAction Stop | |
| $lines = @( | |
| "### Compare CLI Status ($category)", | |
| "- Status: $($cli.status)", | |
| "- Exit Code: $($cli.exitCode)" | |
| ) | |
| if ($cli.summary) { | |
| $lines += ("- Tests: {0} Passed: {1} Failed: {2} Errors: {3}" -f $cli.summary.total,$cli.summary.passed,$cli.summary.failed,$cli.summary.errors) | |
| if ($cli.summary.duration_s) { $lines += ("- Duration (s): {0}" -f $cli.summary.duration_s) } | |
| } | |
| ($lines -join "`n") | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } catch { | |
| "Compare CLI summary parse failed: $_" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Ensure session index (fallback) | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Ensure-SessionIndex.ps1 -ResultsDir "${{ env.RESULTS_DIR }}" -SummaryJson 'pester-summary.json' | |
| - name: Wire Session Index (S1) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-session-index | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Session index post | |
| if: always() | |
| uses: ./.github/actions/session-index-post | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| validate-schema: true | |
| upload: true | |
| artifact-name: session-index-${{ matrix.category }} | |
| - name: Upload Pester artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: orchestrated-pester-results-${{ matrix.category }} | |
| path: | | |
| ${{ env.RESULTS_DIR }}/**/*.xml | |
| ${{ env.RESULTS_DIR }}/**/*.txt | |
| ${{ env.RESULTS_DIR }}/pester-summary.json | |
| ${{ env.RESULTS_DIR }}/pester-failures.json | |
| ${{ env.RESULTS_DIR }}/session-index.json | |
| - name: Upload invoker boot log (category) | |
| if: always() && hashFiles(format('{0}/_invoker/boot.log', env.RESULTS_DIR)) != '' | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: invoker-boot-${{ matrix.category }} | |
| path: ${{ env.RESULTS_DIR }}/_invoker/boot.log | |
| - name: Wire Guard (pre) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-guard-pre | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Runner Unblock Guard | |
| if: always() | |
| uses: ./.github/actions/runner-unblock-guard | |
| with: | |
| snapshot-path: ${{ env.RESULTS_DIR }}/runner-unblock-snapshot.json | |
| cleanup: ${{ env.UNBLOCK_GUARD == '1' }} | |
| process-names: LabVIEW,LVCompare | |
| - name: Wire Guard (post) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-guard-post | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Ensure Invoker (stop) | |
| if: always() | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: stop | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| category: ${{ matrix.category }} | |
| - name: Emit LV closure telemetry (category post-stop) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'post-stop' | |
| - name: Wire Invoker (stop) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-stop | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Append determinism summary | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Write-DeterminismSummary.ps1 | |
| - name: Append runner identity | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Write-RunnerIdentity.ps1 -SampleId '${{ github.event.inputs.sample_id || '' }}' | |
| - name: Append artifact map | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $paths = @('${{ env.RESULTS_DIR }}/pester-summary.json','${{ env.RESULTS_DIR }}/pester-results.xml','${{ env.RESULTS_DIR }}/pester-failures.json','${{ env.RESULTS_DIR }}/session-index.json') | |
| $pathsList = $paths -join ';' | |
| pwsh -File tools/Write-ArtifactMap.ps1 -PathsList $pathsList -Title 'Artifacts' | |
| - name: Append top failures | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Write-PesterTopFailures.ps1 -ResultsDir "${{ env.RESULTS_DIR }}" -Top 5 | |
| - name: Generate dev dashboard (matrix) | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Invoke-DevDashboard.ps1 -Group 'pester-selfhosted' -ResultsRoot "${{ env.RESULTS_DIR }}" -OutputRoot "${{ env.RESULTS_DIR }}/dev-dashboard" | |
| - name: Upload dev dashboard (matrix) | |
| if: always() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: dev-dashboard-${{ matrix.category }} | |
| path: | | |
| ${{ env.RESULTS_DIR }}/dev-dashboard/dashboard.html | |
| ${{ env.RESULTS_DIR }}/dev-dashboard/dashboard.json | |
| if-no-files-found: warn | |
| - name: Append dev dashboard summary (matrix) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $html = Join-Path "${{ env.RESULTS_DIR }}/dev-dashboard" 'dashboard.html' | |
| $json = Join-Path "${{ env.RESULTS_DIR }}/dev-dashboard" 'dashboard.json' | |
| if ($env:GITHUB_STEP_SUMMARY -and (Test-Path $html)) { | |
| $lines = @( | |
| "### Dev Dashboard (${{ matrix.category }})", | |
| '', | |
| "- HTML: $(Resolve-Path $html)", | |
| "- JSON: $(Resolve-Path $json)" | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Re-run hint | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Write-RerunHint.ps1 -Workflow 'ci-orchestrated.yml' -IncludeIntegration '${{ needs.normalize.outputs.include_integration }}' -SampleId '${{ github.event.inputs.sample_id || '' }}' | |
| drift: | |
| if: ${{ inputs.strategy == 'matrix' || vars.ORCH_STRATEGY == 'matrix' || (inputs.strategy == '' && vars.ORCH_STRATEGY == '') }} | |
| runs-on: [self-hosted, Windows, X64] | |
| timeout-minutes: 3 | |
| needs: [pester-category] | |
| env: | |
| RESULTS_DIR: results/fixture-drift | |
| INVOKER_REQUIRED: '1' | |
| UNBLOCK_GUARD: '0' | |
| LV_SUPPRESS_UI: '0' | |
| CLEAN_LV_BEFORE: ${{ vars.CLEAN_LV_BEFORE || 'true' }} | |
| CLEAN_LV_AFTER: ${{ vars.CLEAN_LV_AFTER || 'true' }} | |
| CLEAN_LV_INCLUDE_COMPARE: ${{ vars.CLEAN_LV_INCLUDE_COMPARE || vars.CLEAN_LVCOMPARE || 'true' }} | |
| LVCI_SINGLE_COMPARE: '1' | |
| WATCH_CONSOLE: '1' | |
| LABVIEW_EXE: C:\\Program Files\\National Instruments\\LabVIEW 2025\\LabVIEW.exe | |
| EMIT_LV_CLOSURE_CRUMBS: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Wire Probe (J1) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J1 | |
| results-dir: results/fixture-drift | |
| - name: Wire Probe (J2) | |
| if: '${{ vars.WIRE_PROBES != ''0'' }}' | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: J2 | |
| results-dir: results/fixture-drift | |
| - name: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: LabVIEW warmup (best-effort) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: pwsh -File tools/Warmup-LabVIEW.ps1 | |
| - name: Append interactivity probe | |
| shell: pwsh | |
| run: pwsh -File tools/Write-InteractivityProbe.ps1 | |
| - name: Wire Invoker (start) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-start | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Ensure Invoker (start) | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: start | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Wire Probe (C1) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: C1 | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Fixture Drift Orchestrator | |
| uses: ./.github/actions/fixture-drift | |
| with: | |
| render-report: 'true' | |
| comment-on-fail: 'true' | |
| upload-artifacts: 'true' | |
| artifact-name: orchestrated-fixture-drift | |
| - name: Wire Probe (C2) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: C2 | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Wire Session Index (S1) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-session-index | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Session index post (best-effort) | |
| if: always() | |
| uses: ./.github/actions/session-index-post | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| validate-schema: true | |
| upload: true | |
| artifact-name: fixture-drift-session-index | |
| - name: Generate dev dashboard (drift) | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Invoke-DevDashboard.ps1 -Group 'fixture-drift' -ResultsRoot '${{ env.RESULTS_DIR }}' -OutputRoot '${{ env.RESULTS_DIR }}/dev-dashboard' | |
| - name: Upload dev dashboard (drift) | |
| if: always() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: dev-dashboard-drift | |
| path: | | |
| ${{ env.RESULTS_DIR }}/dev-dashboard/dashboard.html | |
| ${{ env.RESULTS_DIR }}/dev-dashboard/dashboard.json | |
| if-no-files-found: warn | |
| - name: Append dev dashboard summary (drift) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $html = Join-Path '${{ env.RESULTS_DIR }}/dev-dashboard' 'dashboard.html' | |
| $json = Join-Path '${{ env.RESULTS_DIR }}/dev-dashboard' 'dashboard.json' | |
| if ($env:GITHUB_STEP_SUMMARY -and (Test-Path $html)) { | |
| $lines = @( | |
| '### Dev Dashboard (drift)', | |
| '', | |
| "- HTML: $(Resolve-Path $html)", | |
| "- JSON: $(Resolve-Path $json)" | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Parse Compare Exec JSON (drift) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| pwsh -File tools/Parse-CompareExec.ps1 -SearchDir '${{ env.RESULTS_DIR }}' -OutJson '${{ env.RESULTS_DIR }}/compare-outcome.json' | |
| - name: Upload compare outcome (drift) | |
| if: always() && hashFiles(format('{0}/compare-outcome.json', env.RESULTS_DIR)) != '' | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: compare-outcome-drift | |
| path: ${{ env.RESULTS_DIR }}/compare-outcome.json | |
| - name: Wire Guard (pre) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-guard-pre | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Runner Unblock Guard | |
| if: always() | |
| uses: ./.github/actions/runner-unblock-guard | |
| with: | |
| snapshot-path: ${{ env.RESULTS_DIR }}/runner-unblock-snapshot.json | |
| cleanup: ${{ env.UNBLOCK_GUARD == '1' }} | |
| process-names: LabVIEW,LVCompare | |
| - name: Wire Guard (post) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-guard-post | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Ensure Invoker (stop) | |
| if: always() | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: stop | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Emit LV closure telemetry (drift post-stop) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'post-stop' | |
| - name: Wire Invoker (stop) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-stop | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| publish: | |
| if: ${{ always() && (inputs.strategy == 'matrix' || vars.ORCH_STRATEGY == 'matrix' || (inputs.strategy == '' && vars.ORCH_STRATEGY == '')) && needs.pester-category.result != 'skipped' }} | |
| runs-on: ubuntu-latest | |
| needs: [lint, pester-category, drift] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - 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: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Summarize orchestrated run | |
| shell: pwsh | |
| run: | | |
| $lines = @( | |
| '### CI Orchestrated Summary', | |
| '- Lint: ${{ needs.lint.result }}', | |
| '- Pester: ${{ needs.pester-category.result }}', | |
| '- Drift: ${{ needs.drift.result }}' | |
| ) -join "`n" | |
| $lines | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| - name: Setup Node.js (derive env) | |
| if: always() | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Derive environment snapshot (publish) | |
| if: always() | |
| 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: Append run links (publish) | |
| if: always() | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| $summaryPath = $env:GITHUB_STEP_SUMMARY | |
| if (-not $summaryPath) { return } | |
| if (-not $env:GH_TOKEN) { | |
| Write-Warning '[run-links] GH_TOKEN missing; skipping run link summary.' | |
| return | |
| } | |
| $repo = $env:GITHUB_REPOSITORY | |
| if (-not $repo) { | |
| Write-Warning '[run-links] GITHUB_REPOSITORY missing; skipping run link summary.' | |
| return | |
| } | |
| $runId = '${{ github.run_id }}' | |
| $uri = "https://api.github.com/repos/$repo/actions/runs/$runId/jobs?per_page=100" | |
| $headers = @{ | |
| Authorization = "Bearer $env:GH_TOKEN" | |
| 'X-GitHub-Api-Version' = '2022-11-28' | |
| Accept = 'application/vnd.github+json' | |
| } | |
| try { | |
| $resp = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers -ErrorAction Stop | |
| } catch { | |
| Write-Warning ("[run-links] failed to fetch run jobs: {0}" -f $_.Exception.Message) | |
| return | |
| } | |
| $jobs = @($resp.jobs) | |
| if (-not $jobs -or $jobs.Count -eq 0) { return } | |
| $items = @() | |
| foreach ($job in $jobs) { | |
| if (-not $job.name -or -not $job.html_url) { continue } | |
| $items += ('- [{0}]({1})' -f $job.name, $job.html_url) | |
| } | |
| if ($items.Count -eq 0) { return } | |
| $block = @('### Run Links','') + $items | |
| $block -join "`n" | Out-File -FilePath $summaryPath -Append -Encoding utf8 | |
| - name: Wire Probe (P1) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: P1 | |
| results-dir: tests/results | |
| - name: Download Pester category artifacts (dispatcher) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-dispatcher | |
| path: cats/dispatcher | |
| - name: Download Pester category artifacts (fixtures) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-fixtures | |
| path: cats/fixtures | |
| - name: Download Pester category artifacts (schema) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-schema | |
| path: cats/schema | |
| - name: Download Pester category artifacts (comparevi) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-comparevi | |
| path: cats/comparevi | |
| - name: Download Pester category artifacts (loop) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-loop | |
| path: cats/loop | |
| - name: Download Pester category artifacts (psummary) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-psummary | |
| path: cats/psummary | |
| - name: Download Pester category artifacts (workflow) | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-pester-results-workflow | |
| path: cats/workflow | |
| - name: Summarize Pester categories | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $cats = @('dispatcher','fixtures','schema','comparevi','loop','psummary','workflow') | |
| & (Join-Path $PWD 'tools/Summarize-PesterCategories.ps1') -BaseDir 'cats' -Categories $cats | |
| - name: Download run provenance artifact | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: orchestrated-provenance | |
| path: prov | |
| - name: Validate run provenance (schema-lite) | |
| if: always() | |
| shell: pwsh | |
| continue-on-error: true | |
| run: | | |
| $json = Join-Path 'prov' 'provenance.json' | |
| if (Test-Path $json) { | |
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath $json -SchemaPath docs/schemas/run-provenance-v1.schema.json | |
| $status = if ($LASTEXITCODE -eq 0) { 'ok' } else { 'fail' } | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @('### Run Provenance Schema','',"- Status: $status","- File: $json") | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| } else { | |
| if ($env:GITHUB_STEP_SUMMARY) { "### Run Provenance Schema`n- File: (missing) $json" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| } | |
| - name: Validate session-index (categories) schema-lite | |
| if: always() | |
| shell: pwsh | |
| continue-on-error: true | |
| run: | | |
| $cats = @('dispatcher','fixtures','schema','comparevi','loop','psummary','workflow') | |
| $ok=0; $fail=0; $miss=0 | |
| foreach ($c in $cats) { | |
| $p1 = Join-Path (Join-Path 'cats' $c) 'tests/results/session-index.json' | |
| $p2 = Join-Path (Join-Path 'cats' $c) 'session-index.json' | |
| $json = if (Test-Path $p1) { $p1 } elseif (Test-Path $p2) { $p2 } else { $null } | |
| if (-not $json) { $miss++; continue } | |
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath $json -SchemaPath docs/schemas/session-index-v1.schema.json | |
| if ($LASTEXITCODE -eq 0) { $ok++ } else { $fail++ } | |
| } | |
| if ($env:GITHUB_STEP_SUMMARY) { | |
| $lines = @('### Session Index Schema (categories)','',"- ok: $ok","- fail: $fail","- missing: $miss") | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| # Single Windows job path (v2 folded in) | |
| - name: Re-run with same inputs | |
| if: ${{ always() }} | |
| shell: pwsh | |
| env: | |
| GH_STRATEGY: '${{ inputs.strategy }}' | |
| GH_INCLUDE: '${{ inputs.include_integration }}' | |
| GH_SAMPLE_ID: '${{ inputs.sample_id }}' | |
| run: |- | |
| $strategy = if ($env:GH_STRATEGY) { $env:GH_STRATEGY } else { 'matrix' } | |
| $include = if ($env:GH_INCLUDE) { $env:GH_INCLUDE } else { 'true' } | |
| $sid = if ($env:GH_SAMPLE_ID) { $env:GH_SAMPLE_ID } else { '<id>' } | |
| $cmd = "/run orchestrated strategy={0} include_integration={1} sample_id={2}" -f $strategy,$include,$sid | |
| $lines = @('### Re-run With Same Inputs','',"$ $cmd") | |
| if ($env:GITHUB_STEP_SUMMARY) { $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| - name: Append re-run hint (gh) | |
| if: ${{ always() }} | |
| shell: pwsh | |
| run: pwsh -File tools/Write-RerunHint.ps1 -Workflow 'ci-orchestrated.yml' -IncludeIntegration '${{ inputs.include_integration || 'true' }}' -SampleId '${{ inputs.sample_id || '' }}' | |
| windows-single: | |
| if: ${{ (inputs.strategy == 'single' || vars.ORCH_STRATEGY == 'single') && needs.probe.outputs.ok == 'true' }} | |
| runs-on: [self-hosted, Windows, X64] | |
| timeout-minutes: 10 | |
| needs: [normalize, preflight, probe] | |
| env: | |
| RESULTS_DIR: tests/results/single | |
| UNBLOCK_GUARD: '0' | |
| LV_SUPPRESS_UI: '0' | |
| CLEAN_LABVIEW: '0' | |
| CLEAN_AFTER: '0' | |
| DETECT_LEAKS: '1' | |
| FAIL_ON_LEAKS: '0' | |
| KILL_LEAKS: '0' | |
| WATCH_CONSOLE: '1' | |
| INVOKER_REQUIRED: '1' | |
| LABVIEW_EXE: C:\\Program Files\\National Instruments\\LabVIEW 2025\\LabVIEW.exe | |
| EMIT_LV_CLOSURE_CRUMBS: '1' | |
| TRACE_MATRIX: '1' | |
| TRACE_MATRIX_HTML: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - 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: Configure Git safe.directory (service) | |
| shell: pwsh | |
| run: git config --global --add safe.directory $env:GITHUB_WORKSPACE | |
| - name: Interactivity Probe (gate) | |
| id: ip | |
| shell: pwsh | |
| run: | | |
| pwsh -File tools/Write-InteractivityProbe.ps1 | |
| $ui = [System.Environment]::UserInteractive | |
| $in = $false; try { $in = [Console]::IsInputRedirected } catch {} | |
| $ok = ($ui -and -not $in) | |
| "ok=$ok" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| if (-not $ok -and $env:GITHUB_STEP_SUMMARY) { | |
| $lines = @('### Interactivity Probe','', '- Result: non-interactive (skipping single strategy steps)') | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Snapshot processes (pre-invoker) | |
| if: ${{ steps.ip.outputs.ok == 'true' }} | |
| shell: pwsh | |
| run: | | |
| $procs = Get-Process | Where-Object { $_.Name -in 'pwsh','LVCompare','LabVIEW' } | Select-Object Name,Id,MainWindowTitle | |
| $lines = @('### Pre-Invoker Process Snapshot','') | |
| foreach ($p in $procs) { $lines += ('- {0} (PID {1}) {2}' -f $p.Name,$p.Id,$p.MainWindowTitle) } | |
| if ($env:GITHUB_STEP_SUMMARY) { $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'pre-invoker' | |
| - name: Wire Invoker (start) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-start | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Ensure Invoker (start) | |
| if: ${{ steps.ip.outputs.ok == 'true' }} | |
| id: inv_start_single | |
| continue-on-error: true | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: start | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| timeout-seconds: '60' | |
| retries: '1' | |
| retry-delay-seconds: '0' | |
| - name: Warmup LabVIEW (preflight; leave LVCompare running) | |
| if: ${{ steps.ip.outputs.ok == 'true' }} | |
| env: | |
| WARMUP_SKIP_ARGS: '1' | |
| shell: pwsh | |
| run: pwsh -File tools/Warmup-LabVIEW.ps1 | |
| - name: Snapshot processes (post-invoker/warmup) | |
| if: ${{ steps.ip.outputs.ok == 'true' }} | |
| shell: pwsh | |
| run: | | |
| $procs = Get-Process | Where-Object { $_.Name -in 'pwsh','LVCompare','LabVIEW' } | Select-Object Name,Id,MainWindowTitle | |
| $lines = @('### Post-Invoker/Warmup Process Snapshot','') | |
| foreach ($p in $procs) { $lines += ('- {0} (PID {1}) {2}' -f $p.Name,$p.Id,$p.MainWindowTitle) } | |
| if ($env:GITHUB_STEP_SUMMARY) { $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'post-warmup' | |
| - name: Wire Probe (T1) | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: T1 | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Pester categories (serial, deterministic) | |
| if: ${{ steps.ip.outputs.ok == 'true' && steps.inv_start_single.outputs.ready == 'true' }} | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $cats = @( | |
| @{ name='dispatcher'; inc=@('Invoke-PesterTests*.ps1','PesterAvailability.Tests.ps1','NestedDispatcher*.Tests.ps1') }, | |
| @{ name='fixtures'; inc=@('Fixtures.*.ps1','FixtureValidation*.ps1','FixtureSummary*.ps1','ViBinaryHandling.Tests.ps1','FixtureValidationDiff.Tests.ps1') }, | |
| @{ name='schema'; inc=@('Schema.*.ps1','SchemaLite*.ps1') }, | |
| @{ name='comparevi'; inc=@('CompareVI*.ps1','CanonicalCli.Tests.ps1','Args.Tokenization.Tests.ps1') }, | |
| @{ name='loop'; inc=@('CompareLoop*.ps1','Run-AutonomousIntegrationLoop*.ps1','LoopMetrics.Tests.ps1','Integration-ControlLoop*.ps1','IntegrationControlLoop*.ps1') }, | |
| @{ name='psummary'; inc=@('PesterSummary*.ps1','Write-PesterSummaryToStepSummary*.ps1','AggregationHints*.ps1') }, | |
| @{ name='workflow'; inc=@('Workflow*.ps1','On-FixtureValidationFail.Tests.ps1','Watch.FlakyRecovery.Tests.ps1','FunctionShadowing*.ps1','FunctionProxy.Tests.ps1','RunSummary.Tool*.ps1','Action.CompositeOutputs.Tests.ps1','Binding.MinRepro.Tests.ps1','ArtifactTracking*.ps1','Guard.*.Tests.ps1') } | |
| ) | |
| foreach ($c in $cats) { | |
| $resDir = Join-Path '${{ env.RESULTS_DIR }}' $c.name | |
| $inc = @($c.inc) | |
| ./Invoke-PesterTests.ps1 -TestsPath tests -IntegrationMode "${{ needs.normalize.outputs.include_integration == 'true' && 'include' || 'exclude' }}" -ResultsPath $resDir -EmitFailuresJsonAlways -IncludePatterns $inc | |
| } | |
| - name: Generate traceability matrix (single) | |
| if: ${{ steps.ip.outputs.ok == 'true' && steps.inv_start_single.outputs.ready == 'true' }} | |
| shell: pwsh | |
| run: | | |
| $builder = Join-Path $PWD 'tools/Traceability-Matrix.ps1' | |
| if (Test-Path -LiteralPath $builder) { | |
| & $builder -TestsPath 'tests' -ResultsRoot '${{ env.RESULTS_DIR }}' -RenderHtml -RunId '${{ github.run_id }}' | |
| } else { | |
| Write-Warning "[TraceMatrix] Builder script not found at $builder" | |
| } | |
| - name: Drift (invoker; reuse LabVIEW via -lvpath) | |
| if: ${{ steps.ip.outputs.ok == 'true' && steps.inv_start_single.outputs.ready == 'true' }} | |
| uses: ./.github/actions/fixture-drift | |
| with: | |
| render-report: 'true' | |
| comment-on-fail: 'true' | |
| upload-artifacts: 'true' | |
| artifact-name: orchestrated-single-fixture-drift | |
| report-delay-ms: '150' | |
| - name: Ensure Invoker (stop) | |
| if: always() | |
| uses: ./.github/actions/ensure-invoker | |
| with: | |
| mode: stop | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Emit LV closure telemetry (single post-stop) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| & (Join-Path $PWD 'tools/Emit-LVClosureCrumb.ps1') -ResultsDir '${{ env.RESULTS_DIR }}' -Phase 'post-stop' | |
| - name: Wire Invoker (stop) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-invoker-stop | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Wire Session Index (S1) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-session-index | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Session index post (single) | |
| if: always() | |
| uses: ./.github/actions/session-index-post | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| validate-schema: true | |
| upload: true | |
| artifact-name: orchestrated-single-session-index | |
| - name: Runner Unblock Guard (single) | |
| if: always() | |
| uses: ./.github/actions/runner-unblock-guard | |
| with: | |
| snapshot-path: ${{ env.RESULTS_DIR }}/runner-unblock-snapshot.json | |
| cleanup: ${{ env.UNBLOCK_GUARD == '1' }} | |
| process-names: conhost,pwsh,LabVIEW,LVCompare | |
| - name: Upload invoker boot log (single) | |
| if: always() && hashFiles(format('{0}/_invoker/boot.log', env.RESULTS_DIR)) != '' | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: invoker-boot-single | |
| path: ${{ env.RESULTS_DIR }}/_invoker/boot.log | |
| - name: Append final summary (single) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $lines = @('### CI Orchestrated (single Windows) summary','') | |
| $lines += '- No cleanup; no UI suppression; LVCompare-only' | |
| $lines += ('- Include Integration: {0}' -f ('${{ needs.normalize.outputs.include_integration }}')) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| - name: Generate dev dashboard (single) | |
| if: always() | |
| shell: pwsh | |
| run: pwsh -File tools/Invoke-DevDashboard.ps1 -Group 'pester-selfhosted' -ResultsRoot '${{ env.RESULTS_DIR }}' -OutputRoot '${{ env.RESULTS_DIR }}/dev-dashboard' | |
| - name: Upload dev dashboard (single) | |
| if: always() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: dev-dashboard-single | |
| path: | | |
| ${{ env.RESULTS_DIR }}/dev-dashboard/dashboard.html | |
| ${{ env.RESULTS_DIR }}/dev-dashboard/dashboard.json | |
| if-no-files-found: warn | |
| - name: Upload traceability matrix (single) | |
| if: always() && hashFiles('tests/results/_trace/trace-matrix.json') != '' | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: traceability-matrix-single | |
| path: | | |
| ${{ env.RESULTS_DIR }}/_trace/trace-matrix.json | |
| ${{ env.RESULTS_DIR }}/_trace/trace-matrix.html | |
| if-no-files-found: warn | |
| - name: Append dev dashboard summary (single) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $html = Join-Path '${{ env.RESULTS_DIR }}/dev-dashboard' 'dashboard.html' | |
| $json = Join-Path '${{ env.RESULTS_DIR }}/dev-dashboard' 'dashboard.json' | |
| if ($env:GITHUB_STEP_SUMMARY -and (Test-Path $html)) { | |
| $lines = @( | |
| '### Dev Dashboard (single)', | |
| '', | |
| "- HTML: $(Resolve-Path $html)", | |
| "- JSON: $(Resolve-Path $json)" | |
| ) | |
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | |
| } | |
| - name: Wire Probe (P1) | |
| if: ${{ vars.WIRE_PROBES != '0' }} | |
| uses: ./.github/actions/wire-probe | |
| with: | |
| phase: P1 | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| - name: Agent wait summary (single) | |
| if: always() | |
| uses: ./.github/actions/agent-wait-post | |
| with: | |
| results-dir: ${{ env.RESULTS_DIR }} | |
| fail-on-outside: 'false' | |
| upload-artifact: 'false' | |
| - name: Send Ctrl+C to lingering consoles (optional) | |
| if: always() && env.SEND_CTRL_C == '1' | |
| shell: pwsh | |
| run: pwsh -File tools/Send-CtrlC.ps1 -Names 'pwsh','conhost' -Max 5 | |
| - name: Re-run with same inputs (single) | |
| if: ${{ always() }} | |
| shell: pwsh | |
| env: | |
| GH_STRATEGY: '${{ inputs.strategy }}' | |
| GH_INCLUDE: '${{ inputs.include_integration }}' | |
| GH_SAMPLE_ID: '${{ inputs.sample_id }}' | |
| run: |- | |
| $strategy = if ($env:GH_STRATEGY) { $env:GH_STRATEGY } else { 'single' } | |
| $include = if ($env:GH_INCLUDE) { $env:GH_INCLUDE } else { 'true' } | |
| $sid = if ($env:GH_SAMPLE_ID) { $env:GH_SAMPLE_ID } else { '<id>' } | |
| $cmd = "/run orchestrated strategy={0} include_integration={1} sample_id={2}" -f $strategy,$include,$sid | |
| $lines = @('### Re-run With Same Inputs','',"$ $cmd") | |
| if ($env:GITHUB_STEP_SUMMARY) { $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } | |
| - name: Append re-run hint (gh; single) | |
| if: ${{ always() }} | |
| shell: pwsh | |
| run: pwsh -File tools/Write-RerunHint.ps1 -Workflow 'ci-orchestrated.yml' -IncludeIntegration '${{ inputs.include_integration || 'true' }}' -SampleId '${{ inputs.sample_id || '' }}' |