Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 131 additions & 3 deletions .build/generate-documentation.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
92 changes: 92 additions & 0 deletions .github/workflows/publish-documentation.yml
Original file line number Diff line number Diff line change
@@ -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