From f448a857b527391ec7045cddd6f45d97f4db3aca Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Fri, 28 Aug 2026 14:36:53 -0400 Subject: [PATCH 1/2] ci: Check CHANGELOG.md against the Keep a Changelog format Adds a changelog job to the shared module workflow, so every psake module that adopts it gets the check from one place. The check only applies when the file says the format is the one it follows. A CHANGELOG.md that never mentions Keep a Changelog is skipped, and so is a repository with no changelog at all, so adding this to a shared workflow cannot ambush a consumer that never made the claim. What it verifies, for a file that does make the claim: - Section headings are among the six types Keep a Changelog defines -- Added, Changed, Deprecated, Removed, Fixed, Security. An invented seventh is how a changelog quietly stops being machine-readable. - Release headings read '## [version] - YYYY-MM-DD'. - An '## Unreleased' section exists. Every violation is reported, not just the first, because a changelog that has drifted has usually drifted in several places and one-at-a-time is a poor way to find that out. Written as a self-contained pwsh step rather than a third-party action. The search for an existing one turned up nothing suitable: the mature option, dangoslen/changelog-enforcer, checks that a pull request modified the changelog rather than that it is well-formed, and would misfire on every change that correctly needs no entry. The parsers that do validate format are Node packages with negligible adoption. A pwsh step also avoids adding a moving third-party tag to pin. Verified against real changelogs before landing: psake/psake passes unchanged, psake/PowerShellBuild fails on exactly the drift it has today (a 'Fixes' heading, a 'Breaking changes' heading, and eleven release headings missing the date separator), a changelog that never declares the format is skipped, and a repository with no changelog is skipped. The script was then extracted back out of the YAML and re-run to confirm the embedding did not mangle it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE --- .github/workflows/ModuleCI.yml | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/.github/workflows/ModuleCI.yml b/.github/workflows/ModuleCI.yml index 52ecf51..e2692d6 100644 --- a/.github/workflows/ModuleCI.yml +++ b/.github/workflows/ModuleCI.yml @@ -24,6 +24,78 @@ jobs: uses: reviewdog/action-suggester@v1.21.0 with: tool_name: "PSScriptAnalyzer" + changelog: + name: Check Changelog Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check CHANGELOG.md against the Keep a Changelog format + shell: pwsh + run: | + # Verify CHANGELOG.md against the Keep a Changelog format -- but only when the file + # says that is the format it follows. A repository that never made the claim is not + # held to it, so adding this to a shared workflow cannot ambush a consumer. + $ErrorActionPreference = 'Stop' + + $changelogPath = Join-Path -Path $PWD -ChildPath 'CHANGELOG.md' + if (-not (Test-Path -LiteralPath $changelogPath)) { + Write-Host 'No CHANGELOG.md; nothing to check.' + exit 0 + } + + $line = Get-Content -LiteralPath $changelogPath + if (-not ($line -match 'keep ?a ?changelog')) { + Write-Host 'CHANGELOG.md does not declare the Keep a Changelog format; nothing to check.' + exit 0 + } + + # The six types Keep a Changelog defines. Anything else is a heading someone invented, + # which is how a changelog stops being machine-readable. + $validType = @('Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security') + + $problem = [System.Collections.Generic.List[string]]::new() + $inFence = $false + $sawUnreleased = $false + + for ($i = 0; $i -lt $line.Count; $i++) { + $text = $line[$i] + if ($text.TrimStart().StartsWith('```')) { $inFence = -not $inFence; continue } + if ($inFence) { continue } + + $number = $i + 1 + + if ($text -match '^##\s+(?!#)(.*)$') { + $heading = $Matches[1].Trim() + if ($heading -match '^\[?Unreleased\]?$') { + $sawUnreleased = $true + continue + } + # Keep a Changelog writes a release as: ## [1.0.0] - 2017-06-20 + if ($heading -notmatch '^\[[^\]]+\]\s+-\s+\d{4}-\d{2}-\d{2}$') { + $problem.Add("line ${number}: release heading is not '## [version] - YYYY-MM-DD': ## $heading") + } + } elseif ($text -match '^###\s+(?!#)(.*)$') { + $type = $Matches[1].Trim() + if ($type -notin $validType) { + $problem.Add("line ${number}: '$type' is not a Keep a Changelog type ($($validType -join ', '))") + } + } + } + + if (-not $sawUnreleased) { + $problem.Add('no "## Unreleased" section; Keep a Changelog keeps one at the top for work not yet released') + } + + if ($problem.Count -gt 0) { + Write-Host "CHANGELOG.md does not follow the Keep a Changelog format it declares:`n" + $problem | ForEach-Object { Write-Host " $_" } + Write-Host "`nSee https://keepachangelog.com for the format." + exit 1 + } + + Write-Host "CHANGELOG.md follows the Keep a Changelog format ($($line.Count) lines checked)." + exit 0 + test: name: Run Pwsh Tests # We still have the azure pipeline setup that's running windows From 8f3994847ad0488ab6738daec69b644ea74ae6ee Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Fri, 28 Aug 2026 14:51:16 -0400 Subject: [PATCH 2/2] ci: Validate CHANGELOG.md against the Keep a Changelog format Uses mindsers/changelog-reader-action, the same action and the same validation_level PowerShellOrg already runs in its powershell-ci workflow, so the two orgs check changelogs the same way. This replaces a hand-written pwsh script in the first draft of this branch. The script worked, but the action is strictly better: it validates section types and version identifiers as the script did, and additionally checks chronological ordering and understands both semver and pep440 version schemes. It is also the thing it is named for -- a reader -- so the same action can extract a release's notes when cutting a release, which a bespoke validator would never have grown. Two deviations from PowerShellOrg's configuration, both deliberate: validation_depth is raised from its default of 10 to 100. At the default only the ten most recent entries are checked. psake/PowerShellBuild has sixteen, and the two format defects that prompted this work were in 0.3.0 and 0.7.1 -- both outside a ten-entry window. Format drift accumulates in old entries precisely because nobody looks at them. The step is skipped when the repository has no CHANGELOG.md. This is a shared workflow, and psake-contrib and docs have no changelog; failing a consumer for not having a file it never claimed to have would be wrong. Not pinned to a commit SHA. Every other action here uses a moving tag, and pinning one entry alone is the partial pinning that psake/PowerShellBuild#145 rejected as buying nothing. Worth revisiting org-wide when that issue is settled. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE --- .github/workflows/ModuleCI.yml | 83 +++++----------------------------- 1 file changed, 11 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ModuleCI.yml b/.github/workflows/ModuleCI.yml index e2692d6..820867f 100644 --- a/.github/workflows/ModuleCI.yml +++ b/.github/workflows/ModuleCI.yml @@ -12,6 +12,17 @@ jobs: runs-on: [ubuntu-latest] steps: - uses: actions/checkout@v4 + # Validates CHANGELOG.md against the Keep a Changelog format. Skipped when the + # repository has no changelog, so a consumer without one is not failed for it. + # validation_depth defaults to 10 entries; raised so older releases are checked + # too, which is where format drift accumulates unnoticed. + - name: Validate changelog (keepachangelog) + if: hashFiles('CHANGELOG.md') != '' + uses: mindsers/changelog-reader-action@v2 + with: + validation_level: error + validation_depth: 100 + path: ./CHANGELOG.md - uses: streetsidesoftware/cspell-action@v6 with: strict: false @@ -24,78 +35,6 @@ jobs: uses: reviewdog/action-suggester@v1.21.0 with: tool_name: "PSScriptAnalyzer" - changelog: - name: Check Changelog Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Check CHANGELOG.md against the Keep a Changelog format - shell: pwsh - run: | - # Verify CHANGELOG.md against the Keep a Changelog format -- but only when the file - # says that is the format it follows. A repository that never made the claim is not - # held to it, so adding this to a shared workflow cannot ambush a consumer. - $ErrorActionPreference = 'Stop' - - $changelogPath = Join-Path -Path $PWD -ChildPath 'CHANGELOG.md' - if (-not (Test-Path -LiteralPath $changelogPath)) { - Write-Host 'No CHANGELOG.md; nothing to check.' - exit 0 - } - - $line = Get-Content -LiteralPath $changelogPath - if (-not ($line -match 'keep ?a ?changelog')) { - Write-Host 'CHANGELOG.md does not declare the Keep a Changelog format; nothing to check.' - exit 0 - } - - # The six types Keep a Changelog defines. Anything else is a heading someone invented, - # which is how a changelog stops being machine-readable. - $validType = @('Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security') - - $problem = [System.Collections.Generic.List[string]]::new() - $inFence = $false - $sawUnreleased = $false - - for ($i = 0; $i -lt $line.Count; $i++) { - $text = $line[$i] - if ($text.TrimStart().StartsWith('```')) { $inFence = -not $inFence; continue } - if ($inFence) { continue } - - $number = $i + 1 - - if ($text -match '^##\s+(?!#)(.*)$') { - $heading = $Matches[1].Trim() - if ($heading -match '^\[?Unreleased\]?$') { - $sawUnreleased = $true - continue - } - # Keep a Changelog writes a release as: ## [1.0.0] - 2017-06-20 - if ($heading -notmatch '^\[[^\]]+\]\s+-\s+\d{4}-\d{2}-\d{2}$') { - $problem.Add("line ${number}: release heading is not '## [version] - YYYY-MM-DD': ## $heading") - } - } elseif ($text -match '^###\s+(?!#)(.*)$') { - $type = $Matches[1].Trim() - if ($type -notin $validType) { - $problem.Add("line ${number}: '$type' is not a Keep a Changelog type ($($validType -join ', '))") - } - } - } - - if (-not $sawUnreleased) { - $problem.Add('no "## Unreleased" section; Keep a Changelog keeps one at the top for work not yet released') - } - - if ($problem.Count -gt 0) { - Write-Host "CHANGELOG.md does not follow the Keep a Changelog format it declares:`n" - $problem | ForEach-Object { Write-Host " $_" } - Write-Host "`nSee https://keepachangelog.com for the format." - exit 1 - } - - Write-Host "CHANGELOG.md follows the Keep a Changelog format ($($line.Count) lines checked)." - exit 0 - test: name: Run Pwsh Tests # We still have the azure pipeline setup that's running windows