mirror of https://github.com/raandree/NTFSSecurity
Browse Source
ci: move CI from AppVeyor to GitHub Actions and publish the docs to the wikipull/97/head
committed by
GitHub
18 changed files with 965 additions and 192 deletions
@ -0,0 +1,279 @@ |
|||
<# |
|||
.SYNOPSIS |
|||
Converts the documentation in the Docs folder into the pages of the GitHub wiki. |
|||
|
|||
.DESCRIPTION |
|||
Writes a wiki page for every page in Docs except the contributor guide (Contributing.md and the Contributing |
|||
folder). Docs/README.md becomes the page Home, and every other page keeps its file name, so a page in |
|||
Docs/Cmdlets becomes a page named after its cmdlet. The platyPS metadata at the top of the cmdlet pages is |
|||
removed. Relative links point to the wiki pages; links to other files of the repository point to the files on |
|||
GitHub. Links in code stay unchanged. |
|||
|
|||
The script also writes the sidebar, which lists the cmdlets in the groups of the cmdlet list in Docs/README.md; |
|||
the footer; and the page How-to-install, which keeps the address of the former wiki page working. |
|||
|
|||
The script removes everything in DestinationPath except the .git folder, so that pages that no longer exist in |
|||
Docs disappear from the wiki. |
|||
|
|||
.PARAMETER Path |
|||
Specifies the Docs folder of the repository. |
|||
|
|||
.PARAMETER DestinationPath |
|||
Specifies the folder to write the wiki pages to, usually a clone of the wiki repository. The script creates the |
|||
folder if it doesn't exist. |
|||
|
|||
.PARAMETER RepositoryUrl |
|||
Specifies the address of the repository on GitHub, for links to files that aren't wiki pages. |
|||
|
|||
.PARAMETER Branch |
|||
Specifies the branch for links to files that aren't wiki pages. |
|||
|
|||
.EXAMPLE |
|||
git clone https://github.com/raandree/NTFSSecurity.wiki.git $env:TEMP\wiki |
|||
.\.github\scripts\Export-WikiContent.ps1 -Path .\Docs -DestinationPath $env:TEMP\wiki |
|||
git -C $env:TEMP\wiki status |
|||
|
|||
Writes the wiki pages into a clone of the wiki and shows which pages change. |
|||
#> |
|||
[CmdletBinding(SupportsShouldProcess)] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] |
|||
[string] |
|||
$Path, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[ValidateNotNullOrEmpty()] |
|||
[string] |
|||
$DestinationPath, |
|||
|
|||
[Parameter()] |
|||
[ValidateNotNullOrEmpty()] |
|||
[string] |
|||
$RepositoryUrl = 'https://github.com/raandree/NTFSSecurity', |
|||
|
|||
[Parameter()] |
|||
[ValidateNotNullOrEmpty()] |
|||
[string] |
|||
$Branch = 'master' |
|||
) |
|||
|
|||
$ErrorActionPreference = 'Stop' |
|||
|
|||
function Resolve-RepositoryPath { |
|||
<# |
|||
Returns the path of a link target relative to the repository root, with / as separator, or nothing if the |
|||
link leaves the repository. |
|||
#> |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Directory, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Link |
|||
) |
|||
|
|||
$segments = New-Object -TypeName 'System.Collections.Generic.List[string]' |
|||
foreach ($segment in (('{0}/{1}' -f $Directory, $Link) -split '/')) { |
|||
if ($segment -eq '..') { |
|||
if ($segments.Count -eq 0) { |
|||
return |
|||
} |
|||
$segments.RemoveAt($segments.Count - 1) |
|||
} elseif ($segment -and $segment -ne '.') { |
|||
$segments.Add($segment) |
|||
} |
|||
} |
|||
|
|||
$segments -join '/' |
|||
} |
|||
|
|||
function ConvertTo-WikiLink { |
|||
<# |
|||
Returns the wiki address of a link on a page in Directory: the name of a wiki page, the address of a file of |
|||
the repository on GitHub, or the link itself if it is absolute or points to the same page. |
|||
#> |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Url, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Directory |
|||
) |
|||
|
|||
if ($Url -match '^(?:[a-zA-Z][a-zA-Z0-9+.-]*:|//|#)') { |
|||
return $Url |
|||
} |
|||
|
|||
$linkPath, $anchor = $Url -split '#', 2 |
|||
$target = Resolve-RepositoryPath -Directory $Directory -Link ([uri]::UnescapeDataString($linkPath)) |
|||
if (-not $target) { |
|||
return $Url |
|||
} |
|||
|
|||
$fragment = if ($anchor) { '#' + $anchor } else { '' } |
|||
if ($pageNames.ContainsKey($target)) { |
|||
return $pageNames[$target] + $fragment |
|||
} |
|||
|
|||
$view = if (Test-Path -LiteralPath (Join-Path -Path $repositoryRoot -ChildPath $target) -PathType Container) { |
|||
'tree' |
|||
} else { |
|||
'blob' |
|||
} |
|||
'{0}/{1}/{2}/{3}{4}' -f $RepositoryUrl.TrimEnd('/'), $view, $Branch, $target, $fragment |
|||
} |
|||
|
|||
function Convert-MarkdownLink { |
|||
<# |
|||
Returns the Markdown text with the links of a page in Directory converted to wiki addresses. Links in code |
|||
spans and fenced code blocks stay unchanged. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSReviewUnusedParameter', 'Directory', Justification = 'The match evaluator script block uses it.' |
|||
)] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[AllowEmptyString()] |
|||
[string] |
|||
$Markdown, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Directory |
|||
) |
|||
|
|||
$inlinePattern = '(?<code>(?<ticks>`+).+?\k<ticks>)|(?<prefix>!?\[(?:[^\[\]`]|`[^`]*`)*\]\()(?<url>[^)\s]+)(?<suffix>(?:\s+"[^"]*")?\))' |
|||
$definitionPattern = '^(?<prefix>\s{0,3}\[[^\]]+\]:\s*)(?<url>\S+)(?<suffix>.*)$' |
|||
$convertLink = { |
|||
param ($match) |
|||
|
|||
if ($match.Groups['code'].Success) { |
|||
return $match.Value |
|||
} |
|||
$match.Groups['prefix'].Value + (ConvertTo-WikiLink -Url $match.Groups['url'].Value -Directory $Directory) + |
|||
$match.Groups['suffix'].Value |
|||
} |
|||
|
|||
$fence = $null |
|||
$lines = foreach ($line in ($Markdown -split '\r?\n')) { |
|||
if ($line -match '^\s{0,3}(?<fence>`{3,}|~{3,})') { |
|||
if (-not $fence) { |
|||
$fence = $Matches['fence'] |
|||
} elseif ($Matches['fence'].StartsWith($fence)) { |
|||
$fence = $null |
|||
} |
|||
$line |
|||
continue |
|||
} |
|||
if ($fence) { |
|||
$line |
|||
continue |
|||
} |
|||
|
|||
$line = [regex]::Replace($line, $inlinePattern, $convertLink) |
|||
[regex]::Replace($line, $definitionPattern, $convertLink) |
|||
} |
|||
|
|||
$lines -join "`n" |
|||
} |
|||
|
|||
$docsRoot = (Resolve-Path -LiteralPath $Path).ProviderPath.TrimEnd('\', '/') |
|||
$repositoryRoot = Split-Path -Path $docsRoot -Parent |
|||
$docsName = Split-Path -Path $docsRoot -Leaf |
|||
$destinationRoot = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath).TrimEnd('\', '/') |
|||
|
|||
# Refuse a destination that contains the documentation, such as the repository itself. |
|||
$separator = [IO.Path]::DirectorySeparatorChar |
|||
if (($docsRoot + $separator).StartsWith($destinationRoot + $separator, [StringComparison]::OrdinalIgnoreCase)) { |
|||
throw "The destination '$destinationRoot' contains the documentation in '$docsRoot'. Specify a clone of the wiki." |
|||
} |
|||
|
|||
# Collect the pages: Docs/README.md is the home page, and the contributor guide stays in the repository. |
|||
$pageNames = @{} |
|||
$pages = foreach ($file in Get-ChildItem -LiteralPath $docsRoot -Filter '*.md' -File -Recurse) { |
|||
$repositoryPath = $file.FullName.Substring($repositoryRoot.Length + 1) -replace '\\', '/' |
|||
if ($repositoryPath -match ('^{0}/Contributing(?:\.md$|/)' -f [regex]::Escape($docsName))) { |
|||
continue |
|||
} |
|||
|
|||
$name = if ($repositoryPath -eq "$docsName/README.md") { 'Home' } else { $file.BaseName } |
|||
if ($pageNames.Values -contains $name) { |
|||
throw "Two pages in '$docsRoot' would become the wiki page '$name'." |
|||
} |
|||
$pageNames[$repositoryPath] = $name |
|||
|
|||
$content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 |
|||
$title = if ($content -match '(?m)^#\s+(?<title>.+?)\s*$') { $Matches['title'] } else { $name -replace '-', ' ' } |
|||
[pscustomobject]@{ |
|||
Name = $name |
|||
Title = $title |
|||
Directory = $repositoryPath.Substring(0, $repositoryPath.LastIndexOf('/')) |
|||
Content = $content |
|||
} |
|||
} |
|||
|
|||
# Remove the old pages, but keep the history of the wiki. |
|||
if (Test-Path -LiteralPath $destinationRoot) { |
|||
foreach ($item in Get-ChildItem -LiteralPath $destinationRoot -Force | Where-Object -Property Name -NE -Value '.git') { |
|||
if ($PSCmdlet.ShouldProcess($item.FullName, 'Remove')) { |
|||
Remove-Item -LiteralPath $item.FullName -Recurse -Force |
|||
} |
|||
} |
|||
} elseif ($PSCmdlet.ShouldProcess($destinationRoot, 'Create folder')) { |
|||
New-Item -ItemType Directory -Path $destinationRoot | Out-Null |
|||
} |
|||
|
|||
$wikiPages = [ordered]@{} |
|||
foreach ($page in $pages) { |
|||
$content = [regex]::Replace($page.Content, '\A---\r?\n.*?\r?\n---[ \t]*(?:\r?\n|\z)\s*', '', 'Singleline') |
|||
$wikiPages[$page.Name] = Convert-MarkdownLink -Markdown $content -Directory $page.Directory |
|||
} |
|||
|
|||
# The sidebar links the other pages and the cmdlets, grouped like the cmdlet list of the home page. |
|||
$homePage = $pages | Where-Object -Property Name -EQ -Value 'Home' |
|||
$sidebar = New-Object -TypeName 'System.Collections.Generic.List[string]' |
|||
$sidebar.Add('### [Home](Home)') |
|||
$sidebar.Add('') |
|||
foreach ($page in $pages | Where-Object { $_.Directory -eq $docsName -and $_.Name -ne 'Home' } | Sort-Object -Property Name) { |
|||
$sidebar.Add(('- [{0}]({1})' -f $page.Title, $page.Name)) |
|||
} |
|||
if ($homePage) { |
|||
$section = $null |
|||
foreach ($line in ($homePage.Content -split '\r?\n')) { |
|||
if ($line -match '^##\s+(?<title>.+?)\s*$') { |
|||
$section = $Matches['title'] |
|||
if ($section -eq 'Cmdlets') { |
|||
$sidebar.Add('') |
|||
$sidebar.Add('### Cmdlets') |
|||
} |
|||
} elseif ($section -eq 'Cmdlets' -and $line -match '^###\s+(?<title>.+?)\s*$') { |
|||
$sidebar.Add('') |
|||
$sidebar.Add(('**{0}**' -f $Matches['title'])) |
|||
$sidebar.Add('') |
|||
} elseif ($section -eq 'Cmdlets' -and $line -match '^\|\s*\[(?<name>[^\]]+)\]\((?<url>[^)\s]+)\)') { |
|||
$sidebar.Add(('- [{0}]({1})' -f $Matches['name'], (ConvertTo-WikiLink -Url $Matches['url'] -Directory $docsName))) |
|||
} |
|||
} |
|||
} |
|||
$wikiPages['_Sidebar'] = $sidebar -join "`n" |
|||
|
|||
$wikiPages['_Footer'] = ('This wiki is generated from the [{0}]({1}/tree/{2}/{0}) folder of the repository. ' + |
|||
'To change a page, edit its file there; changes made in the wiki are overwritten.') -f $docsName, $RepositoryUrl.TrimEnd('/'), $Branch |
|||
|
|||
# The former wiki page How-to-install is linked from outside; it now points to the installation steps. |
|||
if ($homePage -and $homePage.Content -match '(?m)^##\s+Installation\s*$') { |
|||
$wikiPages['How-to-install'] = "# How to install`n`nThe installation steps are in the [Installation](Home#installation) section of the [Home](Home) page." |
|||
} |
|||
|
|||
$encoding = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList $false |
|||
foreach ($name in $wikiPages.Keys) { |
|||
$file = Join-Path -Path $destinationRoot -ChildPath "$name.md" |
|||
if ($PSCmdlet.ShouldProcess($file, 'Write wiki page')) { |
|||
[IO.File]::WriteAllText($file, $wikiPages[$name].TrimEnd() + "`n", $encoding) |
|||
} |
|||
} |
|||
@ -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." |
|||
} |
|||
@ -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." |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
--- |
|||
status: accepted |
|||
date: 2026-10-04 |
|||
last-verified: 2026-10-04 |
|||
owner: shared |
|||
source: maintainer decision after work package 4 |
|||
--- |
|||
|
|||
# Decision 11: CI and the wiki run on GitHub Actions |
|||
|
|||
- Choice: `.github/workflows/ci.yml` replaces AppVeyor. On pull requests and |
|||
pushes to `master`, the `build` job (`windows-2025`) builds the module in |
|||
Release, checks the docs against the build, and runs the Pester tests in |
|||
Windows PowerShell 5.1 and PowerShell 7. The `wiki` job converts `Docs` |
|||
with `.github/scripts/Export-WikiContent.ps1` and publishes the wiki from |
|||
`master` with the built-in token; on pull requests it lists the pages that |
|||
would change. `appveyor.yml` is removed. |
|||
- Rationale: The maintainer wants a browsable wiki without a second, hand- |
|||
written copy of the docs (the 2018 wiki went stale), and one CI platform |
|||
instead of two. Public repositories get Windows runners for free, and the |
|||
checks appear on the pull request without a third-party service. |
|||
- Consequences: `Docs` stays the only source (Decision 9); the wiki is a |
|||
generated mirror, and edits made in the wiki are overwritten. Only the |
|||
`wiki` job has `contents: write`; actions are pinned by commit SHA. Test |
|||
results appear in the job summary and as the `test-results` artifact. |
|||
- Rejected: a hand-maintained wiki next to `Docs`, publishing the wiki by |
|||
hand at release time, and keeping AppVeyor for build and tests. |
|||
@ -0,0 +1,228 @@ |
|||
<# |
|||
Tests .github\scripts\Export-WikiContent.ps1, which converts the documentation in Docs into the pages of the |
|||
GitHub wiki: with a small sample of Docs for the conversion rules, and with the real Docs for complete pages and |
|||
working links. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeAll { |
|||
$exportScript = Join-Path -Path $PSScriptRoot -ChildPath '..\.github\scripts\Export-WikiContent.ps1' |
|||
|
|||
function Get-GitHubAnchor { |
|||
# Returns the anchors that GitHub generates for the headings of a Markdown text. |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Markdown |
|||
) |
|||
|
|||
$text = [regex]::Replace($Markdown, '(?ms)^```.*?^```', '') |
|||
$seen = @{} |
|||
foreach ($match in [regex]::Matches($text, '(?m)^#{1,6}\s+(.+?)\s*#*\s*$')) { |
|||
$heading = $match.Groups[1].Value -replace '\[([^\]]*)\]\([^)]*\)', '$1' -replace '[`*]', '' |
|||
$anchor = [regex]::Replace($heading.Trim().ToLowerInvariant(), '[^\p{L}\p{Nd}\s_-]', '') -replace ' ', '-' |
|||
if ($seen.ContainsKey($anchor)) { |
|||
$seen[$anchor]++ |
|||
'{0}-{1}' -f $anchor, $seen[$anchor] |
|||
} else { |
|||
$seen[$anchor] = 0 |
|||
$anchor |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Export-WikiContent.ps1' { |
|||
Context 'When it converts a sample of Docs' { |
|||
BeforeAll { |
|||
$repositoryPath = Join-Path -Path $TestDrive -ChildPath 'repository' |
|||
$docsPath = Join-Path -Path $repositoryPath -ChildPath 'Docs' |
|||
$wikiPath = Join-Path -Path $TestDrive -ChildPath 'wiki' |
|||
foreach ($folder in "$docsPath\Cmdlets", "$docsPath\Contributing", "$wikiPath\.git") { |
|||
New-Item -ItemType Directory -Path $folder -Force | Out-Null |
|||
} |
|||
|
|||
# A clone of the wiki with pages that Docs doesn't have |
|||
Set-Content -LiteralPath "$wikiPath\.git\config" -Value '[core]' |
|||
Set-Content -LiteralPath "$wikiPath\Version-History.textile" -Value '* 4.2.4' |
|||
Set-Content -LiteralPath "$wikiPath\Old-Page.md" -Value '# Old page' |
|||
|
|||
Set-Content -LiteralPath "$repositoryPath\CHANGELOG.md" -Value '# Changelog' |
|||
Set-Content -LiteralPath "$docsPath\Contributing.md" -Value '# Contributing' |
|||
Set-Content -LiteralPath "$docsPath\Contributing\01-Getting-Started.md" -Value '# Get started' |
|||
Set-Content -LiteralPath "$docsPath\Concepts.md" -Value "# Concepts`n`n## Rights`n`nSee [Home](README.md)." |
|||
Set-Content -LiteralPath "$docsPath\README.md" -Value @' |
|||
# Thing |
|||
|
|||
[Concepts](Concepts.md#rights), [Get-Thing](Cmdlets/Get-Thing.md), and [`Set-Thing`](Cmdlets/Set-Thing.md). |
|||
[Changelog](../CHANGELOG.md), [guide](Contributing.md), and [first steps](Contributing/01-Getting-Started.md). |
|||
[Example site](https://example.com/page.md) and [cmdlets](#cmdlets). |
|||
Code keeps its links: `[Concepts](Concepts.md)`. |
|||
|
|||
```powershell |
|||
# [Concepts](Concepts.md) |
|||
``` |
|||
|
|||
## Installation |
|||
|
|||
Install the module. |
|||
|
|||
## Cmdlets |
|||
|
|||
### Getting |
|||
|
|||
| Cmdlet | Description | |
|||
| --- | --- | |
|||
| [Get-Thing](Cmdlets/Get-Thing.md) | Gets a thing. | |
|||
|
|||
### Setting |
|||
|
|||
| Cmdlet | Description | |
|||
| --- | --- | |
|||
| [Set-Thing](Cmdlets/Set-Thing.md) | Sets a thing. | |
|||
'@ |
|||
$metadata = "---`nexternal help file: Thing.dll-Help.xml`nonline version: https://example.com`nschema: 2.0.0`n---`n`n" |
|||
Set-Content -LiteralPath "$docsPath\Cmdlets\Get-Thing.md" -Value ($metadata + |
|||
"# Get-Thing`n`nSee [rights](../Concepts.md#rights), [Set-Thing](Set-Thing.md), and [home](../README.md).") |
|||
Set-Content -LiteralPath "$docsPath\Cmdlets\Set-Thing.md" -Value ($metadata + |
|||
"# Set-Thing`n`nSee [Get-Thing](Get-Thing.md).") |
|||
|
|||
& $exportScript -Path $docsPath -DestinationPath $wikiPath -RepositoryUrl 'https://github.com/contoso/Thing' -Branch 'main' |
|||
|
|||
$homePage = Get-Content -LiteralPath "$wikiPath\Home.md" -Raw |
|||
$sidebar = Get-Content -LiteralPath "$wikiPath\_Sidebar.md" -Raw |
|||
} |
|||
|
|||
It 'Should write Home, Concepts, the cmdlet pages, How-to-install, the sidebar, and the footer' { |
|||
$expected = 'Home.md', 'Concepts.md', 'Get-Thing.md', 'Set-Thing.md', 'How-to-install.md', '_Sidebar.md', '_Footer.md' |
|||
|
|||
((Get-ChildItem -LiteralPath $wikiPath -File).Name | Sort-Object) -join ', ' | |
|||
Should -BeExactly (($expected | Sort-Object) -join ', ') |
|||
} |
|||
|
|||
It 'Should keep the .git folder and remove the pages that Docs does not have' { |
|||
"$wikiPath\.git\config" | Should -Exist |
|||
"$wikiPath\Version-History.textile" | Should -Not -Exist |
|||
"$wikiPath\Old-Page.md" | Should -Not -Exist |
|||
} |
|||
|
|||
It 'Should not publish the contributor guide' { |
|||
Get-ChildItem -LiteralPath $wikiPath -Recurse -File -Filter '*Get*Started*' | Should -BeNullOrEmpty |
|||
"$wikiPath\Contributing.md" | Should -Not -Exist |
|||
} |
|||
|
|||
It 'Should convert <Link> on <Page> into <Expected>' -ForEach @( |
|||
@{ Page = 'Home'; Link = '[Concepts](Concepts.md#rights)'; Expected = '[Concepts](Concepts#rights)' } |
|||
@{ Page = 'Home'; Link = '[Get-Thing](Cmdlets/Get-Thing.md)'; Expected = '[Get-Thing](Get-Thing)' } |
|||
@{ Page = 'Home'; Link = '[`Set-Thing`](Cmdlets/Set-Thing.md)'; Expected = '[`Set-Thing`](Set-Thing)' } |
|||
@{ Page = 'Home'; Link = '[Changelog](../CHANGELOG.md)'; Expected = '[Changelog](https://github.com/contoso/Thing/blob/main/CHANGELOG.md)' } |
|||
@{ Page = 'Home'; Link = '[guide](Contributing.md)'; Expected = '[guide](https://github.com/contoso/Thing/blob/main/Docs/Contributing.md)' } |
|||
@{ Page = 'Home'; Link = '[first steps](Contributing/01-Getting-Started.md)'; Expected = '[first steps](https://github.com/contoso/Thing/blob/main/Docs/Contributing/01-Getting-Started.md)' } |
|||
@{ Page = 'Home'; Link = '[Example site](https://example.com/page.md)'; Expected = '[Example site](https://example.com/page.md)' } |
|||
@{ Page = 'Home'; Link = '[cmdlets](#cmdlets)'; Expected = '[cmdlets](#cmdlets)' } |
|||
@{ Page = 'Concepts'; Link = '[Home](README.md)'; Expected = '[Home](Home)' } |
|||
@{ Page = 'Get-Thing'; Link = '[rights](../Concepts.md#rights)'; Expected = '[rights](Concepts#rights)' } |
|||
@{ Page = 'Get-Thing'; Link = '[Set-Thing](Set-Thing.md)'; Expected = '[Set-Thing](Set-Thing)' } |
|||
@{ Page = 'Get-Thing'; Link = '[home](../README.md)'; Expected = '[home](Home)' } |
|||
) { |
|||
$content = Get-Content -LiteralPath "$wikiPath\$Page.md" -Raw |
|||
|
|||
$content | Should -Match ([regex]::Escape($Expected)) |
|||
if ($Link -ne $Expected) { |
|||
$content | Should -Not -Match ([regex]::Escape($Link)) |
|||
} |
|||
} |
|||
|
|||
It 'Should keep links in code unchanged' { |
|||
$homePage | Should -Match ([regex]::Escape('`[Concepts](Concepts.md)`')) |
|||
$homePage | Should -Match ([regex]::Escape('# [Concepts](Concepts.md)')) |
|||
} |
|||
|
|||
It 'Should remove the platyPS metadata from the cmdlet pages' { |
|||
Get-Content -LiteralPath "$wikiPath\Get-Thing.md" -TotalCount 1 | Should -BeExactly '# Get-Thing' |
|||
} |
|||
|
|||
It 'Should link Home and the other pages in the sidebar' { |
|||
$sidebar | Should -Match ([regex]::Escape('[Home](Home)')) |
|||
$sidebar | Should -Match ([regex]::Escape('[Concepts](Concepts)')) |
|||
} |
|||
|
|||
It 'Should list the cmdlets in the sidebar under the groups of Docs/README.md' { |
|||
$sidebar | Should -Match '(?s)Getting.*\[Get-Thing\]\(Get-Thing\).*Setting.*\[Set-Thing\]\(Set-Thing\)' |
|||
} |
|||
|
|||
It 'Should say in the footer that the wiki is generated from Docs' { |
|||
Get-Content -LiteralPath "$wikiPath\_Footer.md" -Raw | |
|||
Should -Match ([regex]::Escape('(https://github.com/contoso/Thing/tree/main/Docs)')) |
|||
} |
|||
|
|||
It 'Should keep the address of the former page How-to-install' { |
|||
Get-Content -LiteralPath "$wikiPath\How-to-install.md" -Raw | Should -Match ([regex]::Escape('(Home#installation)')) |
|||
} |
|||
} |
|||
|
|||
Context 'When it converts the documentation of the repository' { |
|||
BeforeAll { |
|||
$repositoryPath = Join-Path -Path $PSScriptRoot -ChildPath '..' |
|||
$docsPath = Join-Path -Path $repositoryPath -ChildPath 'Docs' |
|||
$wikiPath = Join-Path -Path $TestDrive -ChildPath 'wiki-of-the-repository' |
|||
|
|||
& $exportScript -Path $docsPath -DestinationPath $wikiPath |
|||
|
|||
$pages = Get-ChildItem -LiteralPath $wikiPath -Filter '*.md' |
|||
$cmdletNames = (Get-ChildItem -LiteralPath (Join-Path -Path $docsPath -ChildPath 'Cmdlets') -Filter '*.md').BaseName |
|||
} |
|||
|
|||
It 'Should write a page for every page in Docs except the contributor guide, and one for every cmdlet' { |
|||
$topPages = (Get-ChildItem -LiteralPath $docsPath -Filter '*.md' | |
|||
Where-Object -Property Name -NotIn -Value 'README.md', 'Contributing.md').BaseName |
|||
$expected = @('Home', 'How-to-install', '_Sidebar', '_Footer') + $topPages + $cmdletNames |
|||
|
|||
($pages.BaseName | Sort-Object) -join ', ' | Should -BeExactly (($expected | Sort-Object) -join ', ') |
|||
} |
|||
|
|||
It 'Should keep the page name Version-History, which the release notes of 4.2.4 and 4.2.6 link to' { |
|||
Join-Path -Path $wikiPath -ChildPath 'Version-History.md' | Should -Exist |
|||
} |
|||
|
|||
It 'Should list every cmdlet in the sidebar' { |
|||
$sidebar = Get-Content -LiteralPath (Join-Path -Path $wikiPath -ChildPath '_Sidebar.md') -Raw |
|||
|
|||
$cmdletNames | Where-Object -FilterScript { $sidebar -notmatch ('\]\({0}\)' -f [regex]::Escape($_)) } | |
|||
Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Should link only to existing wiki pages, their anchors, and existing files of the repository' { |
|||
$anchors = @{} |
|||
foreach ($page in $pages) { |
|||
$anchors[$page.BaseName] = @(Get-GitHubAnchor -Markdown (Get-Content -LiteralPath $page.FullName -Raw)) |
|||
} |
|||
|
|||
$brokenLinks = foreach ($page in $pages) { |
|||
$text = [regex]::Replace((Get-Content -LiteralPath $page.FullName -Raw), '(?ms)^```.*?^```', '') |
|||
$text = [regex]::Replace($text, '`[^`\n]+`', '') |
|||
foreach ($match in [regex]::Matches($text, '\]\((?<url>[^)\s]+)\)')) { |
|||
$url = $match.Groups['url'].Value |
|||
if ($url -match '^https://github\.com/raandree/NTFSSecurity/(?:blob|tree)/master/(?<path>[^#]+)') { |
|||
if (-not (Test-Path -LiteralPath (Join-Path -Path $repositoryPath -ChildPath $Matches['path']))) { |
|||
'{0}: {1}' -f $page.BaseName, $url |
|||
} |
|||
} elseif ($url -notmatch '^[a-z]+:') { |
|||
$target, $anchor = $url -split '#', 2 |
|||
if (-not $target) { |
|||
$target = $page.BaseName |
|||
} |
|||
if (-not $anchors.ContainsKey($target) -or ($anchor -and $anchors[$target] -notcontains $anchor)) { |
|||
'{0}: {1}' -f $page.BaseName, $url |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
$brokenLinks | Should -BeNullOrEmpty |
|||
} |
|||
} |
|||
} |
|||
@ -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." |
|||
} |
|||
Loading…
Reference in new issue