From 20b57dc3f5d5916a93eddc41e027740c66a56e27 Mon Sep 17 00:00:00 2001 From: Jeffrey Stuhr <110697945+fadwen@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:16:13 -0800 Subject: [PATCH] ci(release): create the GitHub release after the Gallery upload A tag published the module to the PowerShell Gallery but created no GitHub release, so the repository lists none for any published version. The other module repositories get the same job, so all of them release the same way. A github-release job now follows the publish job. It takes the module name and version from the root manifest and uses that version's CHANGELOG.md section as the release notes, with an Update-PSResource line and a link to the full history. It fails when the section is missing or empty. The notes are built in every run, a dry run included, so a missing section shows up before a tag is pushed. The release itself is created only for a tag push, with --verify-tag, and a release that already exists for the tag is left as written. The job alone has contents: write; the workflow keeps contents: read. --- .github/workflows/release.yml | 75 +++++++++++++++++++++++++++++++++++ CLAUDE.md | 5 ++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63bb698..436578b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,3 +93,78 @@ jobs: throw 'The PSGALLERY_API_KEY repository secret is not set. Add it under Settings -> Secrets and variables -> Actions.' } ./Build/Publish-Module.ps1 -Confirm:$false + + github-release: + name: 'GitHub release' + needs: publish + # Without this job a tag published to the Gallery but showed no GitHub release, so the + # repository page kept naming an older hand-made release as Latest. The notes are the + # version's CHANGELOG section. They are built in every run, a dry run included, so a missing + # section fails before a tag is pushed rather than after the upload. + runs-on: ubuntu-latest + # Creating the release is the only write this workflow makes to the repository + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Build the release notes from CHANGELOG.md + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $manifest = @(Get-ChildItem -Path . -Filter *.psd1 -File) + if ($manifest.Count -ne 1) { + throw "Expected one module manifest at the repository root, found $($manifest.Count)." + } + $module = $manifest[0].BaseName + $version = (Import-PowerShellDataFile -Path $manifest[0].FullName).ModuleVersion + + # The section runs from the version's heading to the next '## [' heading + $lines = @(Get-Content -Path ./CHANGELOG.md) + $heading = '^' + [regex]::Escape("## [$version]") + $start = -1 + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match $heading) { $start = $i; break } + } + if ($start -lt 0) { + throw "CHANGELOG.md has no '## [$version]' section to use as the release notes." + } + $end = $lines.Count + for ($i = $start + 1; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match '^## \[') { $end = $i; break } + } + $section = if ($end -gt $start + 1) { ($lines[($start + 1)..($end - 1)] -join "`n").Trim() } + if (-not $section) { + throw "The '## [$version]' section of CHANGELOG.md is empty." + } + + $fence = '```' + $history = "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/blob/v$version/CHANGELOG.md" + $notes = @("${fence}powershell", "Update-PSResource $module", $fence, '', $section, '', + "Full history: [CHANGELOG.md]($history)") -join "`n" + Set-Content -Path ./release-notes.md -Value $notes -Encoding utf8 + "RELEASE_TAG=v$version" | Add-Content -Path $env:GITHUB_ENV + "RELEASE_TITLE=$module $version" | Add-Content -Path $env:GITHUB_ENV + Write-Host "Release notes for $module $version ($($notes.Length) characters):" + Write-Host $notes + + - name: Create the GitHub release + if: ${{ startsWith(github.ref, 'refs/tags/') && !inputs.dryRun }} + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $ErrorActionPreference = 'Stop' + # A release made by hand before this job ran is kept as written + gh release view $env:RELEASE_TAG *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host "A release for $env:RELEASE_TAG already exists; leaving it unchanged." + exit 0 + } + # --verify-tag refuses to create a tag; GitHub marks the release Latest by version + gh release create $env:RELEASE_TAG --title $env:RELEASE_TITLE --notes-file ./release-notes.md --verify-tag + if ($LASTEXITCODE -ne 0) { + throw "gh release create failed with exit code $LASTEXITCODE." + } diff --git a/CLAUDE.md b/CLAUDE.md index 9b54e8d..0b77a7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,7 +145,10 @@ the publish itself is gated by `ShouldProcess`. Bump `ModuleVersion`, update `CHANGELOG.md` and the manifest's `ReleaseNotes`, then `git tag v && git push origin v`. `.github/workflows/release.yml` -re-runs every gate, rejects a tag that disagrees with the manifest, stages, verifies and publishes. +re-runs every gate, rejects a tag that disagrees with the manifest, stages, verifies and publishes, +then creates the tag's GitHub release from that version's `## []` CHANGELOG section. That +section must exist and must not be empty, or the job fails; a dry run checks it too. A release that +already exists for the tag is left as written. The API key is the `PSGALLERY_API_KEY` repository secret, passed as an environment variable. Locally the script resolves `-ApiKey`, then `$env:PSGALLERY_API_KEY`, then a SecretManagement secret named `PSGallery-ApiKey`.