From 2177e4b1e832b1df2c12e90ee41b4ada4c8f5e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sat, 18 Jul 2026 11:46:12 +0200 Subject: [PATCH 1/6] 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 --- .github/workflows/ci.yml | 257 +++++++++++++++++++++++++++++++++++++++ codecov.yml | 30 +++++ 2 files changed, 287 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 codecov.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..6e8e9c67b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,257 @@ +# GitHub Actions CI — pilot port of azure-pipelines.yml. +# +# This runs ALONGSIDE Azure Pipelines (we did not remove azure-pipelines.yml), +# so every push/PR triggers both systems and we can compare them before cutting +# over. Mirrors the Azure setup: build once on Windows, publish the built module +# as an artifact, then fan out the test matrix (all 8 legs run in parallel). +# +# Coverage: each leg runs `test.ps1 -CC`, producing a JaCoCo coverage.xml. Azure +# rendered this via PublishCodeCoverageResults@2; here we surface it natively in +# the run's job summaries (per leg, plus a consolidated table in "Done") and keep +# the raw coverage.xml as an artifact. +# +# Image policy: GA only. We float `*-latest` for the newest GA image and pin the +# one previous GA image ("latest + reasonably recent"). No preview images +# (macos-26, windows-2025-vs2026) are used. Note macos-14 is on the deprecation +# clock (brownouts Oct 2026, removal Nov 2 2026) — swap it out before then. +name: CI (GitHub Actions pilot) + +on: + push: + branches: [main, 'rel/*'] + paths-ignore: + - '.devcontainer/**' + - '.vscode/**' + - 'docs/**' + - 'images/**' + - '**/*.md' + pull_request: + branches: [main, 'rel/*', 'dev/*'] + paths-ignore: + - '.devcontainer/**' + - '.vscode/**' + - 'docs/**' + - 'images/**' + - '**/*.md' + workflow_dispatch: + +permissions: + contents: read + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '1' + DOTNET_CLI_TELEMETRY_OPTOUT: '1' + DOTNET_GENERATE_ASPNET_CERTIFICATE: '0' + DOTNET_NOLOGO: '1' + CI: '1' + +# Supersede in-flight runs for the same branch/PR to save minutes. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: windows-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Setup .NET (global.json) + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + cache: true + cache-dependency-path: '**/packages.lock.json' + + - name: Build module (inline, locked restore) + shell: pwsh + run: | + "Running .NET SDK v$(dotnet --version)" + ./build.ps1 -LockedRestore -Clean -Inline + + # Publish the built tree so every test leg runs the exact same build. + # Mirrors Azure's `publish: $(Build.SourcesDirectory)` + .artifactignore. + - name: Upload build output + uses: actions/upload-artifact@v4 + with: + name: pester-build + retention-days: 1 + include-hidden-files: true + path: | + bin/ + src/ + tst/ + build.ps1 + test.ps1 + global.json + !src/csharp/**/bin/** + !src/csharp/**/obj/** + !src/csharp/.vs/** + + test: + name: ${{ matrix.name }} + needs: build + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + # PowerShell 7 (psexe: pwsh) — latest + one previous GA per OS. + - { name: 'PS7 - Ubuntu latest', id: ps7-ubuntu-latest, os: ubuntu-latest, psexe: pwsh } + - { name: 'PS7 - Ubuntu 22.04', id: ps7-ubuntu-2204, os: ubuntu-22.04, psexe: pwsh } + - { name: 'PS7 - macOS latest', id: ps7-macos-latest, os: macos-latest, psexe: pwsh } + - { name: 'PS7 - macOS 14', id: ps7-macos-14, os: macos-14, psexe: pwsh } + - { name: 'PS7 - Windows latest', id: ps7-windows-latest, os: windows-latest, psexe: pwsh } + - { name: 'PS7 - Windows 2022', id: ps7-windows-2022, os: windows-2022, psexe: pwsh } + # Windows PowerShell 5.1 (psexe: powershell) — Windows only. + - { name: 'PS5.1 - Windows latest', id: ps51-windows-latest, os: windows-latest, psexe: powershell } + - { name: 'PS5.1 - Windows 2022', id: ps51-windows-2022, os: windows-2022, psexe: powershell } + steps: + # No checkout — we test the published build, exactly like Azure (checkout: none). + - name: Download build output + uses: actions/download-artifact@v4 + with: + name: pester-build + path: . + + # `shell:` cannot take a matrix expression, so run from a fixed pwsh (present + # on every runner) and launch the leg's interpreter: pwsh for PS7, powershell + # (Windows PowerShell 5.1) for the 5.1 legs. exit $LASTEXITCODE so a failure + # in the launched process still fails the job. + - name: Test Pester + shell: pwsh + run: | + & ${{ matrix.psexe }} -NoProfile -Command "& ./test.ps1 -CI -CC -PassThru -NoBuild" + exit $LASTEXITCODE + + # Render this leg's JaCoCo coverage into the job summary (Azure parity). + - name: Coverage summary + if: always() + shell: pwsh + env: + LEG_NAME: ${{ matrix.name }} + run: | + $ErrorActionPreference = 'Continue' + function Append-Summary([string]$text) { + [System.IO.File]::AppendAllText($env:GITHUB_STEP_SUMMARY, $text, (New-Object System.Text.UTF8Encoding($false))) + } + try { + if (-not (Test-Path 'coverage.xml')) { + Append-Summary "## Coverage - $env:LEG_NAME`n`nNo coverage.xml was produced.`n" + return + } + [xml]$xml = Get-Content -LiteralPath 'coverage.xml' + $counters = @($xml.report.counter) # report-level totals (direct children) + $rows = foreach ($type in 'LINE','INSTRUCTION','METHOD','CLASS') { + $c = $counters | Where-Object { $_.type -eq $type } + if ($c) { + $cov = [int]$c.covered; $mis = [int]$c.missed; $tot = $cov + $mis + $pct = if ($tot) { [math]::Round(100.0 * $cov / $tot, 2) } else { 0 } + $pctStr = ([double]$pct).ToString('0.##', [System.Globalization.CultureInfo]::InvariantCulture) + [pscustomobject]@{ Type = $type; Covered = $cov; Missed = $mis; Total = $tot; PctStr = $pctStr } + } + } + $line = $rows | Where-Object { $_.Type -eq 'LINE' } + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("## Coverage - $env:LEG_NAME`n") + if ($line) { [void]$sb.AppendLine("**Line coverage: $($line.PctStr)%** ($($line.Covered)/$($line.Total) lines)`n") } + [void]$sb.AppendLine("| Metric | Covered | Missed | Total | % |") + [void]$sb.AppendLine("|---|--:|--:|--:|--:|") + foreach ($r in $rows) { [void]$sb.AppendLine("| $($r.Type) | $($r.Covered) | $($r.Missed) | $($r.Total) | $($r.PctStr)% |") } + Append-Summary ($sb.ToString() + "`n") + if ($line) { Write-Host "Line coverage ($env:LEG_NAME): $($line.PctStr)%" } + } catch { + Append-Summary "## Coverage - $env:LEG_NAME`n`nCoverage summary failed: $($_.Exception.Message)`n" + } + + # Upload this leg's JaCoCo report to Codecov. `flags` keeps a per-leg + # breakdown while Codecov merges all legs into one project report. + # pester/Pester is public, so upload works tokenless (v5 opt-out); a + # CODECOV_TOKEN secret, if set, makes uploads more reliable. + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + disable_search: true + flags: ${{ matrix.id }} + name: ${{ matrix.name }} + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: results-${{ matrix.id }} + retention-days: 7 + if-no-files-found: ignore + path: | + testResults.xml + coverage.xml + + # Single gate after the matrix so branch protection / auto-merge only needs to + # require "Done" instead of listing all 8 legs. Mirrors Azure's Done stage. + # It also renders a consolidated coverage table (one glance at all legs). + done: + name: Done + needs: test + if: always() + runs-on: ubuntu-latest + steps: + - name: Download coverage reports + if: always() + uses: actions/download-artifact@v4 + with: + pattern: results-* + path: coverage-reports + + - name: Consolidated coverage + if: always() + shell: pwsh + run: | + $ErrorActionPreference = 'Continue' + function Append-Summary([string]$text) { + [System.IO.File]::AppendAllText($env:GITHUB_STEP_SUMMARY, $text, (New-Object System.Text.UTF8Encoding($false))) + } + function Get-Pct($counters, [string]$type) { + $c = $counters | Where-Object { $_.type -eq $type } + if (-not $c) { return $null } + $cov = [int]$c.covered; $tot = $cov + [int]$c.missed + $pct = if ($tot) { [math]::Round(100.0 * $cov / $tot, 2) } else { 0 } + ([double]$pct).ToString('0.##', [System.Globalization.CultureInfo]::InvariantCulture) + } + try { + $files = Get-ChildItem -Path 'coverage-reports' -Recurse -Filter 'coverage.xml' -ErrorAction SilentlyContinue + if (-not $files) { Append-Summary "## Coverage by matrix leg`n`nNo coverage reports found.`n"; return } + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("## Coverage by matrix leg`n") + [void]$sb.AppendLine("| Leg | Line % | Instruction % |") + [void]$sb.AppendLine("|---|--:|--:|") + foreach ($f in ($files | Sort-Object FullName)) { + $leg = (Split-Path (Split-Path $f.FullName -Parent) -Leaf) -replace '^results-', '' + try { + [xml]$xml = Get-Content -LiteralPath $f.FullName + $counters = @($xml.report.counter) + $lpct = Get-Pct $counters 'LINE' + $ipct = Get-Pct $counters 'INSTRUCTION' + [void]$sb.AppendLine("| $leg | $(if ($null -ne $lpct) { "$lpct%" } else { 'n/a' }) | $(if ($null -ne $ipct) { "$ipct%" } else { 'n/a' }) |") + } catch { + [void]$sb.AppendLine("| $leg | error | error |") + } + } + Append-Summary ($sb.ToString() + "`n") + } catch { + Append-Summary "## Coverage by matrix leg`n`nConsolidated coverage failed: $($_.Exception.Message)`n" + } + + - name: All test legs passed + run: | + echo "Test matrix result: ${{ needs.test.result }}" + [ "${{ needs.test.result }}" = "success" ] || exit 1 + echo "done" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..9fc23f205 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,30 @@ +# Codecov configuration — added alongside the GitHub Actions CI pilot +# (.github/workflows/ci.yml). Coverage is uploaded per matrix leg using flags. +# +# During the pilot we keep coverage status "informational" so Codecov never +# posts a failing/blocking commit status while we compare against Azure. + +codecov: + # We upload one report per matrix leg (8 legs). Wait for all of them before + # finalizing notifications/PR comment so the merged picture is complete. + notify: + after_n_builds: 8 + +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true + +comment: + layout: "header, diff, flags, files" + require_changes: false + +# Per-leg flags (ps7-ubuntu-latest, ps51-windows-2022, ...) uploaded by ci.yml. +# Carry a flag's last-known coverage forward on commits where it didn't upload. +flag_management: + default_rules: + carryforward: true From b9ca9a41283372a57794b966e348f81391c0e40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sat, 18 Jul 2026 22:07:19 +0200 Subject: [PATCH 2/6] Address review: add test-reporter and Dependabot for actions - Publish NUnit3 test results per matrix leg with dorny/test-reporter, a GitHub-native test report (Azure test-tab parity). test.ps1 gains an env-selectable result format (PESTER_TESTRESULT_FORMAT); Azure keeps the default NUnit 2.5, only the pilot switches to NUnit3. - Add Dependabot for github-actions (weekly, grouped, incl. major) to keep the actions current and ahead of Node.js runtime deprecations. Co-Authored-By: Claude Opus 4.8 --- .github/dependabot.yml | 16 ++++++++++++++++ .github/workflows/ci.yml | 19 +++++++++++++++++++ test.ps1 | 7 +++++++ 3 files changed, 42 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..9f3ed4223 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + # Keep the GitHub Actions used by the workflows current — in particular so we + # stay ahead of Node.js runtime deprecations, which land as major bumps. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" + update-types: + - major + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e8e9c67b..a5d43c330 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,8 @@ on: permissions: contents: read + actions: read + checks: write # dorny/test-reporter creates a check run with the test report env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '1' @@ -97,6 +99,10 @@ jobs: needs: build runs-on: ${{ matrix.os }} timeout-minutes: 20 + # NUnit3 so dorny/test-reporter (below) can parse the result file. test.ps1 + # reads this; unset elsewhere (e.g. Azure) keeps the default NUnit 2.5. + env: + PESTER_TESTRESULT_FORMAT: NUnit3 strategy: fail-fast: false matrix: @@ -195,6 +201,19 @@ jobs: testResults.xml coverage.xml + # Render the NUnit3 results as a check run (Azure test-tab parity). + # fail-on-error: false so a parse hiccup — or a fork PR, where the token is + # read-only and can't create checks — doesn't fail the leg. + - name: Publish test report + if: always() + uses: dorny/test-reporter@v3 + with: + name: 'Tests - ${{ matrix.name }}' + path: testResults.xml + reporter: dotnet-nunit + fail-on-error: false + use-actions-summary: 'true' + # Single gate after the matrix so branch protection / auto-merge only needs to # require "Done" instead of listing all 8 legs. Mirrors Azure's Done stage. # It also renders a consolidated coverage table (one glance at all legs). diff --git a/test.ps1 b/test.ps1 index 8939f2e3e..2b0c31f88 100644 --- a/test.ps1 +++ b/test.ps1 @@ -185,6 +185,13 @@ if ($CI) { $configuration.CodeCoverage.Enabled = $false $configuration.TestResult.Enabled = $true + + # Let CI pick the test-result format. Azure Pipelines consumes the default + # NUnit 2.5 schema; the GitHub Actions pilot sets NUnit3 so dorny/test-reporter + # can render it. Unset -> unchanged for every existing caller. + if ($env:PESTER_TESTRESULT_FORMAT) { + $configuration.TestResult.OutputFormat = $env:PESTER_TESTRESULT_FORMAT + } } $r = Invoke-Pester -Configuration $configuration From 247ede194b20057e522c5876b002a88c3f80b9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sat, 18 Jul 2026 22:26:58 +0200 Subject: [PATCH 3/6] Give test legs a checkout so test-reporter has git context dorny/test-reporter shells out to git and fails with exit 128 when the leg has no repo checkout (the legs are artifact-only). Add a shallow checkout before the artifact download; the artifact overlays it, so the tests still run against the published build. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5d43c330..250879f73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,14 @@ jobs: - { name: 'PS5.1 - Windows latest', id: ps51-windows-latest, os: windows-latest, psexe: powershell } - { name: 'PS5.1 - Windows 2022', id: ps51-windows-2022, os: windows-2022, psexe: powershell } steps: - # No checkout — we test the published build, exactly like Azure (checkout: none). + # Shallow checkout only so dorny/test-reporter (below) has git context — it + # shells out to git and errors without a repo. The artifact overlays this, + # so the tests still run against the published build, like Azure. + - name: Checkout (git context for test-reporter) + uses: actions/checkout@v6 + with: + fetch-depth: 1 + - name: Download build output uses: actions/download-artifact@v4 with: From 4d4136b003f96bf602a9686d231859cb2b1283be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sun, 19 Jul 2026 10:41:23 +0200 Subject: [PATCH 4/6] Use NUnit3 test results for both Azure and the pilot Azure PublishTestResults@2 supports NUnit3, so test.ps1 can just emit the modern format for both consumers. Drops the PESTER_TESTRESULT_FORMAT env indirection. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 4 ---- test.ps1 | 9 +++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 250879f73..9d509f1c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,10 +99,6 @@ jobs: needs: build runs-on: ${{ matrix.os }} timeout-minutes: 20 - # NUnit3 so dorny/test-reporter (below) can parse the result file. test.ps1 - # reads this; unset elsewhere (e.g. Azure) keeps the default NUnit 2.5. - env: - PESTER_TESTRESULT_FORMAT: NUnit3 strategy: fail-fast: false matrix: diff --git a/test.ps1 b/test.ps1 index 2b0c31f88..d927c7e8b 100644 --- a/test.ps1 +++ b/test.ps1 @@ -186,12 +186,9 @@ if ($CI) { $configuration.TestResult.Enabled = $true - # Let CI pick the test-result format. Azure Pipelines consumes the default - # NUnit 2.5 schema; the GitHub Actions pilot sets NUnit3 so dorny/test-reporter - # can render it. Unset -> unchanged for every existing caller. - if ($env:PESTER_TESTRESULT_FORMAT) { - $configuration.TestResult.OutputFormat = $env:PESTER_TESTRESULT_FORMAT - } + # Modern NUnit3 schema. Both consumers read it: Azure Pipelines + # (PublishTestResults@2) and the GitHub Actions pilot (dorny/test-reporter). + $configuration.TestResult.OutputFormat = 'NUnit3' } $r = Invoke-Pester -Configuration $configuration From 75d4f35a7e50a60ecaded0371dcbc9f7a24bb00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sat, 22 Aug 2026 08:24:57 +0200 Subject: [PATCH 5/6] Cut CI over to GitHub Actions, remove the Azure pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pilot ran alongside Azure since July and matches it, so this removes azure-pipelines.yml and makes the workflow the CI. Releasing stays on Azure DevOps in azure-pipelines-publish.yml, it needs the signing service connection and the publishing variable groups. Test results now render from test-report.yml, a workflow_run workflow, instead of from inside the matrix legs. That is the setup dorny/test-reporter recommends for public repos, and the reason the report was not showing: a run triggered by a pull request from a fork gets a read only token and cannot create a check run. The separate workflow runs in the base repo and can. It lists only failed suites and tests, a full pass list gets truncated by GitHub. Legs no longer check out the repo, that checkout only existed to give test-reporter git context. The gate to require in branch protection is "Done". 🤖 --- .artifactignore | 15 --- .github/workflows/ci.yml | 55 ++++------- .github/workflows/test-report.yml | 44 +++++++++ CONTRIBUTING.md | 4 +- README.md | 4 +- azure-pipelines.yml | 152 ------------------------------ codecov.yml | 8 +- test.ps1 | 4 +- 8 files changed, 74 insertions(+), 212 deletions(-) delete mode 100644 .artifactignore create mode 100644 .github/workflows/test-report.yml delete mode 100644 azure-pipelines.yml diff --git a/.artifactignore b/.artifactignore deleted file mode 100644 index 1c7b2496e..000000000 --- a/.artifactignore +++ /dev/null @@ -1,15 +0,0 @@ -src/csharp/bin/ -src/csharp/obj/ -src/csharp/.vs/ -publish/ -.git -.gitignore -.gitattributes -.artifactignore -azure-pipelines.yml -.github -.vscode -LICENSE -README.MD -VERIFICATION.txt - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d509f1c7..30337ab48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,25 @@ -# GitHub Actions CI — pilot port of azure-pipelines.yml. +# GitHub Actions CI — the CI for Pester. # -# This runs ALONGSIDE Azure Pipelines (we did not remove azure-pipelines.yml), -# so every push/PR triggers both systems and we can compare them before cutting -# over. Mirrors the Azure setup: build once on Windows, publish the built module -# as an artifact, then fan out the test matrix (all 8 legs run in parallel). +# This replaces azure-pipelines.yml, which is removed in the same change. Only +# releasing still runs on Azure DevOps (azure-pipelines-publish.yml), because it +# needs the signing service connection and the publishing variable groups. # -# Coverage: each leg runs `test.ps1 -CC`, producing a JaCoCo coverage.xml. Azure -# rendered this via PublishCodeCoverageResults@2; here we surface it natively in -# the run's job summaries (per leg, plus a consolidated table in "Done") and keep -# the raw coverage.xml as an artifact. +# Shape: build once on Windows, publish the built module as an artifact, then fan +# out the test matrix (all 8 legs run in parallel), then a single "Done" gate. +# +# Coverage: each leg runs `test.ps1 -CC`, producing a JaCoCo coverage.xml. We +# surface it in the run's job summaries (per leg, plus a consolidated table in +# "Done") and keep the raw coverage.xml as an artifact. +# +# Test results are uploaded as artifacts and rendered by test-report.yml, which +# runs after this workflow. That split is what makes the report work for pull +# requests from forks, where this workflow's token cannot create check runs. # # Image policy: GA only. We float `*-latest` for the newest GA image and pin the # one previous GA image ("latest + reasonably recent"). No preview images # (macos-26, windows-2025-vs2026) are used. Note macos-14 is on the deprecation # clock (brownouts Oct 2026, removal Nov 2 2026) — swap it out before then. -name: CI (GitHub Actions pilot) +name: CI on: push: @@ -37,8 +42,6 @@ on: permissions: contents: read - actions: read - checks: write # dorny/test-reporter creates a check run with the test report env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '1' @@ -114,14 +117,8 @@ jobs: - { name: 'PS5.1 - Windows latest', id: ps51-windows-latest, os: windows-latest, psexe: powershell } - { name: 'PS5.1 - Windows 2022', id: ps51-windows-2022, os: windows-2022, psexe: powershell } steps: - # Shallow checkout only so dorny/test-reporter (below) has git context — it - # shells out to git and errors without a repo. The artifact overlays this, - # so the tests still run against the published build, like Azure. - - name: Checkout (git context for test-reporter) - uses: actions/checkout@v6 - with: - fetch-depth: 1 - + # No checkout: the legs run against the artifact from Build, so they test + # the exact same build. - name: Download build output uses: actions/download-artifact@v4 with: @@ -204,21 +201,9 @@ jobs: testResults.xml coverage.xml - # Render the NUnit3 results as a check run (Azure test-tab parity). - # fail-on-error: false so a parse hiccup — or a fork PR, where the token is - # read-only and can't create checks — doesn't fail the leg. - - name: Publish test report - if: always() - uses: dorny/test-reporter@v3 - with: - name: 'Tests - ${{ matrix.name }}' - path: testResults.xml - reporter: dotnet-nunit - fail-on-error: false - use-actions-summary: 'true' - - # Single gate after the matrix so branch protection / auto-merge only needs to - # require "Done" instead of listing all 8 legs. Mirrors Azure's Done stage. + # Single gate after the matrix so branch protection and auto-merge only need to + # require "Done" instead of listing all 8 legs. This is the check to require on + # main and on the release branches. # It also renders a consolidated coverage table (one glance at all legs). done: name: Done diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml new file mode 100644 index 000000000..f805e7db6 --- /dev/null +++ b/.github/workflows/test-report.yml @@ -0,0 +1,44 @@ +# Renders the test results produced by ci.yml as check runs on the commit. +# +# This is a separate workflow on purpose. dorny/test-reporter has to create a +# check run, and a workflow triggered by a pull request from a fork gets a +# read-only token that cannot do that. A workflow_run workflow always runs in +# the context of the base repository, with a writable token, so the report shows +# up for fork pull requests too. +# https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories +name: Test report + +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: + contents: read + actions: read # read the artifacts of the run that triggered this one + checks: write # create the check run holding the report + +jobs: + report: + name: Test report + runs-on: ubuntu-latest + # Nothing to report when the run never got to the tests. + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' }} + steps: + # One artifact per matrix leg (results-ps7-ubuntu-latest, ...), so the + # regex capture becomes the check name: "Tests - ps7-ubuntu-latest". + # Only failed suites and tests are listed; a full pass list is thousands + # of lines and GitHub truncates the report. + - uses: dorny/test-reporter@v3 + with: + artifact: /results-(.*)/ + name: 'Tests - $1' + path: testResults.xml + reporter: dotnet-nunit + list-suites: failed + list-tests: failed + use-actions-summary: 'false' + # A failed test already fails the CI run, no need to also paint this + # workflow red. fail-on-empty stays on, so a report we cannot find or + # parse is still visible instead of silently doing nothing. + fail-on-error: 'false' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68546c435..c2361687b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,9 +92,9 @@ Get-Help ./test.ps1 -Detailed ## Continuous Integration -The Azure Devops Pipeline azure-pipelines.yml file contains the code definition used for builds, unit and integration tests in the CI pipeline. +CI runs on GitHub Actions. `.github/workflows/ci.yml` builds the module once and then runs the test matrix: PS7 and PS5.1, on Windows, Linux (Ubuntu) and MacOS. `.github/workflows/test-report.yml` renders the test results afterwards, so they also show up for pull requests from forks. -Within the pipeline, tests are executed against PS7 and PS5.1, on Windows, Linux (Ubuntu) and MacOS. +Releasing still runs on Azure DevOps (`azure-pipelines-publish.yml`), because it needs the code signing and publishing credentials. ## Documentation diff --git a/README.md b/README.md index 68cc46dd6..a66cd068a 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,9 @@ test_script: See it [in action here!](https://ci.appveyor.com/project/nohwnd/planets) If you do not need to test your scripts against PowerShell Core, just simply remove the entire line mentioning Ubuntu. -Pester itself is built on AzureDevOps, and distributed mainly via PowerShell gallery. +Pester itself is built on GitHub Actions, and distributed mainly via PowerShell gallery. -[![Build Status](https://nohwnd.visualstudio.com/Pester/_apis/build/status/Pester%20PR?branchName=main)](https://nohwnd.visualstudio.com/Pester/_build/latest?definitionId=6&branchName=main) [![latest version](https://img.shields.io/powershellgallery/v/Pester.svg?label=latest+version)](https://www.powershellgallery.com/packages/Pester) [![downloads](https://img.shields.io/powershellgallery/dt/Pester.svg?label=downloads)](https://www.powershellgallery.com/packages/Pester) +[![Build Status](https://github.com/pester/Pester/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/pester/Pester/actions/workflows/ci.yml?query=branch%3Amain) [![latest version](https://img.shields.io/powershellgallery/v/Pester.svg?label=latest+version)](https://www.powershellgallery.com/packages/Pester) [![downloads](https://img.shields.io/powershellgallery/dt/Pester.svg?label=downloads)](https://www.powershellgallery.com/packages/Pester) ## Further reading diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 67a9e0379..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,152 +0,0 @@ -trigger: - branches: - include: - - main - - rel/* - paths: - exclude: - - .devcontainer - - .github - - .vscode - - docs - - images - - BACKERS.md - - CODE_OF_CONDUCT.md - - CONTRIBUTING.md - - README.md - - SECURITY.md - - SUPPORT.md -pr: - branches: - include: - - main - - rel/* - - dev/* - paths: - exclude: - - .devcontainer - - .github - - .vscode - - docs - - images - - BACKERS.md - - CODE_OF_CONDUCT.md - - CONTRIBUTING.md - - README.md - - SECURITY.md - - SUPPORT.md - -variables: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_GENERATE_ASPNET_CERTIFICATE: 0 - DOTNET_NOLOGO: 1 - CI: 1 - NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages - -stages: - - stage: Build - jobs: - - job: build - workspace: - clean: all - pool: - vmImage: windows-2022 - timeoutInMinutes: 5 - steps: - - checkout: self - fetchDepth: 1 - - task: Cache@2 - displayName: Cache - inputs: - # https://docs.microsoft.com/en-us/azure/devops/pipelines/artifacts/caching-nuget?view=azure-devops - key: 'nuget | "$(Agent.OS)" | **/packages.lock.json,!**/bin/**,!**/obj/**' - path: '$(NUGET_PACKAGES)' - cacheHitVar: 'CACHE_RESTORED' - - powershell: | - "Running .NET SDK v$(dotnet --version)" - ./build.ps1 -LockedRestore -Clean -Inline - - publish: $(Build.SourcesDirectory) - artifact: all - - - stage: Test - jobs: - - job: - workspace: - clean: all - strategy: - matrix: - PS7_Ubuntu_22_04: - vmImage: ubuntu-22.04 - pwsh: true - PS7_Ubuntu_24_04: - vmImage: ubuntu-24.04 - pwsh: true - PS7_macOS_14: - vmImage: macOS-14 - pwsh: true - PS7_macOS_15: - vmImage: macOS-15 - pwsh: true - PS7_Windows_Server2022: - vmImage: windows-2022 - pwsh: true - PS_5_1_Windows_Server2022: - vmImage: windows-2022 - pwsh: false - PS7_Windows_Server2025: - vmImage: windows-2025 - pwsh: true - PS_5_1_Windows_Server2025: - vmImage: windows-2025 - pwsh: false - pool: - vmImage: $[ variables['vmImage'] ] - steps: - - checkout: none - - task: DownloadPipelineArtifact@2 - inputs: - buildType: 'current' - artifactName: 'all' - targetPath: '$(Build.SourcesDirectory)' - - task: PowerShell@2 - displayName: 'Test Pester' - inputs: - targetType: inline - pwsh: $(pwsh) - script: | - & ./test.ps1 -CI -CC -PassThru -NoBuild - workingDirectory: '$(Build.SourcesDirectory)' - - task: PublishCodeCoverageResults@2 - inputs: - summaryFileLocation: 'coverage.xml' - pathToSources: '$(Build.SourcesDirectory)/bin/' - failIfCoverageEmpty: false - condition: succeededOrFailed() - - task: PublishTestResults@2 - inputs: - testResultsFormat: 'NUnit' - testResultsFiles: 'testResults.xml' - failTaskOnFailedTests: true - condition: succeededOrFailed() - timeoutInMinutes: 20 - # Run one step after the matrix so we don't have to list all the - # separate items in GitHub, and still are able to await the completion of the run - # before auto-merge is allowed. - - stage: Done - jobs: - - job: Done - workspace: - clean: all - steps: - - checkout: none - - task: PowerShell@2 - displayName: 'All done' - inputs: - targetType: inline - pwsh: true - script: | - "done" - workingDirectory: '$(Build.SourcesDirectory)' - - diff --git a/codecov.yml b/codecov.yml index 9fc23f205..ffffd8207 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,8 +1,8 @@ -# Codecov configuration — added alongside the GitHub Actions CI pilot -# (.github/workflows/ci.yml). Coverage is uploaded per matrix leg using flags. +# Codecov configuration for .github/workflows/ci.yml. Coverage is uploaded per +# matrix leg using flags. # -# During the pilot we keep coverage status "informational" so Codecov never -# posts a failing/blocking commit status while we compare against Azure. +# Coverage status is "informational", so Codecov reports the numbers but never +# posts a failing or blocking commit status. codecov: # We upload one report per matrix leg (8 legs). Wait for all of them before diff --git a/test.ps1 b/test.ps1 index 398fd160e..82a960e04 100644 --- a/test.ps1 +++ b/test.ps1 @@ -190,8 +190,8 @@ if ($CI) { $configuration.TestResult.Enabled = $true - # Modern NUnit3 schema. Both consumers read it: Azure Pipelines - # (PublishTestResults@2) and the GitHub Actions pilot (dorny/test-reporter). + # Modern NUnit3 schema, which is what dorny/test-reporter reads in + # .github/workflows/test-report.yml. $configuration.TestResult.OutputFormat = 'NUnit3' } From 1a691ae58bde1e32eeb016557e0c3b3aad78de20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sat, 22 Aug 2026 09:09:03 +0200 Subject: [PATCH 6/6] Update actions to the versions that run on Node 24 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runner forces Node 20 actions onto Node 24 and warns about it on every job. setup-dotnet v6, upload-artifact v7, download-artifact v8, codecov-action v7. The github-script warning came from codecov-action v5, it is gone with v7. download-artifact v8 fails the job when the download digest does not match, instead of only warning. 🤖 --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30337ab48..b2c538a9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: fetch-depth: 1 - name: Setup .NET (global.json) - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: global-json-file: global.json cache: true @@ -81,7 +81,7 @@ jobs: # Publish the built tree so every test leg runs the exact same build. # Mirrors Azure's `publish: $(Build.SourcesDirectory)` + .artifactignore. - name: Upload build output - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: pester-build retention-days: 1 @@ -120,7 +120,7 @@ jobs: # No checkout: the legs run against the artifact from Build, so they test # the exact same build. - name: Download build output - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: pester-build path: . @@ -181,7 +181,7 @@ jobs: # CODECOV_TOKEN secret, if set, makes uploads more reliable. - name: Upload coverage to Codecov if: always() - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: files: ./coverage.xml disable_search: true @@ -192,7 +192,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: results-${{ matrix.id }} retention-days: 7 @@ -213,7 +213,7 @@ jobs: steps: - name: Download coverage reports if: always() - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: results-* path: coverage-reports