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