1- [Diagnostics.CodeAnalysis.SuppressMessageAttribute (
1+ # Requires -Version 7.0
2+ # Requires -Modules PSModule
3+
4+ <#
5+ . SYNOPSIS
6+ Creates or resumes a GitHub release for a versioned PowerShell module artifact.
7+
8+ . DESCRIPTION
9+ Validates the supplied module artifact, creates a GitHub release when its tag does not
10+ already exist, uploads the artifact ZIP, and reports the result on the source pull request.
11+
12+ . EXAMPLE
13+ ./release.ps1
14+
15+ Creates or resumes the release using inputs supplied by the Release-PSModule action.
16+
17+ . INPUTS
18+ None. The Release-PSModule composite action supplies values through environment variables.
19+
20+ . OUTPUTS
21+ None. The script writes ReleaseTag and ReleaseUrl to the GitHub Actions output file.
22+ #>
23+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute (
224 ' PSUseDeclaredVarsMoreThanAssignments' , ' usePRBodyAsReleaseNotes' ,
325 Justification = ' Variable is used in script blocks.'
426)]
2547[CmdletBinding ()]
2648param ()
2749
50+ $ErrorActionPreference = ' Stop'
2851$PSStyle.OutputRendering = ' Ansi'
2952
3053Import-Module - Name ' PSModule' - Force
3154
32- # region Load inputs
3355LogGroup ' Load inputs' {
3456 $env: GITHUB_REPOSITORY_NAME = $env: GITHUB_REPOSITORY -replace ' .+/'
3557
@@ -67,9 +89,7 @@ LogGroup 'Load inputs' {
6789 Write-Host " Release tag: [$releaseTag ]"
6890 Write-Host " WhatIf: [$whatIf ]"
6991}
70- # endregion Load inputs
7192
72- # region Load PR information
7393LogGroup ' Load PR information' {
7494 $githubEventJson = Get-Content - Raw $env: GITHUB_EVENT_PATH
7595 $githubEvent = $githubEventJson | ConvertFrom-Json
@@ -80,9 +100,7 @@ LogGroup 'Load PR information' {
80100 $prNumber = $pull_request.number
81101 $prHeadRef = $pull_request.head.ref
82102}
83- # endregion Load PR information
84103
85- # region Resolve version from manifest
86104LogGroup ' Resolve version from manifest' {
87105 Add-PSModulePath - Path (Split-Path - Path $modulePath - Parent)
88106 $manifestFilePath = Join-Path - Path $modulePath - ChildPath " $name .psd1"
@@ -101,100 +119,101 @@ LogGroup 'Resolve version from manifest' {
101119 Write-Host ' ::warning::Test-ModuleManifest returned warnings (e.g. unresolved RequiredModules). Continuing with data-file validation.'
102120 }
103121
104- try {
105- $manifestData = Import-PowerShellDataFile - Path $manifestFilePath
106- } catch {
107- Write-Error " Failed to import manifest data file [$manifestFilePath ]: $ ( $_.Exception.Message ) "
108- exit 1
109- }
122+ $manifestData = Import-PowerShellDataFile - Path $manifestFilePath - ErrorAction Stop
110123 $moduleVersion = $manifestData.ModuleVersion
111124 if (-not ($moduleVersion -match ' ^\d+\.\d+\.\d+$' )) {
112- Write-Error (" ModuleVersion [$moduleVersion ] must be in Major.Minor.Patch format. " +
125+ throw (" ModuleVersion [$moduleVersion ] must be in Major.Minor.Patch format. " +
113126 ' Ensure Build-PSModule has stamped the artifact with a final version.' )
114- exit 1
115127 }
116128 if ($moduleVersion -eq ' 999.0.0' ) {
117- Write-Error (' ModuleVersion is the placeholder [999.0.0]. ' +
129+ throw (' ModuleVersion is the placeholder [999.0.0]. ' +
118130 ' The artifact was not stamped with a real version by the build step.' )
119- exit 1
120131 }
121132 $prerelease = $manifestData.PrivateData.PSData.Prerelease
122133 if ([string ]::IsNullOrWhiteSpace($prerelease )) {
123134 $prerelease = ' '
124135 $createPrerelease = $false
125136 } else {
126137 if ($prerelease -notmatch ' ^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$' ) {
127- Write-Error (" Prerelease label [$prerelease ] is not a valid SemVer prerelease identifier. " +
138+ throw (" Prerelease label [$prerelease ] is not a valid SemVer prerelease identifier. " +
128139 ' It must contain only alphanumerics, hyphens, and dots as separators.' )
129- exit 1
130140 }
131141 $createPrerelease = $true
132142 }
133143
134144 $releaseType = if ($createPrerelease ) { ' New prerelease' } else { ' New release' }
135145 $publishPSVersion = if ($createPrerelease ) { " $moduleVersion -$prerelease " } else { $moduleVersion }
136146
137- [PSCustomObject ]@ {
138- ModuleVersion = $moduleVersion
139- Prerelease = $prerelease
140- CreatePrerelease = $createPrerelease
141- GalleryVersion = $publishPSVersion
142- ReleaseTag = $releaseTag
143- PRNumber = $prNumber
144- PRHeadRef = $prHeadRef
145- } | Format-List | Out-String
146-
147- # Expose release tag to subsequent steps so cleanup can exclude the just-created tag.
148- " PSMODULE_RELEASE_PSMODULE_CONTEXT_ReleaseTag=$releaseTag " | Out-File - Path $env: GITHUB_ENV - Append - Encoding utf8NoBOM
147+ Write-Host (
148+ [PSCustomObject ]@ {
149+ ModuleVersion = $moduleVersion
150+ Prerelease = $prerelease
151+ CreatePrerelease = $createPrerelease
152+ GalleryVersion = $publishPSVersion
153+ ReleaseTag = $releaseTag
154+ PRNumber = $prNumber
155+ PRHeadRef = $prHeadRef
156+ } | Format-List | Out-String
157+ )
149158}
150- # endregion Resolve version from manifest
151159
152- # region Create GitHub release with module artifact attached
153160# A zip of the built module is uploaded so the GitHub Release page exposes the exact bytes.
154161LogGroup ' Create GitHub release' {
155162 $releaseCreateCommand = @ (' release' , ' create' , $releaseTag )
156163 $notesFilePath = $null
164+ $releaseExists = $false
157165
158- if ($usePRTitleAsReleaseName -and $pull_request.title ) {
159- $releaseCreateCommand += @ (' --title' , $pull_request.title )
160- Write-Host " Using PR title as release name: [$ ( $pull_request.title ) ]"
161- } else {
162- $releaseCreateCommand += @ (' --title' , $releaseTag )
163- }
164-
165- # Build release notes content. Uses temp file to avoid escaping issues with special characters.
166- # Precedence rules for the three UsePR* parameters:
167- # 1. UsePRTitleAsNotesHeading + UsePRBodyAsReleaseNotes: Creates "# Title (#PR)\n\nBody" format.
168- # 2. UsePRBodyAsReleaseNotes only: Uses PR body as-is.
169- # 3. Fallback: Auto-generates notes via GitHub's --generate-notes.
170- if ($usePRTitleAsNotesHeading -and $usePRBodyAsReleaseNotes -and $pull_request.title -and $pull_request.body ) {
171- $notes = " # $ ( $pull_request.title ) (#$prNumber )`n`n $ ( $pull_request.body ) "
172- $notesFilePath = [System.IO.Path ]::GetTempFileName()
173- Set-Content - Path $notesFilePath - Value $notes - Encoding utf8
174- $releaseCreateCommand += @ (' --notes-file' , $notesFilePath )
175- Write-Host ' Using PR title as H1 heading with link and body as release notes'
176- } elseif ($usePRBodyAsReleaseNotes -and $pull_request.body ) {
177- $notesFilePath = [System.IO.Path ]::GetTempFileName()
178- Set-Content - Path $notesFilePath - Value $pull_request.body - Encoding utf8
179- $releaseCreateCommand += @ (' --notes-file' , $notesFilePath )
180- Write-Host ' Using PR body as release notes'
181- } else {
182- $releaseCreateCommand += @ (' --generate-notes' )
183- }
166+ if (-not $whatIf ) {
167+ $encodedReleaseTag = [System.Uri ]::EscapeDataString($releaseTag )
168+ $releaseLookupResult = gh api " repos/$env: GITHUB_REPOSITORY /releases/tags/$encodedReleaseTag " -- jq ' .html_url' 2>&1
169+ $releaseLookupExitCode = $LASTEXITCODE
184170
185- if ($createPrerelease ) {
186- $releaseCreateCommand += @ (' --target' , $prHeadRef , ' --prerelease' )
171+ if ($releaseLookupExitCode -eq 0 ) {
172+ $releaseURL = ($releaseLookupResult | Out-String ).Trim()
173+ $releaseExists = $true
174+ Write-Host " GitHub release [$releaseTag ] already exists. Resuming its artifact upload."
175+ } elseif (($releaseLookupResult | Out-String ) -notmatch ' HTTP 404' ) {
176+ throw " Unable to determine whether release [$releaseTag ] exists: $ ( $releaseLookupResult | Out-String ) "
177+ }
187178 }
188179
189- if ($whatIf ) {
190- Write-Host " WhatIf: gh $ ( $releaseCreateCommand -join ' ' ) "
191- $releaseURL = " https://github.com/$env: GITHUB_REPOSITORY /releases/tag/$releaseTag "
192- } else {
180+ if (-not $releaseExists ) {
181+ if ($usePRTitleAsReleaseName -and $pull_request.title ) {
182+ $releaseCreateCommand += @ (' --title' , $pull_request.title )
183+ Write-Host " Using PR title as release name: [$ ( $pull_request.title ) ]"
184+ } else {
185+ $releaseCreateCommand += @ (' --title' , $releaseTag )
186+ }
187+
188+ # Build release notes content. Uses a file to preserve special characters.
189+ if ($usePRTitleAsNotesHeading -and $usePRBodyAsReleaseNotes -and $pull_request.title -and $pull_request.body ) {
190+ $notes = " # $ ( $pull_request.title ) (#$prNumber )`n`n $ ( $pull_request.body ) "
191+ $notesFilePath = [System.IO.Path ]::GetTempFileName()
192+ Set-Content - Path $notesFilePath - Value $notes - Encoding utf8
193+ $releaseCreateCommand += @ (' --notes-file' , $notesFilePath )
194+ Write-Host ' Using PR title as H1 heading with link and body as release notes'
195+ } elseif ($usePRBodyAsReleaseNotes -and $pull_request.body ) {
196+ $notesFilePath = [System.IO.Path ]::GetTempFileName()
197+ Set-Content - Path $notesFilePath - Value $pull_request.body - Encoding utf8
198+ $releaseCreateCommand += @ (' --notes-file' , $notesFilePath )
199+ Write-Host ' Using PR body as release notes'
200+ } else {
201+ $releaseCreateCommand += @ (' --generate-notes' )
202+ }
203+
204+ if ($createPrerelease ) {
205+ $releaseCreateCommand += @ (' --target' , $prHeadRef , ' --prerelease' )
206+ }
207+
193208 try {
194- $releaseURL = gh @releaseCreateCommand
195- if ($LASTEXITCODE -ne 0 ) {
196- Write-Error " Failed to create the release [$releaseTag ]."
197- exit $LASTEXITCODE
209+ if ($whatIf ) {
210+ Write-Host " WhatIf: gh $ ( $releaseCreateCommand -join ' ' ) "
211+ $releaseURL = " https://github.com/$env: GITHUB_REPOSITORY /releases/tag/$releaseTag "
212+ } else {
213+ $releaseURL = gh @releaseCreateCommand
214+ if ($LASTEXITCODE -ne 0 ) {
215+ throw " Failed to create the release [$releaseTag ]."
216+ }
198217 }
199218 } finally {
200219 if ($notesFilePath -and (Test-Path - Path $notesFilePath )) {
@@ -218,8 +237,7 @@ LogGroup 'Create GitHub release' {
218237 try {
219238 gh release upload $releaseTag $zipPath -- clobber
220239 if ($LASTEXITCODE -ne 0 ) {
221- Write-Error " Failed to upload module artifact to release [$releaseTag ]."
222- exit $LASTEXITCODE
240+ throw " Failed to upload module artifact to release [$releaseTag ]."
223241 }
224242 Write-Host " ::notice title=📦 Attached module artifact to release::$zipFileName "
225243 } finally {
@@ -234,12 +252,14 @@ LogGroup 'Create GitHub release' {
234252 } else {
235253 gh pr comment $prNumber - b " ✅ $releaseType `: GitHub - [$name $releaseTag ]($releaseURL )"
236254 if ($LASTEXITCODE -ne 0 ) {
237- Write-Error ' Failed to comment on the pull request.'
238- exit $LASTEXITCODE
255+ throw ' Failed to comment on the pull request.'
239256 }
240257 }
258+ if ($env: GITHUB_OUTPUT ) {
259+ " ReleaseTag=$releaseTag " | Out-File - Path $env: GITHUB_OUTPUT - Append - Encoding utf8NoBOM
260+ " ReleaseUrl=$releaseURL " | Out-File - Path $env: GITHUB_OUTPUT - Append - Encoding utf8NoBOM
261+ }
241262 Write-Host " ::notice title=✅ $releaseType `: GitHub - $name $releaseTag ::$releaseURL "
242263}
243- # endregion Create GitHub release
244264
245265Write-Host " Release creation complete. Version: [$releaseTag ]"
0 commit comments