|
| 1 | +# GitHub Actions CI — pilot port of azure-pipelines.yml. |
| 2 | +# |
| 3 | +# This runs ALONGSIDE Azure Pipelines (we did not remove azure-pipelines.yml), |
| 4 | +# so every push/PR triggers both systems and we can compare them before cutting |
| 5 | +# over. Mirrors the Azure setup: build once on Windows, publish the built module |
| 6 | +# as an artifact, then fan out the test matrix (all 8 legs run in parallel). |
| 7 | +# |
| 8 | +# Coverage: each leg runs `test.ps1 -CC`, producing a JaCoCo coverage.xml. Azure |
| 9 | +# rendered this via PublishCodeCoverageResults@2; here we surface it natively in |
| 10 | +# the run's job summaries (per leg, plus a consolidated table in "Done") and keep |
| 11 | +# the raw coverage.xml as an artifact. |
| 12 | +# |
| 13 | +# Image policy: GA only. We float `*-latest` for the newest GA image and pin the |
| 14 | +# one previous GA image ("latest + reasonably recent"). No preview images |
| 15 | +# (macos-26, windows-2025-vs2026) are used. Note macos-14 is on the deprecation |
| 16 | +# clock (brownouts Oct 2026, removal Nov 2 2026) — swap it out before then. |
| 17 | +name: CI (GitHub Actions pilot) |
| 18 | + |
| 19 | +on: |
| 20 | + push: |
| 21 | + branches: [main, 'rel/*'] |
| 22 | + paths-ignore: |
| 23 | + - '.devcontainer/**' |
| 24 | + - '.vscode/**' |
| 25 | + - 'docs/**' |
| 26 | + - 'images/**' |
| 27 | + - '**/*.md' |
| 28 | + pull_request: |
| 29 | + branches: [main, 'rel/*', 'dev/*'] |
| 30 | + paths-ignore: |
| 31 | + - '.devcontainer/**' |
| 32 | + - '.vscode/**' |
| 33 | + - 'docs/**' |
| 34 | + - 'images/**' |
| 35 | + - '**/*.md' |
| 36 | + workflow_dispatch: |
| 37 | + |
| 38 | +permissions: |
| 39 | + contents: read |
| 40 | + |
| 41 | +env: |
| 42 | + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '1' |
| 43 | + DOTNET_CLI_TELEMETRY_OPTOUT: '1' |
| 44 | + DOTNET_GENERATE_ASPNET_CERTIFICATE: '0' |
| 45 | + DOTNET_NOLOGO: '1' |
| 46 | + CI: '1' |
| 47 | + |
| 48 | +# Supersede in-flight runs for the same branch/PR to save minutes. |
| 49 | +concurrency: |
| 50 | + group: ci-${{ github.workflow }}-${{ github.ref }} |
| 51 | + cancel-in-progress: true |
| 52 | + |
| 53 | +jobs: |
| 54 | + build: |
| 55 | + name: Build |
| 56 | + runs-on: windows-latest |
| 57 | + timeout-minutes: 10 |
| 58 | + steps: |
| 59 | + - uses: actions/checkout@v6 |
| 60 | + with: |
| 61 | + fetch-depth: 1 |
| 62 | + |
| 63 | + - name: Setup .NET (global.json) |
| 64 | + uses: actions/setup-dotnet@v4 |
| 65 | + with: |
| 66 | + global-json-file: global.json |
| 67 | + cache: true |
| 68 | + cache-dependency-path: '**/packages.lock.json' |
| 69 | + |
| 70 | + - name: Build module (inline, locked restore) |
| 71 | + shell: pwsh |
| 72 | + run: | |
| 73 | + "Running .NET SDK v$(dotnet --version)" |
| 74 | + ./build.ps1 -LockedRestore -Clean -Inline |
| 75 | +
|
| 76 | + # Publish the built tree so every test leg runs the exact same build. |
| 77 | + # Mirrors Azure's `publish: $(Build.SourcesDirectory)` + .artifactignore. |
| 78 | + - name: Upload build output |
| 79 | + uses: actions/upload-artifact@v4 |
| 80 | + with: |
| 81 | + name: pester-build |
| 82 | + retention-days: 1 |
| 83 | + include-hidden-files: true |
| 84 | + path: | |
| 85 | + bin/ |
| 86 | + src/ |
| 87 | + tst/ |
| 88 | + build.ps1 |
| 89 | + test.ps1 |
| 90 | + global.json |
| 91 | + !src/csharp/**/bin/** |
| 92 | + !src/csharp/**/obj/** |
| 93 | + !src/csharp/.vs/** |
| 94 | +
|
| 95 | + test: |
| 96 | + name: ${{ matrix.name }} |
| 97 | + needs: build |
| 98 | + runs-on: ${{ matrix.os }} |
| 99 | + timeout-minutes: 20 |
| 100 | + strategy: |
| 101 | + fail-fast: false |
| 102 | + matrix: |
| 103 | + include: |
| 104 | + # PowerShell 7 (psexe: pwsh) — latest + one previous GA per OS. |
| 105 | + - { name: 'PS7 - Ubuntu latest', id: ps7-ubuntu-latest, os: ubuntu-latest, psexe: pwsh } |
| 106 | + - { name: 'PS7 - Ubuntu 22.04', id: ps7-ubuntu-2204, os: ubuntu-22.04, psexe: pwsh } |
| 107 | + - { name: 'PS7 - macOS latest', id: ps7-macos-latest, os: macos-latest, psexe: pwsh } |
| 108 | + - { name: 'PS7 - macOS 14', id: ps7-macos-14, os: macos-14, psexe: pwsh } |
| 109 | + - { name: 'PS7 - Windows latest', id: ps7-windows-latest, os: windows-latest, psexe: pwsh } |
| 110 | + - { name: 'PS7 - Windows 2022', id: ps7-windows-2022, os: windows-2022, psexe: pwsh } |
| 111 | + # Windows PowerShell 5.1 (psexe: powershell) — Windows only. |
| 112 | + - { name: 'PS5.1 - Windows latest', id: ps51-windows-latest, os: windows-latest, psexe: powershell } |
| 113 | + - { name: 'PS5.1 - Windows 2022', id: ps51-windows-2022, os: windows-2022, psexe: powershell } |
| 114 | + steps: |
| 115 | + # No checkout — we test the published build, exactly like Azure (checkout: none). |
| 116 | + - name: Download build output |
| 117 | + uses: actions/download-artifact@v4 |
| 118 | + with: |
| 119 | + name: pester-build |
| 120 | + path: . |
| 121 | + |
| 122 | + # `shell:` cannot take a matrix expression, so run from a fixed pwsh (present |
| 123 | + # on every runner) and launch the leg's interpreter: pwsh for PS7, powershell |
| 124 | + # (Windows PowerShell 5.1) for the 5.1 legs. exit $LASTEXITCODE so a failure |
| 125 | + # in the launched process still fails the job. |
| 126 | + - name: Test Pester |
| 127 | + shell: pwsh |
| 128 | + run: | |
| 129 | + & ${{ matrix.psexe }} -NoProfile -Command "& ./test.ps1 -CI -CC -PassThru -NoBuild" |
| 130 | + exit $LASTEXITCODE |
| 131 | +
|
| 132 | + # Render this leg's JaCoCo coverage into the job summary (Azure parity). |
| 133 | + - name: Coverage summary |
| 134 | + if: always() |
| 135 | + shell: pwsh |
| 136 | + env: |
| 137 | + LEG_NAME: ${{ matrix.name }} |
| 138 | + run: | |
| 139 | + $ErrorActionPreference = 'Continue' |
| 140 | + function Append-Summary([string]$text) { |
| 141 | + [System.IO.File]::AppendAllText($env:GITHUB_STEP_SUMMARY, $text, (New-Object System.Text.UTF8Encoding($false))) |
| 142 | + } |
| 143 | + try { |
| 144 | + if (-not (Test-Path 'coverage.xml')) { |
| 145 | + Append-Summary "## Coverage - $env:LEG_NAME`n`nNo coverage.xml was produced.`n" |
| 146 | + return |
| 147 | + } |
| 148 | + [xml]$xml = Get-Content -LiteralPath 'coverage.xml' |
| 149 | + $counters = @($xml.report.counter) # report-level totals (direct children) |
| 150 | + $rows = foreach ($type in 'LINE','INSTRUCTION','METHOD','CLASS') { |
| 151 | + $c = $counters | Where-Object { $_.type -eq $type } |
| 152 | + if ($c) { |
| 153 | + $cov = [int]$c.covered; $mis = [int]$c.missed; $tot = $cov + $mis |
| 154 | + $pct = if ($tot) { [math]::Round(100.0 * $cov / $tot, 2) } else { 0 } |
| 155 | + $pctStr = ([double]$pct).ToString('0.##', [System.Globalization.CultureInfo]::InvariantCulture) |
| 156 | + [pscustomobject]@{ Type = $type; Covered = $cov; Missed = $mis; Total = $tot; PctStr = $pctStr } |
| 157 | + } |
| 158 | + } |
| 159 | + $line = $rows | Where-Object { $_.Type -eq 'LINE' } |
| 160 | + $sb = [System.Text.StringBuilder]::new() |
| 161 | + [void]$sb.AppendLine("## Coverage - $env:LEG_NAME`n") |
| 162 | + if ($line) { [void]$sb.AppendLine("**Line coverage: $($line.PctStr)%** ($($line.Covered)/$($line.Total) lines)`n") } |
| 163 | + [void]$sb.AppendLine("| Metric | Covered | Missed | Total | % |") |
| 164 | + [void]$sb.AppendLine("|---|--:|--:|--:|--:|") |
| 165 | + foreach ($r in $rows) { [void]$sb.AppendLine("| $($r.Type) | $($r.Covered) | $($r.Missed) | $($r.Total) | $($r.PctStr)% |") } |
| 166 | + Append-Summary ($sb.ToString() + "`n") |
| 167 | + if ($line) { Write-Host "Line coverage ($env:LEG_NAME): $($line.PctStr)%" } |
| 168 | + } catch { |
| 169 | + Append-Summary "## Coverage - $env:LEG_NAME`n`nCoverage summary failed: $($_.Exception.Message)`n" |
| 170 | + } |
| 171 | +
|
| 172 | + # Upload this leg's JaCoCo report to Codecov. `flags` keeps a per-leg |
| 173 | + # breakdown while Codecov merges all legs into one project report. |
| 174 | + # pester/Pester is public, so upload works tokenless (v5 opt-out); a |
| 175 | + # CODECOV_TOKEN secret, if set, makes uploads more reliable. |
| 176 | + - name: Upload coverage to Codecov |
| 177 | + if: always() |
| 178 | + uses: codecov/codecov-action@v5 |
| 179 | + with: |
| 180 | + files: ./coverage.xml |
| 181 | + disable_search: true |
| 182 | + flags: ${{ matrix.id }} |
| 183 | + name: ${{ matrix.name }} |
| 184 | + token: ${{ secrets.CODECOV_TOKEN }} |
| 185 | + fail_ci_if_error: false |
| 186 | + |
| 187 | + - name: Upload test results |
| 188 | + if: always() |
| 189 | + uses: actions/upload-artifact@v4 |
| 190 | + with: |
| 191 | + name: results-${{ matrix.id }} |
| 192 | + retention-days: 7 |
| 193 | + if-no-files-found: ignore |
| 194 | + path: | |
| 195 | + testResults.xml |
| 196 | + coverage.xml |
| 197 | +
|
| 198 | + # Single gate after the matrix so branch protection / auto-merge only needs to |
| 199 | + # require "Done" instead of listing all 8 legs. Mirrors Azure's Done stage. |
| 200 | + # It also renders a consolidated coverage table (one glance at all legs). |
| 201 | + done: |
| 202 | + name: Done |
| 203 | + needs: test |
| 204 | + if: always() |
| 205 | + runs-on: ubuntu-latest |
| 206 | + steps: |
| 207 | + - name: Download coverage reports |
| 208 | + if: always() |
| 209 | + uses: actions/download-artifact@v4 |
| 210 | + with: |
| 211 | + pattern: results-* |
| 212 | + path: coverage-reports |
| 213 | + |
| 214 | + - name: Consolidated coverage |
| 215 | + if: always() |
| 216 | + shell: pwsh |
| 217 | + run: | |
| 218 | + $ErrorActionPreference = 'Continue' |
| 219 | + function Append-Summary([string]$text) { |
| 220 | + [System.IO.File]::AppendAllText($env:GITHUB_STEP_SUMMARY, $text, (New-Object System.Text.UTF8Encoding($false))) |
| 221 | + } |
| 222 | + function Get-Pct($counters, [string]$type) { |
| 223 | + $c = $counters | Where-Object { $_.type -eq $type } |
| 224 | + if (-not $c) { return $null } |
| 225 | + $cov = [int]$c.covered; $tot = $cov + [int]$c.missed |
| 226 | + $pct = if ($tot) { [math]::Round(100.0 * $cov / $tot, 2) } else { 0 } |
| 227 | + ([double]$pct).ToString('0.##', [System.Globalization.CultureInfo]::InvariantCulture) |
| 228 | + } |
| 229 | + try { |
| 230 | + $files = Get-ChildItem -Path 'coverage-reports' -Recurse -Filter 'coverage.xml' -ErrorAction SilentlyContinue |
| 231 | + if (-not $files) { Append-Summary "## Coverage by matrix leg`n`nNo coverage reports found.`n"; return } |
| 232 | + $sb = [System.Text.StringBuilder]::new() |
| 233 | + [void]$sb.AppendLine("## Coverage by matrix leg`n") |
| 234 | + [void]$sb.AppendLine("| Leg | Line % | Instruction % |") |
| 235 | + [void]$sb.AppendLine("|---|--:|--:|") |
| 236 | + foreach ($f in ($files | Sort-Object FullName)) { |
| 237 | + $leg = (Split-Path (Split-Path $f.FullName -Parent) -Leaf) -replace '^results-', '' |
| 238 | + try { |
| 239 | + [xml]$xml = Get-Content -LiteralPath $f.FullName |
| 240 | + $counters = @($xml.report.counter) |
| 241 | + $lpct = Get-Pct $counters 'LINE' |
| 242 | + $ipct = Get-Pct $counters 'INSTRUCTION' |
| 243 | + [void]$sb.AppendLine("| $leg | $(if ($null -ne $lpct) { "$lpct%" } else { 'n/a' }) | $(if ($null -ne $ipct) { "$ipct%" } else { 'n/a' }) |") |
| 244 | + } catch { |
| 245 | + [void]$sb.AppendLine("| $leg | error | error |") |
| 246 | + } |
| 247 | + } |
| 248 | + Append-Summary ($sb.ToString() + "`n") |
| 249 | + } catch { |
| 250 | + Append-Summary "## Coverage by matrix leg`n`nConsolidated coverage failed: $($_.Exception.Message)`n" |
| 251 | + } |
| 252 | +
|
| 253 | + - name: All test legs passed |
| 254 | + run: | |
| 255 | + echo "Test matrix result: ${{ needs.test.result }}" |
| 256 | + [ "${{ needs.test.result }}" = "success" ] || exit 1 |
| 257 | + echo "done" |
0 commit comments