diff --git a/.build/scripts/New-PSModule.ps1 b/.build/scripts/New-PSModule.ps1 index a65e13a..7d81e50 100644 --- a/.build/scripts/New-PSModule.ps1 +++ b/.build/scripts/New-PSModule.ps1 @@ -116,10 +116,11 @@ function New-PSModule { $moduleFileExists = Test-Path -Path $moduleFilePath $action = if ($moduleFileExists) { 'Overwrite file' - } else { + } + else { 'Create file' } - + if ($moduleFileExists) { if ($PSCmdlet.ShouldProcess($moduleFilePath, $action)) { if ($ConfirmPreference -eq 'None' -or $PSCmdlet.ShouldContinue('Overwrite existing file?', $moduleFilePath)) { @@ -141,17 +142,17 @@ function New-PSModule { else { 'Create manifest' } - + if ($manifestExists) { if ($PSCmdlet.ShouldProcess($manifestFilePath, $action)) { if ($ConfirmPreference -eq 'None' -or $PSCmdlet.ShouldContinue('Overwrite existing manifest?', $manifestFilePath)) { - New-ModuleManifest -Path $manifestFilePath -ModuleVersion '0.1.0' -RootModule "$moduleName.psm1" -Confirm:$false -WhatIf:$false + New-ModuleManifest -Path $manifestFilePath -ModuleVersion '0.1.0' -RootModule "$moduleName.psm1" -FunctionsToExport @() -CmdletsToExport @() -VariablesToExport '' -AliasesToExport @() -Confirm:$false -WhatIf:$false } } } else { if ($PSCmdlet.ShouldProcess($manifestFilePath, $action)) { - New-ModuleManifest -Path $manifestFilePath -ModuleVersion '0.1.0' -Description $moduleName -RootModule "$moduleName.psm1" -Confirm:$false -WhatIf:$false + New-ModuleManifest -Path $manifestFilePath -ModuleVersion '0.1.0' -Description $moduleName -RootModule "$moduleName.psm1" -FunctionsToExport @() -CmdletsToExport @() -VariablesToExport '' -AliasesToExport @() -Confirm:$false -WhatIf:$false } } } diff --git a/.build/scripts/Write-PSModuleDocs.ps1 b/.build/scripts/Write-PSModuleDocs.ps1 new file mode 100644 index 0000000..0605c05 --- /dev/null +++ b/.build/scripts/Write-PSModuleDocs.ps1 @@ -0,0 +1,244 @@ +<# + .SYNOPSIS + Generates markdown documentation for all public functions in the PowerShell module. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$VerbosePreference = 'SilentlyContinue' + +function Write-PSModuleDocs { + [CmdletBinding()] + <# + .SYNOPSIS + Generates markdown documentation for all public functions in the PowerShell module. + + .DESCRIPTION + Imports the built module directly from the output manifest and generates markdown + documentation for each public function. Output files are written to the docs folder + in the repository root, with one file per function named .md. + + .EXAMPLE + Write-PSModuleDocs + + Generates markdown documentation for all public functions and writes them to the docs folder. + #> + param() + + $gitRoot = Resolve-Path -Path "$PSScriptRoot\..\.." + $moduleName = Split-Path -Path $gitRoot -Leaf + $outputModulePath = "$gitRoot\.output\$moduleName" + $docsPath = "$gitRoot\docs" + + if (-not (Test-Path -Path $outputModulePath -PathType Container)) { + throw "Module directory not found at: '$outputModulePath', run 'make build' to build the module." + } + + $versionFolder = Get-ChildItem -Path $outputModulePath -Directory | Sort-Object Name -Descending | Select-Object -First 1 + + if (-not $versionFolder) { + throw "No version folder found in '$outputModulePath', run 'make build' to build the module." + } + + $manifestPath = "$($versionFolder.FullName)\$moduleName.psd1" + + if (-not (Test-Path -Path $manifestPath -PathType Leaf)) { + throw "Module manifest not found at: '$manifestPath', run 'make build' to build the module." + } + + try { + Import-Module $manifestPath -Force -ErrorAction Stop + } + catch { + throw "Module could not be imported from '$manifestPath': $_" + } + + try { + $module = Get-Module -Name $moduleName + if (-not $module) { + throw "Module '$moduleName' could not be found after import." + } + + $allAlias = Get-Alias + $docs = [Collections.Generic.List[PSCustomObject]]::new() + + foreach ($command in $module.ExportedFunctions.Values) { + $help = Get-Help $command -Full + + # If there's no real help documentation, skip + if (-not $help -or ( + (-not $help.Description) -and + (-not $help.examples) -and + ($help.Synopsis.Trim() -eq $command.Name -or -not $help.Synopsis) + )) { + continue + } + + $parameters = $help.parameters.parameter + $examples = $help.examples.example + $alias = @(($allAlias | where ResolvedCommandName -eq $command.Name).Name) + + $sections = [Collections.Generic.List[string]]::new() + + # Header + $header = "# $($command.Name)" + if ($alias) { + $header += " ($($alias -join ', '))" + } + $sections.Add($header) + + # Synopsis + if ($help.Synopsis -and $help.Synopsis.Trim() -notlike "$($command.Name)*") { + $sections.Add("## Synopsis`n`n$($help.Synopsis)") + } + + # Description + if ($help.Description.Text) { + $sections.Add("## Description`n`n$($help.Description.Text)") + } + + # Syntax + $syntaxLines = foreach ($paramSet in $command.ParameterSets) { + $paramStrings = foreach ($param in $paramSet.Parameters | where Name -notin [Management.Automation.Cmdlet]::CommonParameters) { + $typeName = if ($param.ParameterType -ne [switch]) { + " <$($param.ParameterType.Name)>" + } + else { + '' + } + $token = if ($param.Position -ge 0) { + "[-$($param.Name)$typeName]" | foreach { + if ($param.IsMandatory) { + "[-$($param.Name)$typeName]" + } + else { + "[[-$($param.Name)$typeName]]" + } + } + } + else { + if ($param.IsMandatory) { + "-$($param.Name)$typeName" + } + else { + "[-$($param.Name)$typeName]" + } + } + $token + } + "$($command.Name) $($paramStrings -join ' ')" + } + $syntaxText = ($syntaxLines -join "`n").Trim() + if ($syntaxText) { + $sections.Add("## Syntax`n`n``````powershell`n$syntaxText`n``````") + } + + # Parameters + if ($parameters) { + $paramLines = [Collections.Generic.List[string]]::new() + $paramLines.Add('## Parameters') + + foreach ($param in $parameters) { + $paramSection = "### -$($param.Name)" + + if ($param.Description.Text) { + $paramSection += "`n`n$($param.Description.Text)" + } + + $bullets = [Collections.Generic.List[string]]::new() + + if ($param.Type.Name) { + $bullets.Add("- **Type**: $($param.Type.Name)") + } + if ($param.Required) { + $bullets.Add("- **Required**: $($param.Required)") + } + if ($param.Position) { + $bullets.Add("- **Position**: $($param.Position)") + } + + $bullets.Add("- **Default value**: $(if ($param.defaultValue) { + $param.defaultValue + } + else { + 'None' + })") + + if ($param.pipelineInput) { + $bullets.Add("- **Accepts pipeline input**: $($param.pipelineInput)") + } + + $paramSection += "`n`n$($bullets -join "`n")" + $paramLines.Add($paramSection) + } + + $sections.Add($paramLines -join "`n`n") + } + + # Examples + if ($examples) { + $exampleLines = [Collections.Generic.List[string]]::new() + $exampleLines.Add('## Examples') + + $i = 1 + foreach ($example in $examples) { + $exampleSection = "### Example $i" + + $remarksText = ($example.remarks.Text -join '').Trim() + if ($remarksText) { + $exampleSection += "`n`n$remarksText" + } + + if ($example.code) { + $exampleSection += "`n`n``````powershell`n$($example.code)`n``````" + } + + $exampleLines.Add($exampleSection) + $i++ + } + + $sections.Add($exampleLines -join "`n`n") + } + + $docs.Add([PSCustomObject]@{ + FileName = "$($command.Name).md" + Content = $sections -join "`n`n" + }) + } + + # Clean up docs folder + if (Test-Path -Path $docsPath -PathType Container) { + Get-ChildItem -Path $docsPath -File | Remove-Item -Force + } + else { + $null = New-Item -Path $docsPath -ItemType Directory -Force + } + + if ($docs.Count -eq 0) { + $null = New-Item -Path "$docsPath\.gitkeep" -ItemType File -Force + Write-Verbose 'No documentation generated; created .gitkeep in docs folder.' + return + } + + foreach ($doc in $docs) { + $filePath = "$docsPath\$($doc.FileName)" + Set-Content -Path $filePath -Value $doc.Content -Encoding utf8 -Force + Write-Verbose "Written: $filePath" + } + + Write-Host "Documentation written to '$docsPath' ($($docs.Count) file(s))." -ForegroundColor Green + } + finally { + Get-Module -Name $moduleName -ErrorAction SilentlyContinue | Remove-Module -ErrorAction SilentlyContinue + } +} + +try { + Write-PSModuleDocs @PSBoundParameters +} +catch { + Write-Host $_ -ForegroundColor Red + exit 1 +} \ No newline at end of file diff --git a/.build/template/Template.psm1 b/.build/template/Template.psm1 index 9754f3e..5de72dd 100644 --- a/.build/template/Template.psm1 +++ b/.build/template/Template.psm1 @@ -71,33 +71,9 @@ if (Test-Path -Path $privatePath) { $publicPath = "$PSScriptRoot\public" if (Test-Path -Path $publicPath) { - # Snapshots - $currentAlias = Get-Alias | select Name, ReferencedCommand - $currentCommands = (Get-Command).Name - Get-ChildItem -Path $publicPath -Filter '*.ps1' | where PSIsContainer -eq $false | foreach { . $_.FullName } - - # Find new aliases - $aliasToExport = Compare-Object $currentAlias (Get-Alias | select Name, ReferencedCommand) -Property Name, ReferencedCommand | - where SideIndicator -eq '=>' | - select -ExpandProperty InputObject - - # Find new commands - $commandsToExport = Compare-Object $currentCommands (Get-Command).Name | - where SideIndicator -eq '=>' | - where InputObject -notin $aliasToExport.ReferencedCommand | - select -ExpandProperty InputObject - - # Combine all functions and export - $allFunctions = @($commandsToExport) + @($aliasToExport.ReferencedCommand) | - where { $_ } | - select -Unique - - if ($allFunctions -or $aliasToExport) { - Export-ModuleMember -Function $allFunctions -Alias $aliasToExport.Name - } } #endregion Public diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index e378efd..e0284ff 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -9,6 +9,7 @@ on: - '!LICENSE' - '!Makefile' - '!.github/workflows/release.yml' + - '!docs/**' workflow_dispatch: permissions: diff --git a/.github/workflows/update_docs.yml b/.github/workflows/update_docs.yml new file mode 100644 index 0000000..b24a979 --- /dev/null +++ b/.github/workflows/update_docs.yml @@ -0,0 +1,53 @@ +name: Update Documentation + +on: + pull_request: + paths: + - '**.psd1' + - '!**/classes.psd1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-docs: + runs-on: windows-latest + + env: + GITHUB_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY: ${{ github.event.repository.name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.head_ref }} + + - name: Setup Profile + shell: pwsh + run: .\.build\scripts\Setup-RunnerProfile.ps1 -Verbose + + - name: Build Module + run: .\.build\scripts\Build-PSModule.ps1 + + - name: Generate Documentation + shell: pwsh + run: .\.build\scripts\Write-PSModuleDocs.ps1 + + - name: Configure Git + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "DocsBot" + + - name: Commit Documentation Changes + run: | + git add docs/ + if git diff --staged --quiet; then + echo "No documentation changes to commit" + else + git commit -m "AUTO: Exported Command Documentation" + git push origin ${{ github.head_ref }} + fi