diff --git a/.build/generate-documentation.ps1 b/.build/generate-documentation.ps1 index 0a38c2e1..4a096505 100644 --- a/.build/generate-documentation.ps1 +++ b/.build/generate-documentation.ps1 @@ -2,9 +2,26 @@ .SYNOPSIS Builds the markdown documentation for the module. .DESCRIPTION - Builds the markdown documentation for the module using the PlatyPS PowerShell module. + Builds the markdown documentation for the module using the PlatyPS PowerShell module, generating one + markdown page per exported function. When -WikiPath is specified, the generated documentation is also + synchronized into the provided wiki checkout, matching the SdnDiagnostics.wiki repository structure: + - Function pages are copied/overwritten into a `functions\` subfolder of the wiki. + - Function pages for functions that no longer exist are removed from `functions\`. + - The `## Functions` section of `_SideBar.md` is regenerated with a link to every exported function. + Any hand-authored content above the `## Functions` heading (e.g. Home, How To Guides, + Troubleshooting, Learning sections) is preserved as-is. + Other hand-authored wiki pages (e.g. Home.md) are never touched. + .PARAMETER WikiPath + Path to a local checkout of the project's GitHub wiki repository (e.g. a checkout of + microsoft/SdnDiagnostics.wiki). When specified, generated documentation is synchronized into this path. #> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $false)] + [System.String]$WikiPath +) + $ErrorActionPreference = "Stop" $platyFromPoshGallery = Find-Module -Name platyPS @@ -21,7 +38,6 @@ else { $modulePath = "$PSScriptRoot\..\src\SdnDiagnostics.psd1" $docPath = "$PSScriptRoot\..\.documentation\functions" -$sideBarNav = "$PSScriptRoot\..\.documentation\_SideBar.md" if(-NOT (Test-Path -Path $docPath -PathType Container)) { $null = New-Item -Path $docPath -ItemType Directory -Force @@ -41,9 +57,121 @@ if($oldArticles){ "Generating function documentation" | Write-Host $null = New-MarkdownHelp -Module SdnDiagnostics -OutputFolder $docPath -NoMetadata -Force +$exportedFunctions = Get-Command -Module SdnDiagnostics | Sort-Object -Property Name $currentFiles = Get-ChildItem -Path $docPath\* -Include *.md -foreach($function in (Get-Command -Module SdnDiagnostics)){ +foreach($function in $exportedFunctions){ if($function.Name -inotin ($currentFiles).BaseName){ "Documentation not generated for {0}" -f $function.Name | Write-Host -ForegroundColor:Yellow } } + +if($WikiPath){ + if(-NOT (Test-Path -Path $WikiPath -PathType Container)){ + throw "WikiPath '$WikiPath' does not exist or is not a directory." + } + + "Synchronizing generated documentation into wiki path '{0}'" -f $WikiPath | Write-Host + + # mirrors the SdnDiagnostics.wiki repository structure, where function pages live under a + # `functions\` subfolder alongside other hand-authored top-level wiki pages + $wikiFunctionsPath = Join-Path -Path $WikiPath -ChildPath "functions" + if(-NOT (Test-Path -Path $wikiFunctionsPath -PathType Container)) { + $null = New-Item -Path $wikiFunctionsPath -ItemType Directory -Force + } + + # a manifest of function names generated by this script on the previous run is used to identify + # which pages are safe to remove. Only pages that this script itself generated previously (and are + # no longer exported) are treated as stale -- a hand-authored page that happens to match a + # PowerShell approved-verb naming convention (e.g. functions\Get-Started.md) is never a candidate + # for removal because it will never appear in the manifest. + $exportedFunctionNames = $exportedFunctions.Name + $manifestPath = Join-Path -Path $wikiFunctionsPath -ChildPath ".generated-manifest.json" + + if(Test-Path -Path $manifestPath -PathType Leaf) { + # ConvertFrom-Json writes its array result as a single non-enumerated pipeline object, so + # wrapping the whole pipeline in @(...) would double-nest it into a 1-element array containing + # the entire array. Assign directly instead, then coerce a single-name manifest (which + # ConvertFrom-Json unwraps to a plain string) into a 1-element array. + $previouslyGeneratedNames = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json + if($previouslyGeneratedNames -isnot [array]) { + $previouslyGeneratedNames = @($previouslyGeneratedNames) + } + $staleFunctionNames = $previouslyGeneratedNames | Where-Object { $_ -inotin $exportedFunctionNames } + if($staleFunctionNames){ + $staleWikiFunctionDocs = Get-ChildItem -Path "$wikiFunctionsPath\*" -Include *.md | Where-Object { $_.BaseName -iin $staleFunctionNames } + if($staleWikiFunctionDocs){ + "Removing {0} stale function page(s) from wiki:" -f $staleWikiFunctionDocs.Count | Write-Host + $staleWikiFunctionDocs | ForEach-Object { " - {0}" -f $_.Name | Write-Host } + $staleWikiFunctionDocs | Remove-Item -Force + } + } + + # functions present in the exported set but not in the previous manifest are newly-added pages + $newFunctionNames = $exportedFunctionNames | Where-Object { $_ -inotin $previouslyGeneratedNames } + } + else { + "No generated-page manifest found; skipping stale page removal for this run" | Write-Host -ForegroundColor:Yellow + # first run: every generated page is "new" from the wiki's perspective + $newFunctionNames = $exportedFunctionNames + } + + Get-ChildItem -Path "$docPath\*" -Include *.md | Copy-Item -Destination $wikiFunctionsPath -Force + + # summarize exactly which articles were published to the wiki on this run so it is visible in + # the pipeline log without needing to inspect the wiki repository's commit diff afterwards + "Publishing {0} function article(s) to wiki path 'functions\':" -f $exportedFunctionNames.Count | Write-Host + foreach($name in $exportedFunctionNames){ + if($name -iin $newFunctionNames){ + " - {0} (new)" -f $name | Write-Host -ForegroundColor:Green + } + else { + " - {0}" -f $name | Write-Host + } + } + + # record which function pages this script generated so a future run can safely identify stale + # pages without guessing based on naming convention alone + $exportedFunctionNames | ConvertTo-Json | Set-Content -Path $manifestPath -Force + + # regenerate only the "## Functions" section of _SideBar.md, preserving any hand-authored + # content (Home, How To Guides, Troubleshooting Guides, Learning, etc.) above that heading + "Updating wiki sidebar" | Write-Host + $sideBarWikiPath = Join-Path -Path $WikiPath -ChildPath "_SideBar.md" + $functionsHeadingPattern = '^#+\s*Functions\s*$' + + $prefixLines = [System.Collections.Generic.List[string]]::new() + if(Test-Path -Path $sideBarWikiPath -PathType Leaf) { + $existingSideBarLines = @(Get-Content -Path $sideBarWikiPath) + $headingIndex = -1 + for($i = 0; $i -lt $existingSideBarLines.Count; $i++){ + if($existingSideBarLines[$i] -match $functionsHeadingPattern){ + $headingIndex = $i + break + } + } + + if($headingIndex -ge 0){ + if($headingIndex -gt 0){ + $prefixLines.AddRange([string[]]$existingSideBarLines[0..($headingIndex - 1)]) + } + } + else { + $prefixLines.AddRange([string[]]$existingSideBarLines) + } + } + else { + "No existing _SideBar.md found at wiki root; creating a new one" | Write-Host -ForegroundColor:Yellow + } + + $newSideBarContent = [System.Collections.Generic.List[string]]::new() + $newSideBarContent.AddRange($prefixLines) + if($newSideBarContent.Count -gt 0 -and $newSideBarContent[$newSideBarContent.Count - 1] -ne ''){ + $newSideBarContent.Add('') + } + $newSideBarContent.Add('## Functions') + foreach($function in $exportedFunctions){ + $newSideBarContent.Add("- [$($function.Name)]($($function.Name))") + } + + $newSideBarContent | Set-Content -Path $sideBarWikiPath -Force +} diff --git a/.github/workflows/publish-documentation.yml b/.github/workflows/publish-documentation.yml new file mode 100644 index 00000000..9783a1f8 --- /dev/null +++ b/.github/workflows/publish-documentation.yml @@ -0,0 +1,92 @@ +name: Publish Documentation + +# Controls when the workflow will run +on: + # Triggers the workflow on push events but only for the main branch, and only when + # exported function source may have changed. + push: + branches: + - main + paths: + - 'src/**' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Serialize runs so concurrent pushes to main can't race on the wiki checkout/push. Runs are queued +# (not cancelled) so every triggering commit still gets published, each one regenerating docs from +# its own checkout after the previous run's push has completed. +concurrency: + group: publish-documentation-wiki + cancel-in-progress: false + +permissions: + contents: read + +jobs: + publish-documentation: + # The type of runner that the job will run on + runs-on: windows-latest + + permissions: + # required to push the generated documentation to the wiki + contents: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + + - name: 'Checkout SdnDiagnostics' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: main + + - name: 'Checkout SdnDiagnostics Wiki' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.repository }}.wiki + path: wiki + + - name: 'Generate and Sync Function Documentation' + run: | + $wikiPath = (Resolve-Path -Path .\wiki).Path + & .\main\.build\generate-documentation.ps1 -WikiPath $wikiPath + shell: powershell + + - name: 'Publish to Wiki' + run: | + Set-Location -Path .\wiki + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + + $changes = git status --porcelain + if ($changes) { + $shortSha = "${env:GITHUB_SHA}".Substring(0, 7) + git commit -m "docs: sync function documentation from main@$shortSha" + + # the concurrency group above serializes runs of this workflow, but the wiki can also be + # edited out-of-band (e.g. manually). Retry a couple of times with a rebase in that case + # rather than failing the run outright. + $pushed = $false + for ($attempt = 1; $attempt -le 3 -and -not $pushed; $attempt++) { + git push + if ($LASTEXITCODE -eq 0) { + $pushed = $true + } + elseif ($attempt -lt 3) { + "Push failed (attempt $attempt), pulling and retrying" | Write-Host -ForegroundColor Yellow + git pull --rebase + } + } + + if (-not $pushed) { + throw "Failed to push documentation changes to the wiki after multiple attempts" + } + } + else { + "No documentation changes to publish" | Write-Host + } + shell: powershell