From 8695d8d63bed46319f6bb64a50170e8a36cf431a Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Sun, 4 Oct 2026 15:45:30 +0200 Subject: [PATCH] ci: move CI from AppVeyor to GitHub Actions .github/workflows/ci.yml replaces appveyor.yml and runs on pull requests and pushes to master: - build (windows-2025): the same restore, Release build, and documentation checks as before, then the Pester tests in Windows PowerShell 5.1 and in PowerShell 7. .github/scripts/Invoke-Tests.ps1 writes the counts and the failed tests to the job summary and the NUnit file to the test-results artifact, and also fails on test files that fail. - wiki (ubuntu-latest): generates the wiki from Docs; on pull requests it lists the pages that would change, from master it publishes them with the built-in token. Only this job has contents: write. Actions are pinned by commit SHA. Every native command checks its exit code, because GitHub checks only the last one. The contributor guide describes the workflow and the wiki. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .github/scripts/Invoke-Tests.ps1 | 79 ++++++++++++++ .github/workflows/ci.yml | 181 +++++++++++++++++++++++++++++++ Docs/Contributing/02-Writing.md | 48 ++++++-- appveyor.yml | 86 --------------- 4 files changed, 296 insertions(+), 98 deletions(-) create mode 100644 .github/scripts/Invoke-Tests.ps1 create mode 100644 .github/workflows/ci.yml delete mode 100644 appveyor.yml diff --git a/.github/scripts/Invoke-Tests.ps1 b/.github/scripts/Invoke-Tests.ps1 new file mode 100644 index 0000000..2fdc016 --- /dev/null +++ b/.github/scripts/Invoke-Tests.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Runs the Pester tests in the Tests folder and reports the result to GitHub Actions. + +.DESCRIPTION + Imports Pester 5.7.1, runs the tests against the module build in NTFSSecurity\bin\Release, writes the result + file in the NUnit format, and adds the counts and the failed tests to the job summary of GitHub Actions. Fails if + a test or a test file fails. + +.PARAMETER ResultPath + Specifies the path of the result file. + +.PARAMETER Title + Specifies the heading of the test results in the job summary, such as the PowerShell edition. + +.EXAMPLE + .\.github\scripts\Invoke-Tests.ps1 -ResultPath TestResults\WindowsPowerShell.xml -Title 'Windows PowerShell 5.1' + + Runs the tests and writes the result file. Outside GitHub Actions, the script writes no job summary. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $ResultPath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Title +) + +$ErrorActionPreference = 'Stop' +Import-Module -Name Pester -RequiredVersion 5.7.1 + +$resultFolder = Split-Path -Path $ResultPath -Parent +if ($resultFolder -and -not (Test-Path -LiteralPath $resultFolder)) { + New-Item -ItemType Directory -Path $resultFolder | Out-Null +} + +$configuration = New-PesterConfiguration +$configuration.Run.Path = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Tests' +$configuration.Run.PassThru = $true +$configuration.Output.Verbosity = 'Detailed' +$configuration.TestResult.Enabled = $true +$configuration.TestResult.OutputFormat = 'NUnitXml' +$configuration.TestResult.OutputPath = $ResultPath +$result = Invoke-Pester -Configuration $configuration + +if ($env:GITHUB_STEP_SUMMARY) { + $summary = New-Object -TypeName 'System.Collections.Generic.List[string]' + $summary.Add("### Tests in $Title") + $summary.Add('') + $summary.Add('| Result | Passed | Failed | Skipped | Total |') + $summary.Add('| --- | ---: | ---: | ---: | ---: |') + $summary.Add(('| {0} | {1} | {2} | {3} | {4} |' -f $result.Result, $result.PassedCount, $result.FailedCount, + $result.SkippedCount, $result.TotalCount)) + if ($result.Failed.Count -gt 0 -or $result.FailedContainersCount -gt 0) { + $summary.Add('') + $summary.Add('Failed:') + $summary.Add('') + foreach ($test in $result.Failed) { + $message = "$(@($test.ErrorRecord)[0])" -replace '\s+', ' ' + $summary.Add(('- {0}: {1}' -f $test.ExpandedPath, $message)) + } + foreach ($container in $result.Containers | Where-Object -Property Result -EQ -Value 'Failed') { + $summary.Add(('- {0}: {1}' -f $container.Item, ("$(@($container.ErrorRecord)[0])" -replace '\s+', ' '))) + } + } + $summary.Add('') + + $encoding = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList $false + [IO.File]::AppendAllText($env:GITHUB_STEP_SUMMARY, ($summary -join "`n") + "`n", $encoding) +} + +if ($result.Result -ne 'Passed') { + throw "The tests in $Title failed: $($result.FailedCount) failed tests, $($result.FailedContainersCount) failed test files." +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4ef29be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,181 @@ +# Builds the NTFSSecurity module from source, checks that the cmdlet +# documentation in Docs/Cmdlets and the help file generated from it match the +# cmdlets of that build, runs the Pester tests in Windows PowerShell 5.1 and +# PowerShell 7, and publishes Docs to the GitHub wiki from master. +name: CI + +on: + pull_request: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: Build and test + runs-on: windows-2025 + timeout-minutes: 30 + steps: + - name: Keep Windows line endings in the working tree + run: git config --global core.autocrlf true + + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install platyPS, MarkdownLinkCheck, and Pester + shell: powershell + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null + # Installed for all users, so that PowerShell 7 finds the modules, too + Install-Module -Name platyPS -RequiredVersion 0.14.2 -Scope AllUsers -Force + Install-Module -Name MarkdownLinkCheck -RequiredVersion 0.2.0 -Scope AllUsers -Force + # The image includes Pester 3.4.0, which is signed by a different publisher. + Install-Module -Name Pester -RequiredVersion 5.7.1 -Scope AllUsers -Force -SkipPublisherCheck + + - name: Restore the NuGet packages + shell: powershell + run: | + nuget restore NTFSSecurity\packages.config -PackagesDirectory packages -NonInteractive + if ($LASTEXITCODE -ne 0) { + throw "NuGet failed with exit code $LASTEXITCODE." + } + nuget restore Security2\packages.config -PackagesDirectory packages -NonInteractive + if ($LASTEXITCODE -ne 0) { + throw "NuGet failed with exit code $LASTEXITCODE." + } + # Provides the .NET Framework 4.5.2 reference assemblies, so the build does + # not depend on a targeting pack installed on the runner. + nuget install Microsoft.NETFramework.ReferenceAssemblies.net452 -Version 1.0.3 -OutputDirectory packages -NonInteractive + if ($LASTEXITCODE -ne 0) { + throw "NuGet failed with exit code $LASTEXITCODE." + } + + - name: Build the module + id: build + shell: powershell + run: | + # MSBuild isn't on the path of the runner; vswhere finds the one of Visual Studio. + $vswhere = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Microsoft Visual Studio\Installer\vswhere.exe' + $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 + if (-not $msbuild) { + throw 'MSBuild was not found.' + } + $referenceAssemblies = "$env:GITHUB_WORKSPACE\packages\Microsoft.NETFramework.ReferenceAssemblies.net452.1.0.3\build" + & $msbuild NTFSSecurity\NTFSSecurity.csproj /nologo /verbosity:minimal /p:Configuration=Release "/p:TargetFrameworkRootPath=$referenceAssemblies" "/p:FrameworkPathOverride=$referenceAssemblies\.NETFramework\v4.5.2" + if ($LASTEXITCODE -ne 0) { + throw "MSBuild failed with exit code $LASTEXITCODE." + } + + - name: Check the documentation against the build + shell: powershell + run: | + Import-Module -Name platyPS -RequiredVersion 0.14.2 + Import-Module -Name MarkdownLinkCheck -RequiredVersion 0.2.0 + Import-Module -Name .\NTFSSecurity\bin\Release\NTFSSecurity.psd1 -Force + + # 01. Test that the documentation matches the cmdlets built from source + Update-MarkdownHelp -Path ./Docs/Cmdlets | Out-Null + $diff = git diff -- Docs/Cmdlets + if ($diff) { + throw "Help is not up-to-date, run Update-MarkdownHelp: $diff" + } + + # 02. Verify hyperlinks + $brokenLinks = Get-MarkdownLink -Path .\Docs\ -BrokenOnly + if ($brokenLinks) { + throw "Found broken hyperlinks $brokenLinks" + } + + # 03. Test that the help file of the module matches the documentation + New-ExternalHelp -Path ./Docs/Cmdlets -OutputPath ./NTFSSecurity/en-US -Force | Out-Null + $helpChanges = git status --porcelain -- NTFSSecurity/en-US + if ($helpChanges) { + throw "The help file is not up-to-date, run New-ExternalHelp -Path .\Docs\Cmdlets -OutputPath .\NTFSSecurity\en-US -Force: $helpChanges" + } + + # 04. Run the Pester tests against the build, also when the documentation check failed + - name: Run the tests in Windows PowerShell 5.1 + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + shell: powershell + run: .\.github\scripts\Invoke-Tests.ps1 -ResultPath TestResults\WindowsPowerShell.xml -Title 'Windows PowerShell 5.1' + + - name: Run the tests in PowerShell 7 + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + shell: pwsh + run: ./.github/scripts/Invoke-Tests.ps1 -ResultPath TestResults/PowerShell7.xml -Title 'PowerShell 7' + + - name: Upload the test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-results + path: TestResults/ + if-no-files-found: ignore + + wiki: + name: Wiki + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + # Only this job can write: it publishes the wiki from master. On pull + # requests, it shows the pages that would change. + permissions: + contents: write + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Generate the wiki pages + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + gh auth setup-git + $wiki = Join-Path -Path $env:RUNNER_TEMP -ChildPath 'wiki' + git clone --quiet --depth 1 "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY.wiki.git" $wiki + if ($LASTEXITCODE -ne 0) { + throw "Cloning the wiki failed with exit code $LASTEXITCODE." + } + ./.github/scripts/Export-WikiContent.ps1 -Path ./Docs -DestinationPath $wiki -RepositoryUrl "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" + git -C $wiki add --all + $changes = @(git -C $wiki diff --cached --name-status) + $summary = if ($changes) { + @('### Wiki', '', 'Changed pages (A added, M modified, D deleted):', '', '```text') + $changes + @('```') + } else { + @('### Wiki', '', 'The wiki is up to date.') + } + Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value $summary + Add-Content -LiteralPath $env:GITHUB_ENV -Value "WIKI_PATH=$wiki" + + - name: Publish the wiki + if: ${{ github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + git -C $env:WIKI_PATH diff --cached --quiet + if ($LASTEXITCODE -eq 0) { + 'The wiki is up to date.' + exit 0 + } + git -C $env:WIKI_PATH -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' commit --quiet --message "Update from $env:GITHUB_SHA" + if ($LASTEXITCODE -ne 0) { + throw "Committing the wiki failed with exit code $LASTEXITCODE." + } + git -C $env:WIKI_PATH push --quiet + if ($LASTEXITCODE -ne 0) { + throw "Publishing the wiki failed with exit code $LASTEXITCODE." + } diff --git a/Docs/Contributing/02-Writing.md b/Docs/Contributing/02-Writing.md index 872f263..4ff56b7 100644 --- a/Docs/Contributing/02-Writing.md +++ b/Docs/Contributing/02-Writing.md @@ -1,8 +1,9 @@ # Write documentation The NTFSSecurity documentation is written in Markdown and lives in the -`Docs` folder of the repository, where GitHub renders it. This page explains -how the documentation is organized and how to change it. +`Docs` folder of the repository, where GitHub renders it. GitHub Actions also +publishes it to the [wiki](https://github.com/raandree/NTFSSecurity/wiki). +This page explains how the documentation is organized and how to change it. ## Documentation structure @@ -17,6 +18,8 @@ how the documentation is organized and how to change it. | `Docs/Contributing.md`, `Docs/Contributing/*.md` | This contributor guide | | `README.md` | Front page of the GitHub repository | | `CHANGELOG.md` | Changes since 4.2.6 | +| `.github/workflows/ci.yml` | CI build: documentation checks, tests, and wiki publishing | +| `.github/scripts/Export-WikiContent.ps1` | Converts `Docs` into the pages of the wiki | When you add a page, link to it from `Docs/README.md` or from a related page, so that readers can find it. @@ -44,8 +47,9 @@ parameter descriptions, the examples, and the notes. When a cmdlet changes, build the module, import the build output, and update the pages in Windows PowerShell 5.1. A Release build writes the module to -`NTFSSecurity\bin\Release`; the `before_build` and `build_script` steps in -`appveyor.yml` show the commands that the CI build uses: +`NTFSSecurity\bin\Release`; the steps "Restore the NuGet packages" and "Build +the module" in `.github/workflows/ci.yml` show the commands that the CI build +uses: ```powershell Install-Module -Name platyPS -RequiredVersion 0.14.2 @@ -83,19 +87,39 @@ before you push it, open it in Visual Studio Code and press **Ctrl+Shift+V**. In a pull request, the **Files changed** tab shows a changed page rendered when you open its menu (**...**) and select **View file**. +## Publish to the wiki + +After every push to `master`, the CI workflow converts the pages in `Docs`, +except this contributor guide, into the pages of the +[wiki](https://github.com/raandree/NTFSSecurity/wiki) and publishes them. +`Docs/README.md` becomes the home page, every cmdlet page becomes a page with +the name of the cmdlet, and the sidebar lists the cmdlets in the groups of the +cmdlet list in `Docs/README.md`. Don't edit the wiki itself; the next push +overwrites it. + +For a pull request, the summary of the CI run lists the wiki pages that would +change. To look at the pages before you push, write them into a clone of the +wiki: + +```powershell +git clone https://github.com/raandree/NTFSSecurity.wiki.git $env:TEMP\wiki +.\.github\scripts\Export-WikiContent.ps1 -Path .\Docs -DestinationPath $env:TEMP\wiki +``` + ## Check your change Before you open a pull request, check the following: - No page in `Docs/Cmdlets` contains a `{{ ... }}` placeholder. -- `Update-MarkdownHelp` doesn't change any page in `Docs/Cmdlets`. The build - defined in `appveyor.yml` runs the same check. +- `Update-MarkdownHelp` doesn't change any page in `Docs/Cmdlets`. The CI + workflow runs the same check. - `New-ExternalHelp` doesn't change - `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml`. The build runs the same - check. + `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml`. The CI workflow runs the + same check. - The Pester tests in `Tests` pass. They test the module in - `NTFSSecurity\bin\Release`, for example that `Get-Help` shows every page. - The build runs them in Windows PowerShell 5.1 with Pester 5.7.1: + `NTFSSecurity\bin\Release`, for example that `Get-Help` shows every page, + and the conversion to the wiki. The CI workflow runs them in Windows + PowerShell 5.1 and in PowerShell 7 with Pester 5.7.1: ```powershell Install-Module -Name Pester -RequiredVersion 5.7.1 -SkipPublisherCheck @@ -103,8 +127,8 @@ Before you open a pull request, check the following: Invoke-Pester -Path .\Tests -Output Detailed ``` -- All links work. The build checks them with `Get-MarkdownLink` from the - MarkdownLinkCheck module: +- All links work. The CI workflow checks them with `Get-MarkdownLink` from + the MarkdownLinkCheck module: ```powershell Get-MarkdownLink -Path .\Docs -BrokenOnly diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 9354623..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,86 +0,0 @@ -# Builds the NTFSSecurity module from source, checks that the cmdlet -# documentation in Docs/Cmdlets and the help file generated from it match the -# cmdlets of that build, and runs the Pester tests against the build. -image: Visual Studio 2022 - -init: - - ps: git config --global core.autocrlf true - -install: - - ps: | - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null - Install-Module -Name platyPS -RequiredVersion 0.14.2 -Force - Install-Module -Name MarkdownLinkCheck -RequiredVersion 0.2.0 -Force - # The image includes Pester 3.4.0, which is signed by a different publisher. - Install-Module -Name Pester -RequiredVersion 5.7.1 -Force -SkipPublisherCheck - -before_build: - - nuget restore NTFSSecurity\packages.config -PackagesDirectory packages -NonInteractive - - nuget restore Security2\packages.config -PackagesDirectory packages -NonInteractive - # Provides the .NET Framework 4.5.2 reference assemblies, so the build does - # not depend on a targeting pack installed on the build image. - - nuget install Microsoft.NETFramework.ReferenceAssemblies.net452 -Version 1.0.3 -OutputDirectory packages -NonInteractive - -build_script: - - ps: | - $referenceAssemblies = "$env:APPVEYOR_BUILD_FOLDER\packages\Microsoft.NETFramework.ReferenceAssemblies.net452.1.0.3\build" - msbuild NTFSSecurity\NTFSSecurity.csproj /nologo /verbosity:minimal /p:Configuration=Release "/p:TargetFrameworkRootPath=$referenceAssemblies" "/p:FrameworkPathOverride=$referenceAssemblies\.NETFramework\v4.5.2" - if ($LASTEXITCODE -ne 0) { - throw "MSBuild failed with exit code $LASTEXITCODE." - } - -test_script: - - ps: | - $ErrorActionPreference = 'Stop' - Import-Module -Name platyPS - Import-Module -Name MarkdownLinkCheck - Import-Module -Name .\NTFSSecurity\bin\Release\NTFSSecurity.psd1 -Force - - # 01. Test that the documentation matches the cmdlets built from source - Update-MarkdownHelp -Path ./Docs/Cmdlets | Out-Null - $diff = git diff -- Docs/Cmdlets - if ($diff) { - throw "Help is not up-to-date, run Update-MarkdownHelp: $diff" - } - - # 02. Verify hyperlinks - $brokenLinks = Get-MarkdownLink -Path .\Docs\ -BrokenOnly - if ($brokenLinks) { - throw "Found broken hyperlinks $brokenLinks" - } - - # 03. Test that the help file of the module matches the documentation - New-ExternalHelp -Path ./Docs/Cmdlets -OutputPath ./NTFSSecurity/en-US -Force | Out-Null - $helpChanges = git status --porcelain -- NTFSSecurity/en-US - if ($helpChanges) { - throw "The help file is not up-to-date, run New-ExternalHelp -Path .\Docs\Cmdlets -OutputPath .\NTFSSecurity\en-US -Force: $helpChanges" - } - - # 04. Run the Pester tests against the module build - Import-Module -Name Pester -RequiredVersion 5.7.1 - $configuration = New-PesterConfiguration - $configuration.Run.Path = '.\Tests' - $configuration.Run.PassThru = $true - $configuration.Output.Verbosity = 'Detailed' - $result = Invoke-Pester -Configuration $configuration - - # Report every test once on the Tests tab. An uploaded NUnit file lists a - # Pester 5 test once for each block that contains it. - if ($env:APPVEYOR_API_URL -and $result.Tests) { - $tests = @(foreach ($test in $result.Tests) { - @{ - testName = $test.ExpandedPath - testFramework = 'Pester' - fileName = Split-Path -Path $test.ScriptBlock.File -Leaf - outcome = if ($test.Result -eq 'NotRun') { 'NotRunnable' } else { "$($test.Result)" } - durationMilliseconds = [long] $test.Duration.TotalMilliseconds - ErrorMessage = @($test.ErrorRecord | ForEach-Object -Process { "$_" }) -join [Environment]::NewLine - } - }) - $body = [Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject $tests -Compress)) - Invoke-RestMethod -Method Post -Uri ($env:APPVEYOR_API_URL.TrimEnd('/') + '/api/tests/batch') -Body $body -ContentType 'application/json; charset=utf-8' | Out-Null - } - if ($result.FailedCount -gt 0) { - throw "$($result.FailedCount) Pester tests failed." - }