Skip to content

Commit aa86256

Browse files
Reserve immutable module versions independently of release tag prefixes
1 parent 3405467 commit aa86256

12 files changed

Lines changed: 376 additions & 60 deletions

‎.github/actions/Publish-PSModule/action.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ inputs:
2020
ReleaseTag:
2121
description: Exact GitHub tag already resolved for the tested module artifact.
2222
required: true
23+
VersionPrefix:
24+
description: Configured prefix used by the existing resolver; verifies the tag against the tested module version.
25+
required: false
26+
default: v
2327
CommitSha:
2428
description: Immutable commit that produced the tested module artifact; defaults to the original workflow SHA.
2529
required: false
@@ -63,6 +67,7 @@ runs:
6367
PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY: ${{ inputs.PSGALLERY_API_KEY }}
6468
PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest: ${{ inputs.PullRequest }}
6569
PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseTag: ${{ inputs.ReleaseTag }}
70+
PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix: ${{ inputs.VersionPrefix }}
6671
PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha: ${{ inputs.CommitSha || github.sha }}
6772
PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }}
6873
run: ${{ github.action_path }}/src/publish.ps1

‎.github/actions/Publish-PSModule/src/Publish-PSModule.Helpers.psm1‎

Lines changed: 116 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,19 @@ function Get-PSModulePublicationReader {
330330
}
331331
$response.Value.object
332332
}.GetNewClosure()
333+
GetVersionReference = {
334+
param($Name, $Version)
335+
$tag = Get-PSModulePublicationVersionTag -ModuleName $Name -GalleryVersion $Version
336+
$encodedTag = [Uri]::EscapeDataString($tag)
337+
$response = Invoke-PSModulePublicationApi -Endpoint "repos/$Repository/git/ref/tags/$encodedTag" -AllowMissing
338+
if ($null -eq $response) {
339+
return
340+
}
341+
if ($response.Value.ref -cne "refs/tags/$tag" -or $null -eq $response.Value.object) {
342+
throw "Cannot verify the shared module/version ownership coordinate [$tag]."
343+
}
344+
$response.Value.object
345+
}.GetNewClosure()
333346
GetTagObject = {
334347
param($Sha)
335348
$response = (Invoke-PSModulePublicationApi -Endpoint "repos/$Repository/git/tags/$Sha").Value
@@ -391,7 +404,11 @@ function Get-PSModulePublicationState {
391404
[hashtable] $Reader
392405
)
393406

394-
foreach ($name in @('GetSource', 'GetTagReference', 'GetTagObject', 'GetRelease', 'GetPackage', 'GetPackageArtifactDigest')) {
407+
$readerNames = @(
408+
'GetSource', 'GetTagReference', 'GetVersionReference', 'GetTagObject',
409+
'GetRelease', 'GetPackage', 'GetPackageArtifactDigest'
410+
)
411+
foreach ($name in $readerNames) {
395412
if ($Reader[$name] -isnot [scriptblock]) {
396413
throw "The publication reader [$name] is missing."
397414
}
@@ -425,25 +442,59 @@ function Get-PSModulePublicationState {
425442
throw "Release tag [$($expected.ReleaseTag)] belongs to [$tagCommit], not source [$($expected.CommitSha)]."
426443
}
427444
$claim = Get-PSModulePublicationClaim -Message $tagEvidence.Annotation.message
445+
$versionTag = Get-PSModulePublicationVersionTag -ModuleName $expected.ModuleName -GalleryVersion $expected.GalleryVersion
446+
$versionEvidence = @{ Reference = $null; Annotation = $null }
447+
$versionParameters = @{
448+
Tag = $versionTag
449+
AllowMissing = $true
450+
GetTagReference = {
451+
param($Tag)
452+
if ($Tag -cne $versionTag) { throw 'Unexpected package ownership coordinate.' }
453+
$versionEvidence.Reference = & $Reader.GetVersionReference $expected.ModuleName $expected.GalleryVersion
454+
$versionEvidence.Reference
455+
}.GetNewClosure()
456+
GetTagObject = {
457+
param($Sha)
458+
$annotation = & $Reader.GetTagObject $Sha
459+
if ($null -eq $versionEvidence.Annotation) { $versionEvidence.Annotation = $annotation }
460+
$annotation.object
461+
}.GetNewClosure()
462+
}
463+
$versionCommit = Resolve-PSModuleReleaseTagCommit @versionParameters
464+
if ($null -ne $versionCommit -and $versionCommit -cne $expected.CommitSha) {
465+
throw "Module/version coordinate [$versionTag] belongs to another source [$versionCommit]."
466+
}
467+
$versionClaim = Get-PSModulePublicationClaim -Message $versionEvidence.Annotation.message
428468
$release = & $Reader.GetRelease $expected.ReleaseTag
429469
$package = & $Reader.GetPackage $expected.ModuleName $expected.GalleryVersion
430470
if ($null -eq $tagCommit) {
431471
if ($null -ne $release -or $null -ne $package) {
432472
throw "Existing release or Gallery version [$($expected.GalleryVersion)] has no verifiable source/artifact ownership proof."
433473
}
434-
} else {
435-
if ($null -eq $claim -or $tagEvidence.Annotation.tag -cne $expected.ReleaseTag) {
436-
throw "The existing tag [$($expected.ReleaseTag)] has no verifiable publication ownership proof."
437-
}
438-
if ($claim.Repository -ine $expected.Repository -or $claim.PullRequest -ne $expected.PullRequest) {
474+
} elseif ($null -eq $claim -or $tagEvidence.Annotation.tag -cne $expected.ReleaseTag) {
475+
throw "The existing tag [$($expected.ReleaseTag)] has no verifiable publication ownership proof."
476+
}
477+
if ($null -ne $versionCommit -and
478+
($null -eq $versionClaim -or $versionEvidence.Annotation.tag -cne $expected.ReleaseTag)) {
479+
throw "The module/version coordinate [$versionTag] has no verifiable publication proof."
480+
}
481+
if ($null -ne $package -and $null -eq $versionCommit) {
482+
throw "The existing Gallery package [$($expected.GalleryVersion)] has no shared module/version ownership proof."
483+
}
484+
if ($null -ne $tagCommit -and $null -ne $versionCommit -and
485+
$tagEvidence.Reference.sha -cne $versionEvidence.Reference.sha) {
486+
throw 'The release tag and module/version coordinate reference different immutable claims.'
487+
}
488+
foreach ($proof in @($claim, $versionClaim) | Where-Object { $null -ne $_ }) {
489+
if ($proof.Repository -ine $expected.Repository -or $proof.PullRequest -ne $expected.PullRequest) {
439490
throw 'The publication claim belongs to a different repository or producing pull request.'
440491
}
441-
foreach ($name in @('CommitSha', 'ModuleName', 'GalleryVersion', 'ReleaseTag', 'ReleaseType', 'AssetName')) {
442-
if ($claim.$name -cne $expected.$name) {
492+
foreach ($name in @('CommitSha', 'ModuleName', 'GalleryVersion', 'VersionPrefix', 'ReleaseTag', 'ReleaseType', 'AssetName')) {
493+
if ($proof.$name -cne $expected.$name) {
443494
throw "The publication claim has conflicting [$name] ownership."
444495
}
445496
}
446-
if ($claim.ArtifactSha256 -cne $expected.ArtifactSha256 -or $claim.AssetSha256 -cne $expected.AssetSha256) {
497+
if ($proof.ArtifactSha256 -cne $expected.ArtifactSha256 -or $proof.AssetSha256 -cne $expected.AssetSha256) {
447498
throw 'The claimed version belongs to different tested artifact or release asset contents.'
448499
}
449500
}
@@ -487,10 +538,11 @@ function Get-PSModulePublicationState {
487538
}
488539
}
489540
[pscustomobject]@{
490-
TagSha = $tagEvidence.Reference.sha
491-
Claim = $claim
492-
Release = $release
493-
Package = $package
541+
TagSha = $tagEvidence.Reference.sha
542+
VersionTagSha = $versionEvidence.Reference.sha
543+
Claim = if ($null -ne $versionClaim) { $versionClaim } else { $claim }
544+
Release = $release
545+
Package = $package
494546
}
495547
}
496548

@@ -538,35 +590,61 @@ function New-PSModulePublicationTag {
538590
if ($null -ne $State.TagSha -and $current.TagSha -cne $State.TagSha) {
539591
throw 'The existing publication tag changed or disappeared after preflight. Refusing Gallery publication.'
540592
}
541-
if ($null -ne $current.TagSha) {
593+
if ($null -ne $State.VersionTagSha -and $current.VersionTagSha -cne $State.VersionTagSha) {
594+
throw 'The shared module/version claim changed or disappeared after preflight. Refusing publication.'
595+
}
596+
if ($null -ne $current.TagSha -and $null -ne $current.VersionTagSha) {
542597
return $current
543598
}
544-
if (-not $PSCmdlet.ShouldProcess("$($Claim.Repository):$($Claim.ReleaseTag)", 'Create source-bound annotated version tag')) {
599+
if (-not $PSCmdlet.ShouldProcess("$($Claim.Repository):$($Claim.ModuleName):$($Claim.GalleryVersion)",
600+
'Create shared module/version ownership and immutable release tag')) {
545601
return
546602
}
547-
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($Claim | ConvertTo-Json -Compress)))
548-
$message = "<!-- PSModule.Publication:v1:$encoded -->"
549-
$tagFields = @{
550-
tag = $Claim.ReleaseTag
551-
message = $message
552-
object = $Claim.CommitSha
553-
type = 'commit'
554-
}
555-
$tag = (Invoke-PSModulePublicationApi -Endpoint "repos/$($Claim.Repository)/git/tags" `
556-
-Method POST -Field $tagFields).Value
557-
if ($tag.sha -notmatch '^[0-9a-f]{40}$' -or $tag.tag -cne $Claim.ReleaseTag -or
558-
$tag.object.type -cne 'commit' -or $tag.object.sha -cne $Claim.CommitSha -or $tag.message -cne $message) {
559-
throw 'The create-only annotated tag response did not preserve the exact publication claim.'
560-
}
561-
$reference = Invoke-PSModulePublicationApi -Endpoint "repos/$($Claim.Repository)/git/refs" `
562-
-Method POST -Field @{ ref = "refs/tags/$($Claim.ReleaseTag)"; sha = $tag.sha } -AllowConflict
563-
if ($reference.StatusCode -ne 201 -and $reference.Value.message -cne 'Reference already exists') {
564-
throw "Cannot atomically claim release tag [$($Claim.ReleaseTag)] (HTTP $($reference.StatusCode))."
603+
$claimSha = $current.VersionTagSha
604+
if ($null -eq $claimSha) {
605+
$claimSha = $current.TagSha
565606
}
566-
$current = Get-PSModulePublicationState -ExpectedClaim $Claim -Reader $Reader
567-
if ($null -eq $current.TagSha -or
568-
($reference.StatusCode -eq 201 -and $current.TagSha -cne $tag.sha)) {
569-
throw 'The publication tag changed during the create-only claim. Refusing Gallery publication.'
607+
if ($null -eq $claimSha) {
608+
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($Claim | ConvertTo-Json -Compress)))
609+
$message = "<!-- PSModule.Publication:v1:$encoded -->"
610+
$tagFields = @{
611+
tag = $Claim.ReleaseTag
612+
message = $message
613+
object = $Claim.CommitSha
614+
type = 'commit'
615+
}
616+
$tag = (Invoke-PSModulePublicationApi -Endpoint "repos/$($Claim.Repository)/git/tags" `
617+
-Method POST -Field $tagFields).Value
618+
if ($tag.sha -notmatch '^[0-9a-f]{40}$' -or $tag.tag -cne $Claim.ReleaseTag -or
619+
$tag.object.type -cne 'commit' -or $tag.object.sha -cne $Claim.CommitSha -or $tag.message -cne $message) {
620+
throw 'The annotated object did not preserve the exact source/context/artifact claim.'
621+
}
622+
$claimSha = $tag.sha
623+
}
624+
if ($null -eq $current.VersionTagSha) {
625+
$versionTag = Get-PSModulePublicationVersionTag -ModuleName $Claim.ModuleName -GalleryVersion $Claim.GalleryVersion
626+
$reference = Invoke-PSModulePublicationApi -Endpoint "repos/$($Claim.Repository)/git/refs" `
627+
-Method POST -Field @{ ref = "refs/tags/$versionTag"; sha = $claimSha } -AllowConflict
628+
if ($reference.StatusCode -ne 201 -and $reference.Value.message -cne 'Reference already exists') {
629+
throw "Cannot atomically reserve module/version [$versionTag] (HTTP $($reference.StatusCode))."
630+
}
631+
$current = Get-PSModulePublicationState -ExpectedClaim $Claim -Reader $Reader
632+
if ($null -eq $current.VersionTagSha -or
633+
($reference.StatusCode -eq 201 -and $current.VersionTagSha -cne $claimSha)) {
634+
throw 'The module/version coordinate changed during its create-only claim. Refusing publication.'
635+
}
636+
$claimSha = $current.VersionTagSha
637+
}
638+
if ($null -eq $current.TagSha) {
639+
$reference = Invoke-PSModulePublicationApi -Endpoint "repos/$($Claim.Repository)/git/refs" `
640+
-Method POST -Field @{ ref = "refs/tags/$($Claim.ReleaseTag)"; sha = $claimSha } -AllowConflict
641+
if ($reference.StatusCode -ne 201 -and $reference.Value.message -cne 'Reference already exists') {
642+
throw "Cannot atomically create release tag [$($Claim.ReleaseTag)] (HTTP $($reference.StatusCode))."
643+
}
644+
}
645+
$verified = Get-PSModulePublicationState -ExpectedClaim $Claim -Reader $Reader
646+
if ($verified.TagSha -cne $claimSha -or $verified.VersionTagSha -cne $claimSha) {
647+
throw 'The release tag or shared module/version coordinate changed before publication.'
570648
}
571-
$current
649+
$verified
572650
}

‎.github/actions/Publish-PSModule/src/publish.ps1‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ LogGroup 'Load inputs' {
4545
$psGalleryApiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY
4646
$whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true'
4747
$releaseTag = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseTag
48+
$versionPrefix = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix
4849
$sourceCommit = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha
4950
if ([string]::IsNullOrWhiteSpace($sourceCommit)) {
5051
$sourceCommit = $env:GITHUB_SHA
@@ -57,6 +58,7 @@ LogGroup 'Load inputs' {
5758
}
5859

5960
Write-Host "Module name: [$name]"
61+
Write-Host "Version prefix: [$versionPrefix]"
6062
Write-Host "Module path: [$modulePath]"
6163
Write-Host "WhatIf: [$whatIf]"
6264
}
@@ -169,6 +171,7 @@ try {
169171
CommitSha = $sourceCommit
170172
ModuleName = $name
171173
GalleryVersion = $publishPSVersion
174+
VersionPrefix = $versionPrefix
172175
ReleaseTag = $releaseTag
173176
ReleaseType = if ($createPrerelease) { 'Prerelease' } else { 'Release' }
174177
ArtifactSha256 = $artifactDigest
@@ -195,6 +198,7 @@ try {
195198
$publicationState = New-PSModulePublicationTag -Claim $claim -State $publicationState -Reader $publicationReader
196199
$currentState = Get-PSModulePublicationState -ExpectedClaim $claim -Reader $publicationReader
197200
if ($currentState.TagSha -cne $publicationState.TagSha -or
201+
$currentState.VersionTagSha -cne $publicationState.VersionTagSha -or
198202
(Get-PSModuleArtifactDigest -Path $modulePath) -cne $claim.ArtifactSha256) {
199203
throw 'The publication claim or tested artifact changed before the Gallery write. Refusing publication.'
200204
}

0 commit comments

Comments
 (0)