Skip to content

Commit 641a1e6

Browse files
tablackburnclaude
andauthored
ci(release): build release notes from CHANGELOG; auto-populate PSData.ReleaseNotes; harden version expansion (#31)
* ci(release): build release notes from CHANGELOG and harden version expansion Ports the ScheduledTasksManager release-tooling improvements into the template so every module scaffolded from it inherits them. - Create GitHub Release: extract the published version's section from CHANGELOG.md and pass it via --notes-file (in pwsh), instead of --generate-notes. The latter lists every merged PR since the last release tag, which between version bumps is dominated by bot/CI/chore PRs and buries the actual user-facing changes. Includes a Full Changelog compare link and falls back to --generate-notes if a version has no changelog section (so a release is never blocked). - Pass the version output via env (VERSION) in the 'Check if Release Exists', 'Check if PSGallery Version Exists', and 'Create GitHub Release' steps instead of inlining ${{ steps.version.outputs.version }} into the run scripts. Inline template expansion is substituted into the script text before the shell runs (a template-injection vector zizmor/CodeRabbit flag); env vars are read at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(release): populate PSData.ReleaseNotes from CHANGELOG at publish time So a scaffolded module's PowerShell Gallery release-notes panel shows the curated, user-facing notes for each version (matching the GitHub release body) instead of a static CHANGELOG link. - build.depend.psd1: add ChangelogManagement 3.1.0 (Keep a Changelog parser). - build.psake.ps1: new UpdateReleaseNotes task (Depends Build) that reads the entry matching the module version via Get-ChangelogData and sets the built manifest's PrivateData.PSData.ReleaseNotes via Update-ModuleManifest. Wired in via $PSBPublishDependency so it runs before Publish-PSBuildModule. Non-fatal if the changelog can't be read or has no entry for the version. Mirrors the DSC Community Sampler pattern. Scaffolded modules use SemVer Keep a Changelog (CHANGELOG.template.md), the format this was validated against in ScheduledTasksManager. The template repo itself never runs this path (its publish is guarded against the un-initialized placeholder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(release): guard empty changelog notes and unreadable CHANGELOG (review) Addresses Copilot review on #31: - UpdateReleaseNotes: if the matched CHANGELOG entry is empty/whitespace, warn and leave ReleaseNotes unchanged rather than overwriting the built manifest with an empty string. - Create GitHub Release: read CHANGELOG.md defensively (Test-Path + try/catch). A missing or unreadable file now falls back to --generate-notes instead of failing the publish (GitHub's pwsh runs with $ErrorActionPreference = 'Stop'). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(release): exclude current tag from compare-link base (review) Addresses CodeRabbit review on #31: if a v$version tag already exists (e.g. a re-run, or a tag pushed without a release), the previous-tag selection could pick it and produce a self-referential Full Changelog compare link. Filter out "v$version" before selecting the most recent tag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ec17dd4 commit 641a1e6

3 files changed

Lines changed: 108 additions & 8 deletions

File tree

.github/workflows/PublishModuleToPowerShellGallery.yaml

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,24 @@ jobs:
5454
shell: bash
5555
env:
5656
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57+
VERSION: ${{ steps.version.outputs.version }}
5758
run: |
58-
if gh release view "v${{ steps.version.outputs.version }}" > /dev/null 2>&1; then
59+
if gh release view "v$VERSION" > /dev/null 2>&1; then
5960
echo "exists=true" >> $GITHUB_OUTPUT
60-
echo "GitHub release v${{ steps.version.outputs.version }} already exists"
61+
echo "GitHub release v$VERSION already exists"
6162
else
6263
echo "exists=false" >> $GITHUB_OUTPUT
63-
echo "GitHub release v${{ steps.version.outputs.version }} does not exist"
64+
echo "GitHub release v$VERSION does not exist"
6465
fi
6566
6667
- name: Check if PSGallery Version Exists
6768
id: check_psgallery
6869
if: steps.check_release.outputs.exists == 'false'
6970
shell: pwsh
71+
env:
72+
VERSION: ${{ steps.version.outputs.version }}
7073
run: |
71-
$version = "${{ steps.version.outputs.version }}"
74+
$version = $env:VERSION
7275
$published = Find-Module -Name {{ModuleName}} -RequiredVersion $version -Repository PSGallery -ErrorAction SilentlyContinue
7376
if ($published) {
7477
Write-Host "PSGallery version $version already exists"
@@ -85,13 +88,60 @@ jobs:
8588

8689
- name: Create GitHub Release
8790
if: steps.check_release.outputs.exists == 'false'
88-
shell: bash
91+
shell: pwsh
8992
env:
9093
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94+
REPOSITORY: ${{ github.repository }}
95+
VERSION: ${{ steps.version.outputs.version }}
9196
run: |
92-
gh release create "v${{ steps.version.outputs.version }}" \
93-
--title "v${{ steps.version.outputs.version }}" \
94-
--generate-notes
97+
$version = $env:VERSION
98+
99+
# Build release notes from this version's CHANGELOG.md section so the release
100+
# body carries only the curated, user-facing entries (not the full PR list that
101+
# --generate-notes produces, which is dominated by bot/CI/chore PRs).
102+
# Read defensively: a missing/unreadable CHANGELOG.md must fall back to
103+
# --generate-notes (below), never fail the publish.
104+
$changelogLines = $null
105+
if (Test-Path -LiteralPath './CHANGELOG.md') {
106+
try {
107+
$changelogLines = Get-Content -LiteralPath './CHANGELOG.md' -ErrorAction Stop
108+
}
109+
catch {
110+
Write-Host "::warning::Could not read CHANGELOG.md ($($_.Exception.Message)); falling back to auto-generated notes."
111+
}
112+
}
113+
$captured = [System.Collections.Generic.List[string]]::new()
114+
if ($changelogLines) {
115+
$headerPattern = '^##\s+\[' + [regex]::Escape($version) + '\]'
116+
$capturing = $false
117+
foreach ($line in $changelogLines) {
118+
if (-not $capturing) {
119+
if ($line -match $headerPattern) { $capturing = $true }
120+
continue
121+
}
122+
if ($line -match '^##\s+\[') { break } # next version header ends the section
123+
$captured.Add($line)
124+
}
125+
}
126+
$body = ($captured -join "`n").Trim()
127+
128+
if ([string]::IsNullOrWhiteSpace($body)) {
129+
Write-Host "::warning::No CHANGELOG.md section found for $version; falling back to auto-generated notes."
130+
gh release create "v$version" --title "v$version" --generate-notes
131+
}
132+
else {
133+
# Append a compare link against the most recent existing tag. The v$version
134+
# tag does not exist yet (this step creates it), so the latest tag is the
135+
# previous release.
136+
$previousTag = git tag --list 'v*' --sort=-version:refname |
137+
Where-Object { $_ -ne "v$version" } |
138+
Select-Object -First 1
139+
if ($previousTag) {
140+
$body += "`n`n**Full Changelog**: https://github.com/$env:REPOSITORY/compare/$previousTag...v$version"
141+
}
142+
Set-Content -LiteralPath './release-notes.md' -Value $body -Encoding utf8
143+
gh release create "v$version" --title "v$version" --notes-file './release-notes.md'
144+
}
95145
96146
- name: Publish to PSGallery
97147
if: steps.check_release.outputs.exists == 'false' && steps.check_psgallery.outputs.exists == 'false'

build.depend.psd1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@
2525
'PSScriptAnalyzer' = @{
2626
Version = '1.25.0'
2727
}
28+
# Parses CHANGELOG.md (Keep a Changelog format) so the Publish task can populate the
29+
# built manifest's PSData.ReleaseNotes from the matching version's entry.
30+
'ChangelogManagement' = @{
31+
Version = '3.1.0'
32+
}
2833
}

build.psake.ps1

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,50 @@ Task -Name 'Init_Integration' -Description 'Load integration test environment va
4545
}
4646
}
4747

48+
# Populate the built manifest's ReleaseNotes from the matching CHANGELOG.md entry so the
49+
# PowerShell Gallery release-notes panel shows the curated, user-facing notes (the same
50+
# content used for the GitHub release) instead of just a link. Depends on Build so the
51+
# staged manifest in ModuleOutDir exists; runs before Publish (see $PSBPublishDependency
52+
# below). Non-fatal if the changelog can't be read or has no entry for the version being
53+
# published, so a release is never blocked.
54+
Task -Name 'UpdateReleaseNotes' -Depends 'Build' -Description 'Set built manifest ReleaseNotes from the matching CHANGELOG.md entry' {
55+
$changelogPath = Join-Path -Path $PSScriptRoot -ChildPath 'CHANGELOG.md'
56+
if (-not (Test-Path -Path $changelogPath)) {
57+
Write-Warning 'CHANGELOG.md not found; leaving ReleaseNotes unchanged.'
58+
return
59+
}
60+
61+
$moduleVersion = $PSBPreference.General.ModuleVersion
62+
try {
63+
Import-Module -Name 'ChangelogManagement' -ErrorAction Stop
64+
$changelogData = Get-ChangelogData -Path $changelogPath -ErrorAction Stop
65+
}
66+
catch {
67+
Write-Warning "Could not read CHANGELOG.md ($($_.Exception.Message)); leaving ReleaseNotes unchanged."
68+
return
69+
}
70+
71+
$releaseEntry = $changelogData.Released |
72+
Where-Object { [string]$_.Version -eq [string]$moduleVersion } |
73+
Select-Object -First 1
74+
if (-not $releaseEntry) {
75+
Write-Warning "No CHANGELOG.md entry found for version $moduleVersion; leaving ReleaseNotes unchanged."
76+
return
77+
}
78+
79+
$releaseNotes = $releaseEntry.RawData.Trim()
80+
if ([string]::IsNullOrWhiteSpace($releaseNotes)) {
81+
Write-Warning "CHANGELOG.md entry for version $moduleVersion is empty; leaving ReleaseNotes unchanged."
82+
return
83+
}
84+
$builtManifest = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath "$($PSBPreference.General.ModuleName).psd1"
85+
Update-ModuleManifest -Path $builtManifest -ReleaseNotes $releaseNotes -ErrorAction Stop
86+
Write-Host " Set ReleaseNotes on built manifest from CHANGELOG [$($releaseEntry.Version)] ($($releaseNotes.Length) chars)" -ForegroundColor Gray
87+
}
88+
89+
# Inject ReleaseNotes into the built manifest before publishing (PowerShellBuild's Publish
90+
# defaults to depending only on 'Test').
91+
$PSBPublishDependency = @('Test', 'UpdateReleaseNotes')
92+
4893
# Note: -Depends replaces PowerShellBuild's default dependencies, so we must include Pester and Analyze explicitly
4994
Task -Name 'Test' -FromModule 'PowerShellBuild' -MinimumVersion '0.7.3' -Depends 'Init_Integration', 'Pester', 'Analyze'

0 commit comments

Comments
 (0)