Skip to content

Commit c665a9b

Browse files
Discover shared-only claims and verify actual historical release baselines
Deduplicate immutable alias refs and stop unpublished source claims before selecting another version. Use actual historical tag and release evidence for new-source completeness without changing configured-prefix binding or bump policy. (cherry picked from commit 3bdda65)
1 parent 2c4ecf6 commit c665a9b

4 files changed

Lines changed: 137 additions & 53 deletions

File tree

‎.github/actions/Resolve-PSModuleVersion/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Resolves the module version once in Plan, before Build. Existing configured bump labels, preview behavior, version prefixes, and prerelease settings remain the decision authority.
44

5-
For a stable retry, the resolver searches actual version-tag claims for the original immutable consumer source and producing repository/PR or direct-release context. It uses the shared [publication preflight](../Publish-PSModule/README.md) to verify the exact raw Gallery package and any GitHub release/asset evidence before choosing the original version. Numeric equality, release names, mutable targets, and latest-version hints are not ownership proof.
5+
For a stable retry, the resolver searches both release-tag and shared module/version claims for the original immutable consumer source and producing repository/PR or direct-release context. Refs pointing to the same immutable claim are deduplicated. It uses the shared [publication preflight](../Publish-PSModule/README.md) to verify the exact raw Gallery package and any GitHub release/asset evidence before choosing the original version. Numeric equality, release names, mutable targets, and latest-version hints are not ownership proof.
66

77
## Inputs
88

@@ -22,6 +22,8 @@ The existing `Version`, `Prerelease`, `FullVersion`, `ReleaseType`, and `CreateR
2222

2323
Only one exact source/context claim with matching raw package bytes can select a recovery version. An already-created GitHub release with a missing upload can resume through [Release-PSModule](../Release-PSModule/README.md). The publisher retains the proven Gallery package and verifies the rebuilt/tested artifact again before any write.
2424

25+
An unpublished shared-only claim still stops Plan for original-job retry, even after a later source completes another version. A new source checks baseline completeness using the actual historical tag and release, not a tag reconstructed with the current configured prefix. Recovery and new claims still require exact configured-prefix binding; ambiguous historical baselines stop explicitly.
26+
2527
| Failure | Safe behavior |
2628
| --- | --- |
2729
| Gallery-only package has missing, foreign, or legacy proof | Stop version resolution; do not reuse the package or invent a fallback number. |

‎.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1‎

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ function Get-PSModuleGalleryRecoveryVersion {
761761
Recover a stable version only from exact source/context and raw package proof.
762762
763763
.DESCRIPTION
764-
Searches actual version-tag claims, not latest hints or numeric equality.
764+
Searches actual release and shared-coordinate claims, not latest hints or numeric equality.
765765
The canonical preflight validates the original package/release evidence before Build.
766766
A claimed source without package proof, conflicting context, or multiple versions fails.
767767
@@ -806,7 +806,8 @@ function Get-PSModuleGalleryRecoveryVersion {
806806
)
807807

808808
$number = 0L
809-
if ($Context.Repository -notmatch '^[^/\s]+/[^/\s]+$' -or
809+
if ($Context.Repository -isnot [string] -or $Context.CommitSha -isnot [string] -or
810+
$Context.PullRequest -is [array] -or $Context.Repository -notmatch '^[^/\s]+/[^/\s]+$' -or
810811
$Context.CommitSha -notmatch '^[0-9a-f]{40}$' -or
811812
-not [long]::TryParse([string]$Context.PullRequest, [ref]$number) -or $number -lt 0) {
812813
throw 'Immutable repository/PR/source context is required for Gallery recovery proof.'
@@ -824,25 +825,61 @@ function Get-PSModuleGalleryRecoveryVersion {
824825
if ($source -cne $Context.CommitSha) {
825826
throw 'The original consumer source could not be verified before Build.'
826827
}
827-
$owned = [Collections.Generic.List[object]]::new()
828+
$owned = [Collections.Generic.Dictionary[string, object]]::new([StringComparer]::Ordinal)
829+
$baselineTags = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
830+
$baselineClaims = [Collections.Generic.Dictionary[string, object]]::new([StringComparer]::Ordinal)
828831
$references = @(& $Reader.GetTagList)
829832
$prefix = [string]$Configuration.VersionPrefix
833+
$coordinatePrefix = 'PSModule-Publication/'
834+
$stablePattern = '^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$'
835+
$versionPattern = '^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$'
836+
$galleryVersion = $null
837+
if ($PSGalleryVersion.ToString() -ne '0.0.0') {
838+
$galleryVersion = "$($PSGalleryVersion.Major).$($PSGalleryVersion.Minor).$($PSGalleryVersion.Patch)"
839+
}
830840
foreach ($reference in $references) {
831841
if ($reference.ref -isnot [string] -or -not $reference.ref.StartsWith('refs/tags/') -or
832-
$null -eq $reference.object) {
842+
$null -eq $reference.object -or $reference.object -is [array] -or
843+
$reference.object.sha -isnot [string] -or $reference.object.type -isnot [string] -or
844+
$reference.object.sha -notmatch '^[0-9a-f]{40}$' -or
845+
$reference.object.type -cnotin @('tag', 'commit')) {
833846
throw 'The publication tag enumeration returned unverifiable reference metadata.'
834847
}
835848
$tag = $reference.ref.Substring('refs/tags/'.Length)
836-
if (-not $tag.StartsWith($prefix, [StringComparison]::Ordinal) -or
837-
$tag.Substring($prefix.Length) -notmatch '^\d+\.\d+\.\d+$') {
838-
continue
849+
if ($null -ne $galleryVersion -and -not $tag.StartsWith('PSModule-Publication/', [StringComparison]::Ordinal) -and
850+
$tag -cmatch "^\D*$([regex]::Escape($galleryVersion))$") {
851+
$null = $baselineTags.Add($tag)
852+
}
853+
$isCoordinate = $tag.StartsWith($coordinatePrefix, [StringComparison]::Ordinal)
854+
if ($isCoordinate) {
855+
$coordinateParts = $tag.Substring($coordinatePrefix.Length).Split('/')
856+
if ($coordinateParts.Count -ne 2) {
857+
throw "The module/version coordinate [$tag] has unverifiable reference metadata."
858+
}
859+
$tagVersion = $coordinateParts[1]
860+
if ($tagVersion -notmatch $versionPattern) {
861+
throw "The module/version coordinate [$tag] has unverifiable version metadata."
862+
}
863+
$moduleCoordinate = Get-PSModulePublicationVersionTag -ModuleName $ModuleName -GalleryVersion $tagVersion
864+
if ($tag -cne $moduleCoordinate) { continue }
865+
if ($tagVersion.Contains('-')) { continue }
866+
} else {
867+
if (-not $tag.StartsWith($prefix, [StringComparison]::Ordinal) -or
868+
$tag.Substring($prefix.Length) -notmatch $stablePattern) {
869+
continue
870+
}
871+
$tagVersion = $tag.Substring($prefix.Length)
839872
}
840873
$evidence = @{ Annotation = $null; Reference = $null }
841874
$parameters = @{
842875
Tag = $tag
843876
GetTagReference = {
844877
param($Tag)
845-
$evidence.Reference = & $Reader.GetTagReference $Tag
878+
if ($isCoordinate) {
879+
$evidence.Reference = & $Reader.GetVersionReference $ModuleName $tagVersion
880+
} else {
881+
$evidence.Reference = & $Reader.GetTagReference $Tag
882+
}
846883
$evidence.Reference
847884
}.GetNewClosure()
848885
GetTagObject = {
@@ -853,61 +890,101 @@ function Get-PSModuleGalleryRecoveryVersion {
853890
}.GetNewClosure()
854891
}
855892
$tagCommit = Resolve-PSModuleReleaseTagCommit @parameters
893+
$claim = $null
894+
if ($isCoordinate -or $tagCommit -ceq $Context.CommitSha) {
895+
$claim = Get-PSModulePublicationClaim -Message $evidence.Annotation.message
896+
if ($null -eq $claim) {
897+
throw "Existing source-bound claim [$tag] has no verifiable artifact/release-context proof. Do not invent a fallback version."
898+
}
899+
if ($claim.CommitSha -cne $tagCommit -or $claim.Repository -ine $Context.Repository -or
900+
$claim.ModuleName -cne $ModuleName -or $claim.GalleryVersion -cne $tagVersion -or
901+
$claim.ReleaseType -cne 'Release') {
902+
throw "The immutable claim [$tag] has conflicting source/module/version proof."
903+
}
904+
if ($isCoordinate) {
905+
$expectedCoordinate = Get-PSModulePublicationVersionTag -ModuleName $claim.ModuleName -GalleryVersion $claim.GalleryVersion
906+
if ($tag -cne $expectedCoordinate) {
907+
throw "The shared claim [$tag] does not bind its exact module/version coordinate."
908+
}
909+
} elseif ($claim.ReleaseTag -cne $tag) {
910+
throw "The source-bound tag [$tag] has conflicting release coordinates."
911+
}
912+
if ($null -ne $galleryVersion -and $claim.GalleryVersion -ceq $galleryVersion) {
913+
$null = $baselineTags.Add($claim.ReleaseTag)
914+
$baselineClaims[$claim.ReleaseTag] = $claim
915+
}
916+
}
856917
if ($tagCommit -cne $Context.CommitSha) {
857918
continue
858919
}
859-
$claim = Get-PSModulePublicationClaim -Message $evidence.Annotation.message
860-
if ($null -eq $claim) {
861-
throw "Existing source-bound tag [$tag] has no verifiable artifact/release-context proof. Do not invent a fallback version."
862-
}
863920
if ($claim.Repository -ine $Context.Repository -or $claim.PullRequest -ne $number -or
864921
$claim.CommitSha -cne $Context.CommitSha -or $claim.ModuleName -cne $ModuleName -or
865922
$claim.VersionPrefix -cne $prefix -or
866-
$claim.ReleaseType -cne 'Release' -or $claim.ReleaseTag -cne $tag -or
867-
"$prefix$($claim.GalleryVersion)" -cne $tag) {
923+
$claim.ReleaseType -cne 'Release' -or "$prefix$($claim.GalleryVersion)" -cne $claim.ReleaseTag) {
868924
throw "The source-bound tag [$tag] belongs to a different module or release context."
869925
}
870926
$state = Get-PSModulePublicationState -ExpectedClaim $claim -Reader $Reader
871-
if ($state.TagSha -cne $evidence.Reference.sha) {
927+
$observedSha = $state.TagSha
928+
if ($isCoordinate) { $observedSha = $state.VersionTagSha }
929+
if ($observedSha -cne $evidence.Reference.sha -or $observedSha -cne $reference.object.sha) {
872930
throw "Publication claim [$tag] changed while verifying recovery proof. Refusing version resolution."
873931
}
874932
if ($null -eq $state.Package) {
875933
throw ("The original source claim [$tag] has no published raw package proof. " +
876934
'Retain its resolved version/artifact and rerun the original job.')
877935
}
878-
$owned.Add($claim)
936+
$owned[$observedSha] = $claim
879937
}
880938
if ($owned.Count -gt 1) {
881939
throw 'Multiple published versions claim this exact source and release context. Refusing ambiguous recovery.'
882940
}
883941
if ($owned.Count -eq 1) {
884-
$version = New-PSSemVer -Version $owned[0].GalleryVersion
942+
$original = @($owned.Values)[0]
943+
$version = New-PSSemVer -Version $original.GalleryVersion
885944
$version.Prefix = $prefix
886945
Write-Verbose "Recovering proven original publication [$($version.ToString())] before Build."
887946
return $version
888947
}
889-
if ($PSGalleryVersion.ToString() -ne '0.0.0') {
890-
$galleryVersion = "$($PSGalleryVersion.Major).$($PSGalleryVersion.Minor).$($PSGalleryVersion.Patch)"
891-
$tag = "$prefix$galleryVersion"
892-
$release = & $Reader.GetRelease $tag
893-
$assets = @($release.assets | Where-Object {
894-
$_.name -ceq "$ModuleName-$galleryVersion.zip" -and $_.state -ceq 'uploaded'
895-
})
896-
if ($null -eq $release -or $release.tag_name -cne $tag -or $release.prerelease -ne $false -or
897-
$release.draft -ne $false -or $assets.Count -ne 1) {
898-
throw "Gallery-only package [$galleryVersion] has no proven source/artifact recovery context. Do not invent a fallback version."
948+
if ($null -ne $galleryVersion) {
949+
$completed = [Collections.Generic.List[string]]::new()
950+
foreach ($tag in $baselineTags) {
951+
$release = & $Reader.GetRelease $tag
952+
$assets = @($release.assets | Where-Object {
953+
$_.name -ceq "$ModuleName-$galleryVersion.zip" -and $_.state -ceq 'uploaded'
954+
})
955+
if ($null -eq $release -or $release.tag_name -cne $tag -or $release.prerelease -isnot [bool] -or
956+
$release.prerelease -ne $false -or $release.draft -isnot [bool] -or $release.draft -ne $false -or
957+
$assets.Count -ne 1) {
958+
continue
959+
}
960+
if ($baselineClaims.ContainsKey($tag)) {
961+
$baselineClaim = $baselineClaims[$tag]
962+
$baselineState = Get-PSModulePublicationState -ExpectedClaim $baselineClaim -Reader $Reader
963+
if ($null -eq $baselineState.Package) {
964+
throw 'The historical Gallery baseline has no exact published-package proof.'
965+
}
966+
$baselineSource = $baselineClaim.CommitSha
967+
} else {
968+
$tagParameters = @{
969+
Tag = $tag
970+
GetTagReference = $Reader.GetTagReference
971+
GetTagObject = {
972+
param($Sha)
973+
(& $Reader.GetTagObject $Sha).object
974+
}.GetNewClosure()
975+
}
976+
$baselineSource = Resolve-PSModuleReleaseTagCommit @tagParameters
977+
}
978+
if ($baselineSource -ceq $Context.CommitSha) {
979+
throw 'The original source already has publication records but recovery proof was not discovered. Do not invent a fallback version.'
980+
}
981+
$completed.Add($tag)
899982
}
900-
$tagParameters = @{
901-
Tag = $tag
902-
GetTagReference = $Reader.GetTagReference
903-
GetTagObject = {
904-
param($Sha)
905-
(& $Reader.GetTagObject $Sha).object
906-
}.GetNewClosure()
983+
if ($completed.Count -gt 1) {
984+
throw 'Multiple completed tags provide an ambiguous Gallery baseline. Refusing version resolution.'
907985
}
908-
$baselineSource = Resolve-PSModuleReleaseTagCommit @tagParameters
909-
if ($baselineSource -ceq $Context.CommitSha) {
910-
throw 'The original source already has publication records but recovery proof was not discovered. Do not invent a fallback version.'
986+
if ($completed.Count -eq 0) {
987+
throw "Gallery-only package [$galleryVersion] has no proven source/artifact recovery context. Do not invent a fallback version."
911988
}
912989
}
913990
}

‎.github/actions/Resolve-PSModuleVersion/tests/Resolve-PSModuleVersion.Recovery.Tests.ps1‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,23 @@ Describe 'Gallery-only version selection requires original ownership proof' {
4545
}
4646
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($claim | ConvertTo-Json -Compress)))
4747
$record = [pscustomobject]@{
48-
Reference = [pscustomobject]@{
48+
Reference = [pscustomobject]@{
4949
ref = 'refs/tags/v1.2.4'
5050
object = [pscustomobject]@{ type = 'tag'; sha = '3333333333333333333333333333333333333333' }
5151
}
5252
VersionReference = [pscustomobject]@{
5353
ref = 'refs/tags/PSModule-Publication/testmodule/1.2.4'
5454
object = [pscustomobject]@{ type = 'tag'; sha = '3333333333333333333333333333333333333333' }
5555
}
56-
Annotation = [pscustomobject]@{
56+
Annotation = [pscustomobject]@{
5757
sha = '3333333333333333333333333333333333333333'
5858
tag = 'v1.2.4'
5959
object = [pscustomobject]@{ type = 'commit'; sha = $claim.CommitSha }
6060
message = "<!-- PSModule.Publication:v1:$encoded -->"
6161
}
62-
Package = [pscustomobject]@{ Name = 'TestModule'; Version = '1.2.4' }
63-
PackagePath = $packagePath
64-
Release = $null
62+
Package = [pscustomobject]@{ Name = 'TestModule'; Version = '1.2.4' }
63+
PackagePath = $packagePath
64+
Release = $null
6565
}
6666
$state = @{
6767
Record = $record
@@ -188,19 +188,19 @@ Describe 'Gallery-only version selection requires original ownership proof' {
188188
$original.Reference = $null
189189
$original.Package = $null
190190
$later = [pscustomobject]@{
191-
Reference = [pscustomobject]@{
192-
ref = 'refs/tags/v1.2.5'
191+
Reference = [pscustomobject]@{
192+
ref = 'refs/tags/v1.2.5'
193193
object = [pscustomobject]@{ type = 'tag'; sha = '4444444444444444444444444444444444444444' }
194194
}
195195
VersionReference = [pscustomobject]@{
196-
ref = 'refs/tags/PSModule-Publication/testmodule/1.2.5'
196+
ref = 'refs/tags/PSModule-Publication/testmodule/1.2.5'
197197
object = [pscustomobject]@{ type = 'tag'; sha = '4444444444444444444444444444444444444444' }
198198
}
199-
Annotation = [pscustomobject]@{
199+
Annotation = [pscustomobject]@{
200200
sha = '4444444444444444444444444444444444444444'; tag = 'v1.2.5'; message = 'Completed later legacy baseline.'
201201
object = [pscustomobject]@{ type = 'commit'; sha = '2222222222222222222222222222222222222222' }
202202
}
203-
Release = [pscustomobject]@{
203+
Release = [pscustomobject]@{
204204
tag_name = 'v1.2.5'; prerelease = $false; draft = $false
205205
assets = @([pscustomobject]@{ name = 'TestModule-1.2.5.zip'; state = 'uploaded' })
206206
}
@@ -280,15 +280,15 @@ Describe 'Gallery-only version selection requires original ownership proof' {
280280
assets = @([pscustomobject]@{ name = 'TestModule-1.2.4.zip'; state = 'uploaded' })
281281
}
282282
$other = [pscustomobject]@{
283-
Reference = [pscustomobject]@{
284-
ref = 'refs/tags/release-1.2.4'
283+
Reference = [pscustomobject]@{
284+
ref = 'refs/tags/release-1.2.4'
285285
object = [pscustomobject]@{ type = 'tag'; sha = '4444444444444444444444444444444444444444' }
286286
}
287287
Annotation = [pscustomobject]@{
288288
sha = '4444444444444444444444444444444444444444'; tag = 'release-1.2.4'; message = 'Another completed legacy baseline.'
289289
object = [pscustomobject]@{ type = 'commit'; sha = '2222222222222222222222222222222222222222' }
290290
}
291-
Release = [pscustomobject]@{
291+
Release = [pscustomobject]@{
292292
tag_name = 'release-1.2.4'; prerelease = $false; draft = $false
293293
assets = @([pscustomobject]@{ name = 'TestModule-1.2.4.zip'; state = 'uploaded' })
294294
}

0 commit comments

Comments
 (0)