Skip to content

Commit 2177e4b

Browse files
nohwndclaude
andcommitted
Add GitHub Actions CI pilot (with Codecov) alongside Azure Pipelines
Runs in parallel with azure-pipelines.yml so we can compare the two before cutting over. Build once on Windows, then the same 8-leg matrix as Azure: PowerShell 7 on ubuntu/macOS/windows (latest + one previous GA), Windows PowerShell 5.1 on windows (latest + previous). Coverage renders in each run's job summary and a consolidated table, and uploads to Codecov with a flag per leg. GA-only images: floating *-latest plus one pinned previous GA. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3832c29 commit 2177e4b

2 files changed

Lines changed: 287 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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"

codecov.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Codecov configuration — added alongside the GitHub Actions CI pilot
2+
# (.github/workflows/ci.yml). Coverage is uploaded per matrix leg using flags.
3+
#
4+
# During the pilot we keep coverage status "informational" so Codecov never
5+
# posts a failing/blocking commit status while we compare against Azure.
6+
7+
codecov:
8+
# We upload one report per matrix leg (8 legs). Wait for all of them before
9+
# finalizing notifications/PR comment so the merged picture is complete.
10+
notify:
11+
after_n_builds: 8
12+
13+
coverage:
14+
status:
15+
project:
16+
default:
17+
informational: true
18+
patch:
19+
default:
20+
informational: true
21+
22+
comment:
23+
layout: "header, diff, flags, files"
24+
require_changes: false
25+
26+
# Per-leg flags (ps7-ubuntu-latest, ps51-windows-2022, ...) uploaded by ci.yml.
27+
# Carry a flag's last-known coverage forward on commits where it didn't upload.
28+
flag_management:
29+
default_rules:
30+
carryforward: true

0 commit comments

Comments
 (0)