@@ -51,8 +51,9 @@ $ErrorActionPreference = 'Stop'
5151$PSStyle.OutputRendering = ' Ansi'
5252
5353Import-Module - Name ' PSModule' - Force
54+ Import-Module - Name (Join-Path - Path $PSScriptRoot - ChildPath ' Release-PSModule.Helpers.psm1' ) - Force
5455
55- LogGroup ' Load inputs' {
56+ Set-GitHubLogGroup ' Load inputs' {
5657 $env: GITHUB_REPOSITORY_NAME = $env: GITHUB_REPOSITORY -replace ' .+/'
5758
5859 $name = if ([string ]::IsNullOrEmpty($env: PSMODULE_RELEASE_PSMODULE_INPUT_Name )) {
@@ -91,7 +92,7 @@ LogGroup 'Load inputs' {
9192 Write-Host " WhatIf: [$whatIf ]"
9293}
9394
94- LogGroup ' Load release context' {
95+ Set-GitHubLogGroup ' Load release context' {
9596 $githubEventJson = Get-Content - Raw $env: GITHUB_EVENT_PATH
9697 $githubEvent = $githubEventJson | ConvertFrom-Json
9798 $pullRequestJson = $env: PSMODULE_RELEASE_PSMODULE_INPUT_PullRequest
@@ -122,7 +123,7 @@ LogGroup 'Load release context' {
122123 }
123124}
124125
125- LogGroup ' Resolve version from manifest' {
126+ Set-GitHubLogGroup ' Resolve version from manifest' {
126127 Add-PSModulePath - Path (Split-Path - Path $modulePath - Parent)
127128 $manifestFilePath = Join-Path - Path $modulePath - ChildPath " $name .psd1"
128129 Write-Host " Module manifest file path: [$manifestFilePath ]"
@@ -180,20 +181,55 @@ LogGroup 'Resolve version from manifest' {
180181}
181182
182183# A zip of the built module is uploaded so the GitHub Release page exposes the exact bytes.
183- LogGroup ' Create GitHub release' {
184+ Set-GitHubLogGroup ' Create GitHub release' {
184185 $releaseCreateCommand = @ (' release' , ' create' , $releaseTag )
185186 $notesFilePath = $null
186187 $releaseExists = $false
188+ $existingRelease = $null
187189
188190 if (-not $whatIf ) {
189191 $encodedReleaseTag = [System.Uri ]::EscapeDataString($releaseTag )
190- $releaseLookupResult = gh api " repos/$env: GITHUB_REPOSITORY /releases/tags/$encodedReleaseTag " -- jq ' .html_url ' 2>&1
192+ $releaseLookupResult = gh api " repos/$env: GITHUB_REPOSITORY /releases/tags/$encodedReleaseTag " 2>&1
191193 $releaseLookupExitCode = $LASTEXITCODE
192194
193195 if ($releaseLookupExitCode -eq 0 ) {
194- $releaseURL = ($releaseLookupResult | Out-String ).Trim()
196+ $existingRelease = ($releaseLookupResult | Out-String ) | ConvertFrom-Json
197+ if ($existingRelease.tag_name -cne $releaseTag -or
198+ [string ]::IsNullOrWhiteSpace($existingRelease.html_url ) -or
199+ $existingRelease.PSObject.Properties.Name -notcontains ' assets' ) {
200+ throw " Cannot verify the existing release metadata for [$releaseTag ]."
201+ }
202+ $releaseURL = $existingRelease.html_url
195203 $releaseExists = $true
196- Write-Host " GitHub release [$releaseTag ] already exists. Resuming its artifact upload."
204+ $sourceCommit = if (-not [string ]::IsNullOrWhiteSpace($commitSha )) { $commitSha } else { $env: GITHUB_SHA }
205+ if ([string ]::IsNullOrWhiteSpace($sourceCommit )) {
206+ throw ' CommitSha or the original GITHUB_SHA is required to establish immutable ownership before resuming a release.'
207+ }
208+ $tagParameters = @ {
209+ Tag = $releaseTag
210+ GetTagReference = {
211+ param ($Tag )
212+ $encodedTag = [Uri ]::EscapeDataString($Tag )
213+ $response = gh api " repos/$env: GITHUB_REPOSITORY /git/ref/tags/$encodedTag "
214+ if ($LASTEXITCODE -ne 0 ) {
215+ throw " Cannot read release tag [$Tag ] (gh exited $LASTEXITCODE )."
216+ }
217+ ($response | ConvertFrom-Json ).object
218+ }
219+ GetTagObject = {
220+ param ($TagSha )
221+ $response = gh api " repos/$env: GITHUB_REPOSITORY /git/tags/$TagSha "
222+ if ($LASTEXITCODE -ne 0 ) {
223+ throw " Cannot resolve annotated tag [$TagSha ] (gh exited $LASTEXITCODE )."
224+ }
225+ ($response | ConvertFrom-Json ).object
226+ }
227+ }
228+ $tagCommit = Resolve-PSModuleReleaseTagCommit @tagParameters
229+ if ($tagCommit -cne $sourceCommit ) {
230+ throw " Release tag [$releaseTag ] belongs to [$tagCommit ], not the original source [$sourceCommit ]. Refusing to resume."
231+ }
232+ Write-Host " GitHub release [$releaseTag ] belongs to [$sourceCommit ]. Checking its artifact before resuming."
197233 } elseif (($releaseLookupResult | Out-String ) -notmatch ' HTTP 404' ) {
198234 throw " Unable to determine whether release [$releaseTag ] exists: $ ( $releaseLookupResult | Out-String ) "
199235 }
@@ -253,26 +289,44 @@ LogGroup 'Create GitHub release' {
253289
254290 # Attach the built module as a release artifact so consumers can download the exact bytes.
255291 $zipFileName = " $name -$publishPSVersion .zip"
256- $zipPath = Join-Path - Path ([System.IO.Path ]::GetTempPath()) - ChildPath $zipFileName
257- if (Test-Path - Path $zipPath ) {
258- Remove-Item - Path $zipPath - Force
259- }
292+ $zipDirectory = Join-Path - Path ([System.IO.Path ]::GetTempPath()) - ChildPath " PSModuleRelease-$ ( [Guid ]::NewGuid()) "
293+ $zipPath = Join-Path - Path $zipDirectory - ChildPath $zipFileName
260294 if ($whatIf ) {
261- Write-Host " WhatIf: Compress-Archive -Path $modulePath -DestinationPath $zipPath -Force "
262- Write-Host " WhatIf: gh release upload $releaseTag $zipPath --clobber "
295+ Write-Host " WhatIf: Compress-Archive -Path $modulePath -DestinationPath $zipPath "
296+ Write-Host " WhatIf: gh release upload $releaseTag $zipPath "
263297 } else {
264- Write-Host " Compressing module to [$zipPath ]"
265- Compress-Archive - Path $modulePath - DestinationPath $zipPath - Force
298+ $null = New-Item - Path $zipDirectory - ItemType Directory
266299 try {
267- gh release upload $releaseTag $zipPath -- clobber
268- if ($LASTEXITCODE -ne 0 ) {
269- throw " Failed to upload module artifact to release [$releaseTag ]."
300+ Write-Host " Compressing module to [$zipPath ]"
301+ Compress-Archive - Path $modulePath - DestinationPath $zipPath
302+ $matchingAssets = @ ($existingRelease.assets | Where-Object { $_.name -ceq $zipFileName })
303+ if ($matchingAssets.Count -gt 1 ) {
304+ throw " Release [$releaseTag ] contains ambiguous assets named [$zipFileName ]."
305+ }
306+ if ($matchingAssets.Count -eq 1 ) {
307+ $digest = [regex ]::Match([string ]$matchingAssets [0 ].digest, ' ^sha256:([0-9A-Fa-f]{64})$' )
308+ if (-not $digest.Success ) {
309+ throw " Cannot verify the SHA-256 digest of the already-published asset [$zipFileName ]. Refusing to replace it."
310+ }
311+ $localHash = (Get-FileHash - LiteralPath $zipPath - Algorithm SHA256).Hash
312+ if ($localHash -ne $digest.Groups [1 ].Value) {
313+ throw " Asset [$zipFileName ] already has different published contents. Refusing to overwrite it."
314+ }
315+ Write-Host " Existing asset [$zipFileName ] has identical contents; retaining its immutable bytes."
316+ } else {
317+ gh release upload $releaseTag $zipPath
318+ if ($LASTEXITCODE -ne 0 ) {
319+ throw " Failed to upload module artifact to release [$releaseTag ]."
320+ }
321+ Write-Host " ::notice title=📦 Attached module artifact to release::$zipFileName "
270322 }
271- Write-Host " ::notice title=📦 Attached module artifact to release::$zipFileName "
272323 } finally {
273324 if (Test-Path - Path $zipPath ) {
274325 Remove-Item - Path $zipPath - Force
275326 }
327+ if (Test-Path - Path $zipDirectory ) {
328+ Remove-Item - Path $zipDirectory - Force
329+ }
276330 }
277331 }
278332
0 commit comments