Browse Source

Merge pull request #96 from raandree/ai/github-actions

ci: move CI from AppVeyor to GitHub Actions and publish the docs to the wiki
pull/97/head
Raimund Andrée 1 week ago
committed by GitHub
parent
commit
4f9f7ccf58
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 279
      .github/scripts/Export-WikiContent.ps1
  2. 79
      .github/scripts/Invoke-Tests.ps1
  3. 181
      .github/workflows/ci.yml
  4. 46
      .memory-bank/activeContext.md
  5. 5
      .memory-bank/decisions/0006-ci-checks-docs-against-build.md
  6. 7
      .memory-bank/decisions/0008-commit-generated-help.md
  7. 13
      .memory-bank/decisions/0009-docs-on-github.md
  8. 27
      .memory-bank/decisions/0011-github-actions.md
  9. 43
      .memory-bank/progress.md
  10. 2
      .memory-bank/projectbrief.md
  11. 28
      .memory-bank/systemPatterns.md
  12. 71
      .memory-bank/techContext.md
  13. 6
      CHANGELOG.md
  14. 48
      Docs/Contributing/02-Writing.md
  15. 4
      Docs/Version-History.md
  16. 4
      README.md
  17. 228
      Tests/Wiki.Tests.ps1
  18. 86
      appveyor.yml

279
.github/scripts/Export-WikiContent.ps1

@ -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)
}
}

79
.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."
}

181
.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."
}

46
.memory-bank/activeContext.md

@ -9,37 +9,31 @@ source: current task evidence
## Current focus
Work packages 3 and 4 are PR-ready and committed locally, not pushed:
`ai/docs-on-github` (docs on GitHub, wiki retired, Decision 9) and
`ai/manifest-version` (valid manifest, version 5.0.0, Decision 10),
stacked on it. The maintainer pushes both and opens the PRs; the PR
descriptions are in the session files. Merge the docs PR first with a
merge commit.
PRs #94 (docs on GitHub) and #95 (manifest and version 5.0.0, stacked on
PR #94) are open and build on AppVeyor. The move to GitHub Actions (CI and
a wiki generated from `Docs`, Decision 11) is PR-ready on the local branch
`ai/github-actions`, stacked on #95; the maintainer pushes it and opens the
PR. Merge order: #94, #95 (merge commits), then the GitHub Actions PR.
## Evidence
- All 256 relative links in `Docs`, `README.md`, and `CHANGELOG.md` resolve,
including 5 anchors checked against GitHub's slug rules; MarkdownLinkCheck
0.2.0 (CI step 02) finds 0 broken links in `Docs` in Windows PowerShell
5.1; markdownlint reports 0 issues in the changed pages.
- The documented manual install (`Unblock-File`, `Expand-Archive` into
`$env:ProgramFiles\WindowsPowerShell\Modules`) was tested with the 4.2.6
zip in a `$env:TEMP` sandbox: the module imports under `RemoteSigned`.
`Expand-Archive` doesn't pass the download mark on; File Explorer's zip
handler does, and the import then fails.
- The wiki stopped at 4.2.4; the Gallery has 4.2.5 (2019-07-11) and 4.2.6
(2019-07-12), whose notes were reconstructed from `4.2.4..4.2.6`.
- `Wiki.Tests.ps1` failed 25 of 25 before the exporter existed and passes
25 of 25 in Windows PowerShell 5.1 and PowerShell 7.6.1. A negative
control (broken anchor, missing page, missing file) reports all three.
- A dry run against a clone of the live wiki adds 41 pages, changes Home
and How-to-install, and deletes the stale `Cmdlets.md` and
`Version-History.textile`; nothing was pushed.
- `Invoke-Tests.ps1`, as CI calls it, passed locally against a Release
build: Windows PowerShell 5.1 253 of 253; PowerShell 7 217 passed and 36
skipped (`Get-Help -Online` runs only in Windows PowerShell).
- actionlint 1.7.12 reports nothing for `.github/workflows/ci.yml`. The
runner image `windows-2025` has Visual Studio 2022 MSBuild 17.14, NuGet,
the GitHub CLI, and PowerShell 7.6; `master` has no branch protection.
- The local branch `ai/read-the-docs` keeps the dropped strict-build work
(`886c874`, `325ec76`); delete it once it is no longer wanted.
- Work package 4 test first: against the previous build, all 10 new tests
failed for the expected reasons; after the change, the AppVeyor test
script run locally in Windows PowerShell 5.1 passed (228 of 228 Pester
tests), and the new tests pass in PowerShell 7.6.1 (10 of 10).
- The local Release build (`packages\`, `bin\`, `obj\`) is deleted after
the work; rebuild from the NuGet cache (`techContext.md`).
## Next step
After the maintainer pushes: read the AppVeyor results of both PRs through
the REST API. Work package 5 (code defects) starts only after the
maintainer's go-ahead.
After the maintainer pushes: read the AppVeyor results of #94 and #95 and
the first GitHub Actions run with `gh run view`. Work package 5 (code
defects) starts only after the maintainer's go-ahead.

5
.memory-bank/decisions/0006-ci-checks-docs-against-build.md

@ -1,14 +1,15 @@
---
status: accepted
date: 2026-10-02
last-verified: 2026-10-02
last-verified: 2026-10-04
owner: shared
source: PR #91 (moved from systemPatterns.md)
---
# Decision 6: CI checks the docs against a build of the source
- Choice: `appveyor.yml` builds `NTFSSecurity.csproj` and runs
- Choice: The CI workflow (`.github/workflows/ci.yml`, Decision 11; before
that `appveyor.yml`) builds `NTFSSecurity.csproj` and runs
`Update-MarkdownHelp` against `NTFSSecurity\bin\Release`, not against the
module from the PowerShell Gallery.
- Rationale: Checking against the last release fails for every unreleased

7
.memory-bank/decisions/0008-commit-generated-help.md

@ -1,7 +1,7 @@
---
status: accepted
date: 2026-10-02
last-verified: 2026-10-02
last-verified: 2026-10-04
owner: shared
source: maintainer choice in work package 2 (option A)
---
@ -11,8 +11,9 @@ source: maintainer choice in work package 2 (option A)
- Choice: `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml` is generated from
`Docs/Cmdlets` with `New-ExternalHelp` (platyPS 0.14.2, Windows
PowerShell 5.1) and committed. `NTFSSecurity.csproj` copies it to the
output as `Content`, the manifest `FileList` lists it, and `appveyor.yml`
regenerates it and fails on `git status --porcelain -- NTFSSecurity/en-US`.
output as `Content`, the manifest `FileList` lists it, and the CI workflow
(`.github/workflows/ci.yml`) regenerates it and fails on
`git status --porcelain -- NTFSSecurity/en-US`.
- Rationale: Releases are built locally in Visual Studio (Debug, written to
`C:\Program Files\WindowsPowerShell\Modules\NTFSSecurity`) and published
by hand with `Publish-Module`. A committed file ships with every build

13
.memory-bank/decisions/0009-docs-on-github.md

@ -10,16 +10,17 @@ source: maintainer decision in work package 3
- Choice: The documentation lives in `Docs` and `README.md`, and GitHub
renders it. There is no documentation site: the Read the Docs and MkDocs
configuration is removed. The GitHub wiki is retired: its version history
and installation steps moved to `Docs/Version-History.md` and
`Docs/README.md`, and the maintainer turns the wiki off.
configuration is removed. The wiki's version history and installation
steps moved to `Docs/Version-History.md` and `Docs/README.md`; the wiki
itself is now generated from `Docs` (Decision 11) instead of being turned
off.
- Rationale: One source of truth that is versioned with the code, reviewed
in pull requests, and checked by AppVeyor. The Read the Docs project
in pull requests, and checked by CI. The Read the Docs project
`ntfssecurity` belongs to `Sup3rlativ3` and points to a fork that no
longer exists; a wiki is edited outside pull requests and CI.
longer exists; a hand-written wiki is edited outside pull requests and CI.
- Consequences: `online version` links stay on GitHub (Decision 4). Section
anchors follow GitHub's rules. `Docs/index.md` became `Docs/README.md`, so
that GitHub shows it when you open the `Docs` folder.
- Rejected: taking over or re-importing the Read the Docs project with a
strict MkDocs build (prepared on the local branch `ai/read-the-docs`, not
merged), and publishing `Docs` to the wiki.
merged).

27
.memory-bank/decisions/0011-github-actions.md

@ -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.

43
.memory-bank/progress.md

@ -13,8 +13,9 @@ PRs #91, #92, and #93 are merged. The documentation matches the cmdlets at
`master`, and the module ships the help file generated from it
(`en-US\NTFSSecurity.dll-Help.xml`). Otherwise the module source differs
from the 4.2.6 release only by the `Remove-Item2 -PassThru` rename and
`CompatiblePSEditions`. Work packages 3 (`ai/docs-on-github`) and 4
(`ai/manifest-version`, stacked on 3) are PR-ready locally.
`CompatiblePSEditions`. Open PRs: #94 (work package 3) and #95 (work package
4, stacked on #94); the move to GitHub Actions is PR-ready locally on
`ai/github-actions`, stacked on #95.
## Recent milestones
@ -46,6 +47,13 @@ from the 4.2.6 release only by the `Remove-Item2 -PassThru` rename and
PowerShell 5.1, passed: no docs drift, 0 broken links, current help file,
228 of 228 Pester tests. `Test-ModuleManifest` passes in Windows
PowerShell 5.1 and PowerShell 7.6.1.
- 2026-10-04: The maintainer opened #94 and #95, then chose to keep the wiki,
generated from `Docs`, and to move CI from AppVeyor to GitHub Actions in
one PR (Decision 11): committed locally on `ai/github-actions`, stacked on
#95. `Wiki.Tests.ps1` failed 25 of 25 before the exporter existed and
passes in both editions; `Invoke-Tests.ps1` passed locally in Windows
PowerShell 5.1 (253 of 253) and PowerShell 7 (217 passed, 36 skipped);
actionlint found nothing.
## Stable capabilities
@ -64,15 +72,15 @@ next package starts only after the maintainer's go-ahead.
1. Housekeeping: done (#92).
2. Ship help: done (#93).
3. Docs on GitHub (was: Read the Docs), PR-ready on `ai/docs-on-github`:
Read the Docs and MkDocs configuration removed, `Docs/index.md` renamed
to `Docs/README.md`, the wiki's version history and install steps moved
into `Docs` (with reconstructed notes for 4.2.5 and 4.2.6), contributor
guide updated. After the merge, the maintainer turns the wiki off, points
the notes of releases 4.2.4 and 4.2.6 to `Docs/Version-History.md`, and
may ask `Sup3rlativ3` to delete the Read the Docs project.
4. Manifest and version, PR-ready on `ai/manifest-version`, stacked on
`ai/docs-on-github` (merge that PR first with a merge commit).
3. Docs on GitHub (was: Read the Docs), PR #94: Read the Docs and MkDocs
configuration removed, `Docs/index.md` renamed to `Docs/README.md`, the
wiki's version history and install steps moved into `Docs` (with
reconstructed notes for 4.2.5 and 4.2.6), contributor guide updated. Keep
the wiki on: the GitHub Actions PR generates it from `Docs`, which also
keeps the release-note links to `wiki/Version-History` working.
`Sup3rlativ3` may be asked to delete the Read the Docs project.
4. Manifest and version, PR #95, stacked on #94 (merge #94 first with a
merge commit).
Maintainer decisions: `PowerShellVersion` 5.1, `DotNetFrameworkVersion`
4.5.2, `RootModule`; version 5.0.0; `[Alias('PassThur')]` on
`Remove-Item2 -PassThru`, listed under Deprecated; `NTFSSecurity`,
@ -84,12 +92,19 @@ next package starts only after the maintainer's go-ahead.
the whole output folder (`.pdb`, `.xml`,
`System.Management.Automation.dll`), and the removed
`NTFSSecurity-Help.xml` would ship again.
4b. GitHub Actions for CI and the wiki (Decision 11), PR-ready on
`ai/github-actions`, stacked on #95 (merge #94 and #95 first with merge
commits). AppVeyor reports a failure on this PR because it removes
`appveyor.yml`; that's expected. After the merge: delete the AppVeyor
project `raandree/ntfssecurity` and revoke AppVeyor's GitHub access,
check the first wiki publication, and consider **Restrict editing to
collaborators only** for the wiki.
5. Code defects, listed below: `review: on`, one PR per group, regression
test first. Pester 5 tests import `NTFSSecurity\bin\Release`, run in a
`$env:TEMP` sandbox and in `appveyor.yml` (pattern:
`$env:TEMP` sandbox and in the CI workflow (pattern:
`Tests\Help.Tests.ps1`), and skip elevated cases when not elevated;
check whether AppVeyor runs elevated. Each fix updates its cmdlet page
and `CHANGELOG.md`.
check whether the GitHub Actions Windows runner runs elevated. Each fix
updates its cmdlet page and `CHANGELOG.md`.
### Code defects (work package 5)

2
.memory-bank/projectbrief.md

@ -38,7 +38,7 @@ Source: `README.md`, `NTFSSecurity/NTFSSecurity.psd1`.
1. Every exported cmdlet has an accurate platyPS page in `Docs/Cmdlets`.
2. `Update-MarkdownHelp` against the module produces no parameter drift
(the check in `appveyor.yml`).
(the check in `.github/workflows/ci.yml`).
3. The module imports in Windows PowerShell 5.1 and PowerShell 7
(`CompatiblePSEditions = 'Core', 'Desktop'`).
4. Further release criteria: To confirm.

28
.memory-bank/systemPatterns.md

@ -57,6 +57,7 @@ Each Decision record is a file in `decisions/`; read only the relevant ones.
| 8 | [Commit the generated help file and check it in CI](decisions/0008-commit-generated-help.md) |
| 9 | [Keep the documentation on GitHub](decisions/0009-docs-on-github.md) |
| 10 | [One version for the manifest, assemblies, and changelog](decisions/0010-one-version.md) |
| 11 | [CI and the wiki run on GitHub Actions](decisions/0011-github-actions.md) |
## Patterns
@ -64,10 +65,16 @@ Each Decision record is a file in `decisions/`; read only the relevant ones.
- Run platyPS in Windows PowerShell 5.1 against a module build; a copy of
`Docs/Cmdlets` must round-trip through `Update-MarkdownHelp` unchanged.
- GitHub renders the docs (Decision 9). AppVeyor's link check covers only
relative links in `Docs` and ignores anchors, so check anchors against
GitHub's slug rules (lowercase, punctuation removed, spaces to hyphens)
and the links in `README.md` and `CHANGELOG.md` separately.
- GitHub renders the docs (Decision 9), and CI publishes them to the wiki
(Decision 11). The MarkdownLinkCheck step covers only relative links in
`Docs` and ignores anchors; `Tests\Wiki.Tests.ps1` checks every link of
the generated wiki, including anchors (GitHub's slug rules: lowercase,
punctuation removed, spaces to hyphens). Check the links in `README.md`
and `CHANGELOG.md` separately.
- The wiki is generated: edit `Docs`, never the wiki.
`Export-WikiContent.ps1` names a page after its file (`Docs/README.md`
becomes Home), rewrites links, and builds the sidebar from the cmdlet
groups of `Docs/README.md`; a cmdlet missing there fails `Wiki.Tests.ps1`.
- platyPS rewrites non-ASCII punctuation such as em dashes; keep cmdlet pages
ASCII-only.
- In cmdlet pages, end a sentence with a link: platyPS renders a link as
@ -78,15 +85,16 @@ Each Decision record is a file in `decisions/`; read only the relevant ones.
### Testing the module
- Pester 5 tests in `Tests/*.Tests.ps1` import
`NTFSSecurity\bin\Release\NTFSSecurity.psd1` and run in Windows
PowerShell 5.1 (as in AppVeyor); PowerShell 7 runs are a secondary check.
`NTFSSecurity\bin\Release\NTFSSecurity.psd1`; CI runs them in Windows
PowerShell 5.1 and in PowerShell 7 (Decision 11).
- `Get-Help -Online` is tested with the internal test hook
`BypassOnlineHelpRetrieval`, which returns the URI instead of opening a
browser. In PowerShell 7 the hook also skips the help file, so that test
runs only in Windows PowerShell; PowerShell 7 resolves the same URI.
- Report Pester 5 results to AppVeyor through the build worker API, not as
an uploaded NUnit file: the NUnit import files each test under every
enclosing block (870 entries for 218 tests in build 54834154).
runs only in Windows PowerShell (36 skipped tests in PowerShell 7);
PowerShell 7 resolves the same URI.
- `.github/scripts/Invoke-Tests.ps1` runs Pester for CI: the counts and the
failed tests go to the job summary, the NUnit file to the `test-results`
artifact, and it fails on failed test files too (`Result -ne 'Passed'`).
- `Tests\Manifest.Tests.ps1` checks the built manifest: `Test-ModuleManifest`
without errors or warnings, exactly 36 cmdlets, and one version
(Decision 10). Add a new cmdlet to `CmdletsToExport` and to the expected

71
.memory-bank/techContext.md

@ -19,15 +19,17 @@ source: repository evidence
- Module: `NTFSSecurity.psd1` loads `NTFSSecurity.psm1` (aliases `dir2`,
`gi2`, `rm2`, `del2`), `NTFSSecurity.Init.ps1` (Add-Type of the helper
assemblies, prepends `NTFSSecurity.format.ps1xml`), and `NTFSSecurity.dll`.
- Documentation: Markdown in `Docs` and `README.md`, rendered by GitHub; no
documentation site and no wiki (Decision 9). Cmdlet pages are platyPS
0.14 markdown (schema 2.0.0) in `Docs/Cmdlets`.
- Documentation: Markdown in `Docs` and `README.md`, rendered by GitHub and
published to the wiki by CI; no documentation site (Decisions 9 and 11).
Cmdlet pages are platyPS 0.14 markdown (schema 2.0.0) in `Docs/Cmdlets`.
- Help: `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml`, generated from
`Docs/Cmdlets` and committed (Decision 8).
- Tests: Pester 5 tests in `Tests` against the Release build:
`Help.Tests.ps1` (help of every cmdlet), `Manifest.Tests.ps1` (manifest
and versions, Decision 10), and `Remove-Item2.Tests.ps1` (`-PassThur`
alias).
- Tests: Pester 5 tests in `Tests`: `Help.Tests.ps1` (help of every
cmdlet), `Manifest.Tests.ps1` (manifest and versions, Decision 10), and
`Remove-Item2.Tests.ps1` (`-PassThur` alias) against the Release build;
`Wiki.Tests.ps1` (wiki conversion) without a build.
- CI: GitHub Actions, `.github/workflows/ci.yml` with the scripts in
`.github/scripts` (Decision 11).
## Environment
@ -79,12 +81,13 @@ source: repository evidence
(`.pdb`, `AlphaFS.xml`, 7 MB `System.Management.Automation.dll`), and the
published manifest differs from the tag only by `ModuleVersion` (tags
carry the previous version). GitHub releases attach `NTFSSecurity.zip`.
- CI: AppVeyor project `raandree/ntfssecurity` builds branches and pull
requests. The Read the Docs project `ntfssecurity` (maintainer
`Sup3rlativ3`) and a second AppVeyor project are attached to the fork
`Sup3rlativ3/NTFSSecurity`, which no longer exists (GitHub 404,
2026-10-04). That site still serves pages from 2020 and isn't used
(Decision 9).
- CI: GitHub Actions on pull requests and pushes to `master` (Decision 11).
The AppVeyor project `raandree/ntfssecurity` builds until the maintainer
deletes it after the GitHub Actions PR is merged. The Read the Docs
project `ntfssecurity` (maintainer `Sup3rlativ3`) and a second AppVeyor
project are attached to the fork `Sup3rlativ3/NTFSSecurity`, which no
longer exists (GitHub 404, 2026-10-04). That site still serves pages from
2020 and isn't used (Decision 9).
- `Get-FileHash2` fails in PowerShell 7; all other cmdlets passed a smoke
test in PowerShell 7.6.
- `CHANGELOG.md` lists user-visible changes only; CI and build-only changes
@ -102,20 +105,28 @@ source: repository evidence
## Validation
- CI (`appveyor.yml`, image Visual Studio 2022): restore `packages.config`
per project plus `Microsoft.NETFramework.ReferenceAssemblies.net452`
1.0.3, build `NTFSSecurity.csproj` in Release with
`TargetFrameworkRootPath`/`FrameworkPathOverride`, import
`NTFSSecurity\bin\Release\NTFSSecurity.psd1`, then: 01 run
`Update-MarkdownHelp` and fail on `git diff -- Docs/Cmdlets`; 02
- CI (`.github/workflows/ci.yml`): job `build` on `windows-2025` installs
platyPS 0.14.2, MarkdownLinkCheck 0.2.0, and Pester 5.7.1 for all users,
restores `packages.config` per project plus
`Microsoft.NETFramework.ReferenceAssemblies.net452` 1.0.3, builds
`NTFSSecurity.csproj` in Release with the MSBuild that `vswhere` finds,
then: 01 `Update-MarkdownHelp` and fail on `git diff -- Docs/Cmdlets`; 02
`Get-MarkdownLink -BrokenOnly`; 03 regenerate the help file and fail on
`git status --porcelain -- NTFSSecurity/en-US`; 04 Pester 5.7.1 on
`Tests`, each result reported once through the build worker API
(`POST $env:APPVEYOR_API_URL/api/tests/batch`).
- AppVeyor REST API (public, no token): build
`git status --porcelain -- NTFSSecurity/en-US`; 04 `Invoke-Tests.ps1` in
Windows PowerShell 5.1 and in PowerShell 7. Job `wiki` on `ubuntu-latest`
clones the wiki (`gh auth setup-git` with the built-in token), runs
`Export-WikiContent.ps1`, lists the changed pages in the job summary, and
publishes from `master` only. Actions are pinned by commit SHA:
`actions/checkout` v7.0.1, `actions/upload-artifact` v7.0.1.
- Read CI runs with `gh run list --repo raandree/NTFSSecurity --workflow
ci.yml` and `gh run view <id> --log-failed` (read-only). While AppVeyor
still builds: `api/projects/raandree/ntfssecurity/history`, build
`api/projects/raandree/ntfssecurity/builds/<buildId>`, job log
`api/buildjobs/<jobId>/log` (bytes; decode as UTF-8), and test list
`api/buildjobs/<jobId>/tests`.
`api/buildjobs/<jobId>/log` (public, no token).
- Workflow lint: actionlint (download the release zip into `$env:TEMP` and
check its SHA-256 against the checksum file); PowerShell steps check
`$LASTEXITCODE` after every native command, because GitHub checks only
the last one.
- Run platyPS in Windows PowerShell 5.1 to avoid PowerShell 7.4+
`-ProgressAction` noise.
- Placeholder check: no `{{` left in `Docs/Cmdlets/*.md`.
@ -127,8 +138,8 @@ source: repository evidence
path. A run without `bin\Release\en-US` must fail.
- Markdown lint: `npx markdownlint-cli2` with `MD013` limited to prose
(tables, code, and headings excluded) on the conceptual pages.
- YAML: `ConvertFrom-Yaml` (powershell-yaml) on `appveyor.yml`.
- Links: AppVeyor step 02 (MarkdownLinkCheck 0.2.0) checks only relative
links in `Docs`; it strips anchors and skips absolute URLs. Check anchors
against GitHub's heading slugs, and the links in `README.md` and
`CHANGELOG.md`, with a script.
- YAML: `ConvertFrom-Yaml` (powershell-yaml) on `.github/workflows/ci.yml`.
- Links: the CI step 02 (MarkdownLinkCheck 0.2.0) checks only relative
links in `Docs`; it strips anchors and skips absolute URLs.
`Wiki.Tests.ps1` checks the wiki links with their anchors; check the
links in `README.md` and `CHANGELOG.md` with a script.

6
CHANGELOG.md

@ -8,6 +8,12 @@ The format is based on
## [Unreleased]
### Added
- Publish the documentation in the
[wiki](https://github.com/raandree/NTFSSecurity/wiki), generated from the
`Docs` folder after every change, with a sidebar that lists all cmdlets
## [5.0.0] - 2026-10-04
### Changed

48
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

4
Docs/Version-History.md

@ -1,8 +1,8 @@
# Version history
This page lists the changes in NTFSSecurity 4.2.6 and earlier. It replaces
the version history in the former GitHub wiki. For the changes since 4.2.6,
see the [changelog](../CHANGELOG.md).
the hand-written version history that the GitHub wiki kept until 2018. For the
changes since 4.2.6, see the [changelog](../CHANGELOG.md).
## 4.2.5 and 4.2.6

4
README.md

@ -43,6 +43,10 @@ Get-ChildItem2 -Path C:\Data -Recurse | Get-NTFSAccess -ExcludeInherited
## Documentation
Read the documentation in the
[wiki](https://github.com/raandree/NTFSSecurity/wiki), which is generated
from the `Docs` folder, or in the folder itself:
- [Overview](Docs/README.md): features, requirements, and the list of cmdlets
- [Concepts](Docs/Concepts.md): security descriptors, access rights,
inheritance, privileges, long paths, and module settings

228
Tests/Wiki.Tests.ps1

@ -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
}
}
}

86
appveyor.yml

@ -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…
Cancel
Save