From 6bbf6b681c6a86962c0771cad5cb823efe448a54 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Wed, 20 May 2026 19:36:13 -0400 Subject: [PATCH 1/3] ci(release): build release notes from CHANGELOG and harden version expansion Adopts the release-tooling pattern from PowerShellModuleTemplate (#31). - Create GitHub Release: extract the published version's section from CHANGELOG.md and pass it via --notes-file (pwsh), instead of --generate-notes (which lists every merged PR since the last release tag and buries user-facing changes under bot/CI/chore noise). Reads CHANGELOG.md defensively (Test-Path + try/catch) and excludes the current tag from the compare-link base; falls back to --generate-notes if there's no readable section, so a release is never blocked. - Pass the version output via env (VERSION) in the release-check, PSGallery-check, and create-release steps instead of inlining ${{ steps.version.outputs.version }} into run scripts (template-injection class flagged by zizmor/CodeRabbit). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PublishModuleToPowerShellGallery.yaml | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/.github/workflows/PublishModuleToPowerShellGallery.yaml b/.github/workflows/PublishModuleToPowerShellGallery.yaml index a8d57b2..f49b3b5 100644 --- a/.github/workflows/PublishModuleToPowerShellGallery.yaml +++ b/.github/workflows/PublishModuleToPowerShellGallery.yaml @@ -35,20 +35,23 @@ jobs: shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} run: | - if gh release view "v${{ steps.version.outputs.version }}" > /dev/null 2>&1; then + if gh release view "v$VERSION" > /dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT - echo "GitHub release v${{ steps.version.outputs.version }} already exists" + echo "GitHub release v$VERSION already exists" else echo "exists=false" >> $GITHUB_OUTPUT - echo "GitHub release v${{ steps.version.outputs.version }} does not exist" + echo "GitHub release v$VERSION does not exist" fi - name: Check if PSGallery Version Exists id: check_psgallery shell: pwsh + env: + VERSION: ${{ steps.version.outputs.version }} run: | - $version = "${{ steps.version.outputs.version }}" + $version = $env:VERSION $published = Find-Module -Name PlexAutomationToolkit -RequiredVersion $version -Repository PSGallery -ErrorAction SilentlyContinue if ($published) { Write-Host "PSGallery version $version already exists" @@ -65,13 +68,59 @@ jobs: - name: Create GitHub Release if: steps.check_release.outputs.exists == 'false' - shell: bash + shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + VERSION: ${{ steps.version.outputs.version }} run: | - gh release create "v${{ steps.version.outputs.version }}" \ - --title "v${{ steps.version.outputs.version }}" \ - --generate-notes + $version = $env:VERSION + + # Build release notes from this version's CHANGELOG.md section so the release + # body carries only the curated, user-facing entries (not the full PR list that + # --generate-notes produces, which is dominated by bot/CI/chore PRs). + # Read defensively: a missing/unreadable CHANGELOG.md must fall back to + # --generate-notes (below), never fail the publish. + $changelogLines = $null + if (Test-Path -LiteralPath './CHANGELOG.md') { + try { + $changelogLines = Get-Content -LiteralPath './CHANGELOG.md' -ErrorAction Stop + } + catch { + Write-Host "::warning::Could not read CHANGELOG.md ($($_.Exception.Message)); falling back to auto-generated notes." + } + } + $captured = [System.Collections.Generic.List[string]]::new() + if ($changelogLines) { + $headerPattern = '^##\s+\[' + [regex]::Escape($version) + '\]' + $capturing = $false + foreach ($line in $changelogLines) { + if (-not $capturing) { + if ($line -match $headerPattern) { $capturing = $true } + continue + } + if ($line -match '^##\s+\[') { break } # next version header ends the section + $captured.Add($line) + } + } + $body = ($captured -join "`n").Trim() + + if ([string]::IsNullOrWhiteSpace($body)) { + Write-Host "::warning::No CHANGELOG.md section found for $version; falling back to auto-generated notes." + gh release create "v$version" --title "v$version" --generate-notes + } + else { + # Append a compare link against the most recent existing tag, excluding the + # current version's tag (which may already exist on a re-run). + $previousTag = git tag --list 'v*' --sort=-version:refname | + Where-Object { $_ -ne "v$version" } | + Select-Object -First 1 + if ($previousTag) { + $body += "`n`n**Full Changelog**: https://github.com/$env:REPOSITORY/compare/$previousTag...v$version" + } + Set-Content -LiteralPath './release-notes.md' -Value $body -Encoding utf8 + gh release create "v$version" --title "v$version" --notes-file './release-notes.md' + } - name: Publish to PSGallery if: steps.check_psgallery.outputs.exists == 'false' From dab4ee850874fbf28822659806caa0ef5a623c2d Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Wed, 20 May 2026 19:36:19 -0400 Subject: [PATCH 2/3] ci(release): populate PSData.ReleaseNotes from CHANGELOG at publish time So the PowerShell Gallery release-notes panel shows the curated, user-facing notes for each version (matching the GitHub release body) instead of a static 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 through $PSBPublishDependency so it runs before Publish-PSBuildModule. Skips empty entries and is non-fatal if the changelog can't be read. Verified: ChangelogManagement parses this repo's CHANGELOG (0.11.2 resolvable). Co-Authored-By: Claude Opus 4.7 (1M context) --- build.depend.psd1 | 5 +++++ build.psake.ps1 | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/build.depend.psd1 b/build.depend.psd1 index 8b888e5..1ad52e4 100644 --- a/build.depend.psd1 +++ b/build.depend.psd1 @@ -25,6 +25,11 @@ 'PSScriptAnalyzer' = @{ Version = '1.24.0' } + # Parses CHANGELOG.md (Keep a Changelog format) so the Publish task can populate the + # built manifest's PSData.ReleaseNotes from the matching version's entry. + 'ChangelogManagement' = @{ + Version = '3.1.0' + } 'Microsoft.PowerShell.SecretManagement' = @{ Version = '1.1.2' } diff --git a/build.psake.ps1 b/build.psake.ps1 index a31b2d2..34c19d2 100644 --- a/build.psake.ps1 +++ b/build.psake.ps1 @@ -52,5 +52,50 @@ Task -Name 'Init_Integration' -Description 'Load integration test environment va } } +# Populate the built manifest's ReleaseNotes from the matching CHANGELOG.md entry so the +# PowerShell Gallery release-notes panel shows the curated, user-facing notes (the same +# content used for the GitHub release) instead of just a link. Depends on Build so the +# staged manifest in ModuleOutDir exists; runs before Publish (see $PSBPublishDependency +# below). Non-fatal if the changelog can't be read or has no entry for the version being +# published, so a release is never blocked. +Task -Name 'UpdateReleaseNotes' -Depends 'Build' -Description 'Set built manifest ReleaseNotes from the matching CHANGELOG.md entry' { + $changelogPath = Join-Path -Path $PSScriptRoot -ChildPath 'CHANGELOG.md' + if (-not (Test-Path -Path $changelogPath)) { + Write-Warning 'CHANGELOG.md not found; leaving ReleaseNotes unchanged.' + return + } + + $moduleVersion = $PSBPreference.General.ModuleVersion + try { + Import-Module -Name 'ChangelogManagement' -ErrorAction Stop + $changelogData = Get-ChangelogData -Path $changelogPath -ErrorAction Stop + } + catch { + Write-Warning "Could not read CHANGELOG.md ($($_.Exception.Message)); leaving ReleaseNotes unchanged." + return + } + + $releaseEntry = $changelogData.Released | + Where-Object { [string]$_.Version -eq [string]$moduleVersion } | + Select-Object -First 1 + if (-not $releaseEntry) { + Write-Warning "No CHANGELOG.md entry found for version $moduleVersion; leaving ReleaseNotes unchanged." + return + } + + $releaseNotes = $releaseEntry.RawData.Trim() + if ([string]::IsNullOrWhiteSpace($releaseNotes)) { + Write-Warning "CHANGELOG.md entry for version $moduleVersion is empty; leaving ReleaseNotes unchanged." + return + } + $builtManifest = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath "$($PSBPreference.General.ModuleName).psd1" + Update-ModuleManifest -Path $builtManifest -ReleaseNotes $releaseNotes -ErrorAction Stop + Write-Host " Set ReleaseNotes on built manifest from CHANGELOG [$($releaseEntry.Version)] ($($releaseNotes.Length) chars)" -ForegroundColor Gray +} + +# Inject ReleaseNotes into the built manifest before publishing (PowerShellBuild's Publish +# defaults to depending only on 'Test'). +$PSBPublishDependency = @('Test', 'UpdateReleaseNotes') + # Note: -Depends replaces PowerShellBuild's default dependencies, so we must include Pester and Analyze explicitly Task -Name 'Test' -FromModule 'PowerShellBuild' -MinimumVersion '0.7.3' -Depends 'Init_Integration', 'Pester', 'Analyze' From 7b53945462a15e5b78a6c6cd0b7bf1a3da4e121b Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Wed, 20 May 2026 19:41:42 -0400 Subject: [PATCH 3/3] ci(release): make UpdateReleaseNotes manifest write non-fatal (review) Addresses Copilot review on #56: the task is described as non-fatal, but the final Update-ModuleManifest -ErrorAction Stop was unguarded, so a failure (e.g. missing built manifest) would hard-fail Publish. Add a Test-Path check for the built manifest and wrap the update in try/catch that warns and leaves the existing ReleaseNotes in place, keeping the release unblocked as intended. Co-Authored-By: Claude Opus 4.7 (1M context) --- build.psake.ps1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/build.psake.ps1 b/build.psake.ps1 index 34c19d2..5c56065 100644 --- a/build.psake.ps1 +++ b/build.psake.ps1 @@ -89,8 +89,19 @@ Task -Name 'UpdateReleaseNotes' -Depends 'Build' -Description 'Set built manifes return } $builtManifest = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath "$($PSBPreference.General.ModuleName).psd1" - Update-ModuleManifest -Path $builtManifest -ReleaseNotes $releaseNotes -ErrorAction Stop - Write-Host " Set ReleaseNotes on built manifest from CHANGELOG [$($releaseEntry.Version)] ($($releaseNotes.Length) chars)" -ForegroundColor Gray + if (-not (Test-Path -Path $builtManifest)) { + Write-Warning "Built manifest not found at '$builtManifest'; leaving ReleaseNotes unchanged." + return + } + try { + Update-ModuleManifest -Path $builtManifest -ReleaseNotes $releaseNotes -ErrorAction Stop + Write-Host " Set ReleaseNotes on built manifest from CHANGELOG [$($releaseEntry.Version)] ($($releaseNotes.Length) chars)" -ForegroundColor Gray + } + catch { + # Keep publishing unblocked: a failure here just leaves the manifest's existing + # ReleaseNotes in place rather than aborting the release. + Write-Warning "Failed to set ReleaseNotes on the built manifest ($($_.Exception.Message)); leaving it unchanged." + } } # Inject ReleaseNotes into the built manifest before publishing (PowerShellBuild's Publish