diff --git a/.github/workflows/CreateDevRelease.yml b/.github/workflows/CreateDevRelease.yml new file mode 100644 index 0000000..20e443e --- /dev/null +++ b/.github/workflows/CreateDevRelease.yml @@ -0,0 +1,62 @@ +name: Publish Dev Build to GitHub Releases + +on: + push: + branches: [ "develop" ] + workflow_dispatch: + +permissions: + contents: write + +jobs: + dev-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set module version to dev build + shell: pwsh + run: | + $modulePath = "PiHoleShell" + $manifest = Get-ChildItem -Path $modulePath -Filter PiHoleShell.psd1 -Recurse | Select-Object -First 1 + + if (-not $manifest) { + throw "No module manifest (*.psd1) found in $modulePath" + } + + $shortSha = "${{ github.sha }}".Substring(0, 7) + $devVersion = "0.0.0-dev.$shortSha" + (Get-Content $manifest.FullName) -replace '0.0.0', $devVersion | Set-Content $manifest.FullName + Write-Host "Set dev module version to $devVersion" + + - name: Zip folder + run: | + mkdir -p output + zip -r output/release.zip PiHoleShell + + - name: Remove previous dev-latest release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release delete dev-latest --yes --cleanup-tag || true + + - name: Create dev pre-release + uses: softprops/action-gh-release@v1 + with: + tag_name: dev-latest + name: "Development build (develop @ ${{ github.sha }})" + prerelease: true + body: | + Automatically built from the latest `develop` branch commit ${{ github.sha }}. + + This is a test build for local verification only — it is **not** published to the PowerShell Gallery, and this release is overwritten on every push to `develop`. + + To try it out, download and extract `release.zip`, then: + ```powershell + Import-Module .\PiHoleShell\PiHoleShell.psd1 -Force + ``` + files: output/release.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml index 5d3207e..caa4877 100644 --- a/.github/workflows/CreateRelease.yml +++ b/.github/workflows/CreateRelease.yml @@ -75,6 +75,7 @@ jobs: with: tag_name: ${{ steps.bump.outputs.new_tag }} name: "Release ${{ steps.bump.outputs.new_tag }}" + generate_release_notes: true files: output/release.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/SyncReadmeCommandReference.yml b/.github/workflows/SyncReadmeCommandReference.yml new file mode 100644 index 0000000..c409d63 --- /dev/null +++ b/.github/workflows/SyncReadmeCommandReference.yml @@ -0,0 +1,37 @@ +name: Sync README Command Reference + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [ "main" ] + +permissions: + contents: write + +jobs: + sync-readme: + if: github.event.pull_request.head.ref == 'develop' + runs-on: ubuntu-latest + + steps: + - name: Checkout PR head branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Regenerate README command reference + shell: pwsh + run: ./tools/Update-ReadmeCommandReference.ps1 + + - name: Commit changes if needed + run: | + if [ -n "$(git status --porcelain README.md)" ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add README.md + git commit -m "docs: sync README command reference [skip ci]" + git push origin HEAD:${{ github.head_ref }} + else + echo "README.md command reference already up to date." + fi diff --git a/README.md b/README.md index da2c77e..00ebc27 100644 --- a/README.md +++ b/README.md @@ -80,59 +80,60 @@ Every function accepts the same core parameters: ## Command Reference -Functions marked 🚧 are still under active development — signatures and output shapes may change. +Functions marked 🚧 are still under active development — signatures and output shapes may change. This section is generated from the module's actual exported functions by `tools/Update-ReadmeCommandReference.ps1`, and kept in sync automatically on every `develop` → `main` pull request — don't hand-edit the block below. + ### Actions | Function | Description | |---|---| -| `Invoke-PiHoleFlushNetwork` | Flush the network table, removing known devices and their addresses | -| `Restart-PiHoleDnsService` | Restart the `pihole-FTL` service | -| `Update-PiHoleActionsGravity` 🚧 | Run `pihole -g` to rebuild the gravity/adlists database | +| `Invoke-PiHoleFlushNetwork` | Flushes the network table. This includes removing both all known devices and their associated addresses. | +| `Restart-PiHoleDnsService` | Restarts the pihole-FTL service | +| `Update-PiHoleActionsGravity` 🚧 | Update Pi-hole's adlists by running pihole -g. The output of the process is streamed with chunked encoding. Use the optional color query parameter to include ANSI color escape codes in the output. | ### DNS Control | Function | Description | |---|---| -| `Get-PiHoleDnsBlockingStatus` | Get current blocking status and any active timer | -| `Set-PiHoleDnsBlocking` | Enable or disable blocking, optionally for a set duration | +| `Get-PiHoleDnsBlockingStatus` | _No description yet_ | +| `Set-PiHoleDnsBlocking` | _No description yet_ | ### Group Management | Function | Description | |---|---| -| `Get-PiHoleGroup` | List groups | -| `New-PiHoleGroup` | Create a group | -| `Update-PiHoleGroup` | Update an existing group | -| `Remove-PiHoleGroup` 🚧 | Delete a group | +| `Get-PiHoleGroup` | Get groups | +| `New-PiHoleGroup` | Creates a new group in the groups object. | +| `Remove-PiHoleGroup` 🚧 | Delete group | +| `Update-PiHoleGroup` | Items may be updated by replacing them. | ### List Management | Function | Description | |---|---| -| `Get-PiHoleList` 🚧 | List allow/deny lists | -| `Add-PiHoleList` 🚧 | Add a domain to an allow/deny list | -| `Remove-PiHoleList` 🚧 | Remove lists | -| `Search-PiHoleListDomain` | Search all lists for a domain, with optional partial matching | +| `Add-PiHoleList` 🚧 | Add new list | +| `Get-PiHoleList` 🚧 | Get lists | +| `Remove-PiHoleList` 🚧 | Deletes multiple lists in the lists object. | +| `Search-PiHoleListDomain` | _No description yet_ | ### Metrics | Function | Description | |---|---| -| `Get-PiHoleStatsSummary` | Overview of query, system, and FTL activity | -| `Get-PiHoleStatsRecentBlocked` | Most recently blocked domain | -| `Get-PiHoleStatsQueryType` | Query breakdown by DNS record type | -| `Get-PiHoleStatsTopDomain` | Top permitted/blocked domains | -| `Get-PiHoleStatsTopClient` | Top clients by query volume | +| `Get-PiHoleStatsQueryType` | _No description yet_ | +| `Get-PiHoleStatsRecentBlocked` | Request most recently blocked domain | +| `Get-PiHoleStatsSummary` | Get overview of Pi-hole activity Request various query, system, and FTL properties | +| `Get-PiHoleStatsTopClient` | Get top clients Request the top clients (by query count) | +| `Get-PiHoleStatsTopDomain` | _No description yet_ | ### Configuration & Diagnostics | Function | Description | |---|---| -| `Get-PiHoleConfig` 🚧 | Read the Pi-hole configuration | -| `Get-PiHolePadd` 🚧 | Data used to power the PADD dashboard | -| `Get-PiHoleInfoMessage` | Pi-hole diagnosis messages | -| `Get-PiHoleInfoHost` 🚧 | Host system information | +| `Get-PiHoleConfig` 🚧 | _No description yet_ | +| `Get-PiHoleInfoHost` 🚧 | Get info about various host parameters This API hook returns a collection of host infos. | +| `Get-PiHoleInfoMessage` | Get Pi-hole diagnosis messages Request Pi-hole diagnosis messages | +| `Get-PiHolePadd` 🚧 | _No description yet_ | ### Authentication @@ -140,8 +141,9 @@ Session handling is automatic for every command above, but these are available f | Function | Description | |---|---| -| `Get-PiHoleCurrentAuthSession` | List active API sessions | -| `Remove-PiHoleAuthSession` | Revoke a session by ID | +| `Get-PiHoleCurrentAuthSession` | List of all current sessions including their validity and further information about the client such as the IP address and user agent. | +| `Remove-PiHoleAuthSession` | Using this endpoint, a session can be deleted by its ID. | + ## Testing diff --git a/tools/Update-ReadmeCommandReference.ps1 b/tools/Update-ReadmeCommandReference.ps1 new file mode 100644 index 0000000..e49f27c --- /dev/null +++ b/tools/Update-ReadmeCommandReference.ps1 @@ -0,0 +1,131 @@ +<# +.SYNOPSIS +Regenerates the "Command Reference" section of README.md from the module's actual exported functions. + +.DESCRIPTION +Reads the real exported commands from PiHoleShell.psm1 (not just what's documented) and builds a +markdown table per category, using each function's own comment-based help synopsis and its +'#Work In Progress' marker (if present) for the 🚧 flag. Writes the result between the + / markers in README.md, leaving +everything else in the file untouched. Exits with a non-zero code if README.md wasn't already +up to date, so it can be used as a CI freshness check as well as a local regeneration tool. +#> +[CmdletBinding()] +param ( + [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')), + [switch]$Check +) + +$ErrorActionPreference = 'Stop' + +$displayNames = [ordered]@{ + Actions = 'Actions' + DnsControl = 'DNS Control' + GroupManagement = 'Group Management' + ListManagement = 'List Management' + Metrics = 'Metrics' + Config = 'Configuration & Diagnostics' + Padd = 'Configuration & Diagnostics' + FTLInformation = 'Configuration & Diagnostics' + Teleporter = 'Configuration & Diagnostics' + Authentication = 'Authentication' +} +$categoryOrder = @('Actions', 'DnsControl', 'GroupManagement', 'ListManagement', 'Metrics', 'Config', 'Authentication') +$categoryIntros = @{ + Authentication = 'Session handling is automatic for every command above, but these are available for managing sessions directly:' +} + +$modulePath = Join-Path $RepoRoot 'PiHoleShell/PiHoleShell.psm1' +$publicPath = Join-Path $RepoRoot 'PiHoleShell/Public' +$readmePath = Join-Path $RepoRoot 'README.md' + +Import-Module $modulePath -Force +$exported = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-Module PiHoleShell).ExportedCommands.Keys) + +function Get-CleanSynopsis { + param([string]$FunctionName) + + $help = Get-Help -Name $FunctionName -ErrorAction SilentlyContinue + # Get-Help sometimes bleeds a standalone '#Work In Progress' comment that trails the closing + # '#>' into the Synopsis text itself; that's already surfaced separately via the WIP flag. + $rawSynopsis = ($help.Synopsis | Out-String) -replace '(?im)^\s*Work In Progress\s*$', '' + $synopsis = ($rawSynopsis -replace '\s+', ' ').Trim() + + if ([string]::IsNullOrWhiteSpace($synopsis) -or $synopsis -match '^https?://' -or $synopsis -eq $FunctionName) { + return $null + } + return $synopsis +} + +$functionsByCategory = [ordered]@{} +foreach ($category in $categoryOrder) { $functionsByCategory[$category] = [System.Collections.Generic.List[object]]::new() } + +Get-ChildItem -Path $publicPath -Filter '*.ps1' -Recurse | Sort-Object BaseName | ForEach-Object { + $name = $_.BaseName + if (-not $exported.Contains($name)) { return } + + $rawCategory = $_.Directory.Name + $category = if ($categoryOrder -contains $rawCategory) { $rawCategory } else { 'Config' } + + $content = Get-Content -Path $_.FullName -Raw + $isWip = $content -match '#\s*Work In Progress' + $synopsis = Get-CleanSynopsis -FunctionName $name + if (-not $synopsis) { $synopsis = '_No description yet_' } + + $functionsByCategory[$category].Add([PSCustomObject]@{ + Name = $name + Synopsis = $synopsis + IsWip = $isWip + }) +} + +$sections = [System.Collections.Generic.List[string]]::new() +foreach ($category in $categoryOrder) { + $functions = $functionsByCategory[$category] + if ($functions.Count -eq 0) { continue } + + $sections.Add("### $($displayNames[$category])") + $sections.Add('') + if ($categoryIntros.ContainsKey($category)) { + $sections.Add($categoryIntros[$category]) + $sections.Add('') + } + $sections.Add('| Function | Description |') + $sections.Add('|---|---|') + foreach ($fn in $functions) { + $flag = if ($fn.IsWip) { ' 🚧' } else { '' } + $sections.Add("| ``$($fn.Name)``$flag | $($fn.Synopsis) |") + } + $sections.Add('') +} + +$generated = ($sections -join "`n").TrimEnd() + +$readme = Get-Content -Path $readmePath -Raw +$startMarker = '' +$endMarker = '' + +if ($readme -notmatch [regex]::Escape($startMarker) -or $readme -notmatch [regex]::Escape($endMarker)) { + throw "README.md is missing the $startMarker / $endMarker markers." +} + +$pattern = [regex]::Escape($startMarker) + '(?s).*?' + [regex]::Escape($endMarker) +$replacement = "$startMarker`n$generated`n$endMarker" +$newReadme = [regex]::Replace($readme, $pattern, { $replacement }) + +if ($Check) { + if ($newReadme -ne $readme) { + Write-Error 'README.md command reference is out of date. Run tools/Update-ReadmeCommandReference.ps1 to refresh it.' + exit 1 + } + Write-Output 'README.md command reference is up to date.' + exit 0 +} + +if ($newReadme -ne $readme) { + Set-Content -Path $readmePath -Value $newReadme -NoNewline + Write-Output 'README.md command reference updated.' +} +else { + Write-Output 'README.md command reference already up to date.' +}