scripts/Generate-ReadmeFacts.ps1
|
#requires -Version 7.0 <# .SYNOPSIS Manifest-driven generator for the README.md tool-count facts. .DESCRIPTION Reads tools/tool-manifest.json (single source of truth) and rewrites the auto-managed sections of README.md (between BEGIN: <id> / END: <id> markers) so that the user-facing tool count, feature-list count, and tool-catalog summary stay in lockstep with the manifest. Without this generator the three numbers drift on every add/remove and require a hand-edit on every PR. Three marker IDs are managed: tool-count-tagline The bold one-liner under the badges. Format: "**One PowerShell command, N read-only assessment tools (+ 1 opt-in), one unified HTML and Markdown report.** Cloud-first by default: ..." tool-count-feature-list The first bullet under the Feature highlights `details` block. Format: "- **N tools** (+ 1 opt-in) across Azure (...), Entra (...), GitHub (...), and Azure DevOps (...)." tool-catalog-summary The collapsed Tool catalog `details` summary line. Format: "<details><summary><b>Tool catalog (N enabled + 1 opt-in)</b></summary>" The "+ 1 opt-in" label is intentionally static and refers to the only currently-shipped opt-in tool (`copilot-triage`, `enabled: false` in the manifest with an explicit `-EnableAiTriage` switch). Other disabled manifest entries are pre-registered scaffolding for follow-up PRs and are not user-runnable today. The generator is idempotent. Running it twice on a clean tree produces no diff. CI uses -CheckOnly mode to fail when the committed README is stale relative to the manifest. .PARAMETER ManifestPath Path to tools/tool-manifest.json. Defaults to the repo-relative location. .PARAMETER ReadmePath Path to the root README.md. Defaults to the repo-relative location. .PARAMETER CheckOnly Do not write files. Compare the generated content with what is on disk. Exits 0 when in sync, exits 1 when stale (and prints a clear remediation line). .EXAMPLE pwsh -File scripts/Generate-ReadmeFacts.ps1 Regenerate the auto-managed sections of README.md. .EXAMPLE pwsh -File scripts/Generate-ReadmeFacts.ps1 -CheckOnly Used by CI: fail if the committed README facts are stale. #> [CmdletBinding()] param( [string]$ManifestPath, [string]$ReadmePath, [switch]$CheckOnly ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Get-RepoRoot { $candidate = Split-Path -Parent $PSScriptRoot if (-not $candidate) { $candidate = (Get-Location).Path } return $candidate } $repoRoot = Get-RepoRoot if (-not $ManifestPath) { $ManifestPath = Join-Path $repoRoot 'tools/tool-manifest.json' } if (-not $ReadmePath) { $ReadmePath = Join-Path $repoRoot 'README.md' } if (-not (Test-Path -LiteralPath $ManifestPath)) { throw "Manifest not found at: $ManifestPath" } if (-not (Test-Path -LiteralPath $ReadmePath)) { throw "README.md not found at: $ReadmePath" } $manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json if (-not $manifest.tools) { throw "Manifest at $ManifestPath has no 'tools' array." } $enabledCount = @($manifest.tools | Where-Object { $_.enabled }).Count if ($enabledCount -le 0) { throw "Manifest has no enabled tools; refusing to generate misleading README facts." } # "+ 1 opt-in" is a stable marketing label for copilot-triage (the only # currently-shipped opt-in tool, gated by -EnableAiTriage). Other disabled # manifest entries are pre-registered futures (EASM, graph mapping family) # that are not user-runnable yet, so they do not count here. If a second # shipped opt-in lands, extend this expression to count tools whose `comment` # field contains "opt-in" (or introduce an explicit `optIn: true` flag in # the manifest schema). $optInCount = 1 function Convert-ToLfText { param([string]$Text) return ($Text -replace "`r`n", "`n") } # Each section has a stable BEGIN: <id> / END: <id> marker pair. The body # between them is regenerated verbatim from the manifest projection. To add # a new auto-managed fact, append a new entry below and wrap the matching # README block with the same markers. $sections = [ordered]@{ 'tool-count-tagline' = @" **One PowerShell command, $enabledCount read-only assessment tools (+ $optInCount opt-in), one unified HTML and Markdown report.** Cloud-first by default: target remote GitHub and Azure DevOps repositories without cloning anything by hand. "@ 'tool-count-feature-list' = @" - **$enabledCount tools** (+ $optInCount opt-in) across Azure (azqr, PSRule, Powerpipe, AzGovViz, Prowler, Defender for Cloud, ...), Entra (Maester, Identity Correlator, ...), GitHub (gitleaks, Trivy, Scorecard, zizmor), and Azure DevOps (pipeline security, service connections, repos). "@ 'tool-catalog-summary' = @" <details><summary><b>Tool catalog ($enabledCount enabled + $optInCount opt-in)</b></summary> "@ } $current = Convert-ToLfText (Get-Content -LiteralPath $ReadmePath -Raw) $updated = $current foreach ($id in $sections.Keys) { $beginMarker = "<!-- BEGIN: $id (generated by scripts/Generate-ReadmeFacts.ps1; do not edit by hand) -->" $endMarker = "<!-- END: $id -->" $beginIdx = $updated.IndexOf($beginMarker) $endIdx = $updated.IndexOf($endMarker) if ($beginIdx -lt 0 -or $endIdx -lt 0 -or $endIdx -lt $beginIdx) { throw "README.md is missing the BEGIN: $id / END: $id markers. Add them around the auto-managed block before running the generator." } $body = $sections[$id].TrimEnd("`n", "`r") $replacement = "$beginMarker`n$body`n$endMarker" $tail = $endIdx + $endMarker.Length $updated = $updated.Substring(0, $beginIdx) + $replacement + $updated.Substring($tail) } $updated = Convert-ToLfText $updated if ($CheckOnly) { if ($current -ne $updated) { Write-Host "[stale] README.md auto-managed sections are out of sync with the manifest." Write-Host '' Write-Host 'Run: pwsh -File scripts/Generate-ReadmeFacts.ps1' exit 1 } Write-Host "[ok] README.md tool-count facts in sync with manifest." exit 0 } if ($current -eq $updated) { Write-Host "[ok] README.md tool-count facts already in sync; no write needed." exit 0 } [System.IO.File]::WriteAllText($ReadmePath, $updated, [System.Text.UTF8Encoding]::new($false)) Write-Host "[wrote] $ReadmePath" exit 0 |