Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,22 @@ jobs:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.build.outputs.source-commit }}
persist-credentials: false
- uses: actions/download-artifact@v8
with:
artifact-ids: ${{ needs.build.outputs.artifact-id }}
merge-multiple: true
path: .processing/platform-release
- name: Restore packaged release tools
run: Expand-Archive .processing/platform-release/OpenGuidePlatform-PlatformBuild.zip .processing/platform-release/tools
- name: Publish versioned release
env:
GH_TOKEN: ${{ github.token }}
PLATFORM_VERSION: ${{ needs.build.outputs.version }}
run: ./build.ps1 -Stage Release -OutputPath .processing/platform-release
PLATFORM_COMMIT: ${{ needs.build.outputs.source-commit }}
run: ./.processing/platform-release/tools/release.ps1 -Stage Release -AssetsPath .processing/platform-release -SourceCommit $env:PLATFORM_COMMIT
- name: Verify published release
env:
GH_TOKEN: ${{ github.token }}
PLATFORM_VERSION: ${{ needs.build.outputs.version }}
run: ./build.ps1 -Stage Validate -ReleaseTag "v$env:PLATFORM_VERSION" -OutputPath .processing/published-platform
PLATFORM_COMMIT: ${{ needs.build.outputs.source-commit }}
run: ./.processing/platform-release/tools/release.ps1 -Stage Validate -AssetsPath .processing/platform-release -SourceCommit $env:PLATFORM_COMMIT -ReleaseTag "v$env:PLATFORM_VERSION"
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ if($Product -eq 'Platform' -and (Test-Path "$selected/release-manifest.json") -a
$zip=[IO.Compression.ZipFile]::OpenRead($archivePath)
$names=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
try{foreach($entry in $zip.Entries){
if(-not $names.Add($entry.FullName) -or $entry.FullName -match '(^/|\\|:|(^|/)\.\.?(/|$))' -or (($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000 -or ($entry.FullName -cne 'platform-build.json' -and -not $entry.FullName.StartsWith('system/OpenGuidePlatform.PowerShell.PlatformBuild/'))){throw 'PlatformBuild contains an unsafe or overlapping entry.'}
if(-not $names.Add($entry.FullName) -or $entry.FullName -match '(^/|\\|:|(^|/)\.\.?(/|$))' -or (($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000 -or ($entry.FullName -cne 'platform-build.json' -and $entry.FullName -cne 'release.ps1' -and -not $entry.FullName.StartsWith('system/OpenGuidePlatform.PowerShell.PlatformBuild/'))){throw 'PlatformBuild contains an unsafe or overlapping entry.'}
}}finally{$zip.Dispose()}
[IO.Compression.ZipFile]::ExtractToDirectory($archivePath,$selected)
$identity=Get-Content "$selected/platform-build.json" -Raw|ConvertFrom-Json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ $guideHash=(Get-FileHash "$output/$guideArchive").Hash.ToLowerInvariant()
$platformStage=Join-Path $output 'platform-build'
[IO.Directory]::CreateDirectory("$platformStage/system")|Out-Null
Copy-Item "$root/system/OpenGuidePlatform.PowerShell.PlatformBuild" "$platformStage/system/" -Recurse
Copy-Item "$root/system/OpenGuidePlatform.PowerShell.PlatformBuild/Release/release.ps1" "$platformStage/release.ps1"
$dependency=[ordered]@{version=$Version;sha256=$guideHash}
$platformMetadata=[ordered]@{schemaVersion=1;product='OpenGuidePlatform';package='PlatformBuild';version=$Version;sourceCommit=$commit;dependencies=@{GuideSite=$dependency}}
[IO.File]::WriteAllText("$platformStage/platform-build.json",($platformMetadata|ConvertTo-Json -Depth 10))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Requires -Version 7.4
[CmdletBinding()]
param([Parameter(Mandatory)][string]$WorkspaceRoot,[Parameter(Mandatory)][string]$OutputPath,[string]$Repository='nkdAgility/OpenGuidePlatform')
param([Parameter(Mandatory)][string]$WorkspaceRoot,[Parameter(Mandatory)][string]$OutputPath,[string]$SourceCommit,[string]$Repository='nkdAgility/OpenGuidePlatform')
$ErrorActionPreference='Stop'
. "$PSScriptRoot/Publish-PlatformWorkflowAliases.ps1"
$WorkspaceRoot=[IO.Path]::GetFullPath($WorkspaceRoot)
Expand All @@ -15,8 +15,8 @@ foreach($name in @('GuideSite','PlatformBuild')){
$part=$manifest.packages.$name
if($part.archive -cne "OpenGuidePlatform-$name.zip" -or $part.version -cne $manifest.version -or (Get-FileHash "$OutputPath/$($part.archive)").Hash -ine $part.sha256){throw "Release $name package identity or digest mismatch. Rebuild and validate the complete release."}
}
$commit=(& git -C $WorkspaceRoot rev-parse HEAD).Trim()
if($LASTEXITCODE -ne 0 -or $commit -cne $manifest.sourceCommit){throw 'Release checkout does not match the package.'}
$commit=if($SourceCommit){$SourceCommit}else{(& git -C $WorkspaceRoot rev-parse HEAD).Trim()}
if($commit -cnotmatch '^[a-f0-9]{40}$' -or $commit -cne $manifest.sourceCommit){throw 'Release source commit does not match the package.'}
$tag="v$($manifest.version)"
$module=$manifest.nativeHugoModule
if($module.path -cne 'github.com/nkdAgility/OpenGuidePlatform/system/OpenGuidePlatform.Hugo.Guides' -or $module.version -cne $tag -or $module.tag -cne "system/OpenGuidePlatform.Hugo.Guides/$tag" -or $module.sourceCommit -cne $commit){throw 'Native Hugo publication identity differs from the tested release.'}
Expand Down Expand Up @@ -86,7 +86,7 @@ The corresponding native Hugo module tag is $($module.tag). The platform tests a
"@
[IO.File]::WriteAllText("$OutputPath/release-notes.md",$notes)
$releaseFlags=if($prerelease){@('--prerelease','--latest=false')}else{@()}
& gh release create $tag "$OutputPath/OpenGuidePlatform-GuideSite.zip" "$OutputPath/OpenGuidePlatform-PlatformBuild.zip" "$OutputPath/release-manifest.json" --repo $Repository --target $commit @releaseFlags --title "OpenGuidePlatform $($manifest.version)" --generate-notes --notes-file "$OutputPath/release-notes.md"
& gh release create $tag "$OutputPath/OpenGuidePlatform-GuideSite.zip" "$OutputPath/OpenGuidePlatform-PlatformBuild.zip" "$OutputPath/release-manifest.json" --repo $Repository --target $commit @releaseFlags --title $tag --generate-notes --notes-file "$OutputPath/release-notes.md"
if($LASTEXITCODE -ne 0){throw 'Platform release publication failed.'}

Publish-PlatformWorkflowAliases -WorkspaceRoot $WorkspaceRoot -Repository $Repository -Version $manifest.version -Commit $commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,34 @@ function Publish-PlatformWorkflowAliases {
$versionNumber=[System.Management.Automation.SemanticVersion]$Version
$suffix=if($versionNumber.PreReleaseLabel){'-preview'}else{''}
$aliases=@("v$($versionNumber.Major)$suffix","v$($versionNumber.Major).$($versionNumber.Minor)$suffix")
$remote="https://github.com/$Repository.git"
$raw=@(& git ls-remote --refs $remote 'refs/tags/v*')
$raw=& gh api "repos/$Repository/git/matching-refs/tags/v" --paginate --slurp
if($LASTEXITCODE -ne 0){throw 'Cannot inspect workflow aliases. The immutable release is published; rerun Release to finish alias publication.'}
$refs=@{}
foreach($line in $raw){$parts=$line -split '\s+';if($parts.Count -eq 2){$refs[$parts[1]]=$parts[0]}}
foreach($page in @($raw|ConvertFrom-Json)){foreach($item in $page){$refs[$item.ref]=$item}}
foreach($alias in $aliases){
$ref="refs/tags/$alias";$expected=if($refs.ContainsKey($ref)){$refs[$ref]}else{''}
$ref="refs/tags/$alias";$prior=if($refs.ContainsKey($ref)){$refs[$ref]}else{$null}
$expected=if($prior){$prior.object.sha}else{''}
if($expected -ceq $Commit){continue}
if($expected){
$priorVersions=@(foreach($name in $refs.Keys){
if($refs[$name] -cne $expected -or $name -cnotmatch '^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$'){continue}
if($refs[$name].object.sha -cne $expected -or $name -cnotmatch '^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$'){continue}
$prior=[System.Management.Automation.SemanticVersion]$name.Substring(11)
if([bool]$prior.PreReleaseLabel -eq [bool]$versionNumber.PreReleaseLabel){$prior}
})
if(-not $priorVersions.Count){throw "Workflow alias $alias does not identify an immutable release tag. Reconcile it before publishing aliases."}
if(@($priorVersions|Where-Object {$_ -gt $versionNumber}).Count){Write-Host "Keeping newer workflow alias $alias.";continue}
$query='query($owner:String!,$name:String!,$ref:String!){repository(owner:$owner,name:$name){id ref(qualifiedName:$ref){target{oid}}}}'
$owner,$repoName=$Repository.Split('/')
$result=& gh api graphql -f "query=$query" -f "owner=$owner" -f "name=$repoName" -f "ref=$ref" | ConvertFrom-Json
if($LASTEXITCODE -ne 0 -or $result.PSObject.Properties['errors'] -or $result.data.repository.ref.target.oid -cne $expected){throw "Workflow alias $alias changed concurrently."}
$repositoryId=$result.data.repository.id
if(-not $repositoryId){throw 'Cannot identify the repository for alias publication.'}
$mutation='mutation{updateRefs(input:{repositoryId:"'+$repositoryId+'",refUpdates:[{name:"'+$ref+'",beforeOid:"'+$expected+'",afterOid:"'+$Commit+'",force:true}]}){clientMutationId}}'
$response=& gh api graphql -f "query=$mutation" | ConvertFrom-Json
if($LASTEXITCODE -ne 0 -or -not $response -or $response.PSObject.Properties['errors']){throw "Workflow alias $alias changed concurrently or could not be published."}
}else{
$null=& gh api "repos/$Repository/git/refs" --method POST -f "ref=$ref" -f "sha=$Commit"
}
# Lease rejects concurrent publication rather than overwriting a newer alias.
& git -C $WorkspaceRoot -c credential.helper= -c 'credential.helper=!gh auth git-credential' push "--force-with-lease=${ref}:$expected" $remote "${Commit}:$ref"
if($LASTEXITCODE -ne 0){throw "Workflow alias $alias changed concurrently or could not be published. The release is intact; rerun Release."}
Write-Host "Published workflow alias $alias -> v$Version."
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#Requires -Version 7.4
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidateSet('Release','Validate')][string]$Stage,
[Parameter(Mandatory)][string]$AssetsPath,
[Parameter(Mandatory)][ValidatePattern('^[a-f0-9]{40}$')][string]$SourceCommit,
[string]$ReleaseTag,
[string]$Repository='nkdAgility/OpenGuidePlatform'
)
$ErrorActionPreference='Stop'
$assets=[IO.Path]::GetFullPath($AssetsPath)
$manifest=Get-Content (Join-Path $assets 'release-manifest.json') -Raw|ConvertFrom-Json
if($manifest.sourceCommit -cne $SourceCommit){throw 'Release source commit does not match the package.'}
$tag="v$($manifest.version)"
if($ReleaseTag -and $ReleaseTag -cne $tag){throw 'Requested release tag does not match the package.'}
if($Stage -eq 'Release'){
& "$PSScriptRoot/system/OpenGuidePlatform.PowerShell.PlatformBuild/Release/Publish-PlatformRelease.ps1" -WorkspaceRoot $assets -OutputPath $assets -SourceCommit $SourceCommit -Repository $Repository
return
}
Comment on lines +16 to +19
$published=& gh release view $tag --repo $Repository --json targetCommitish,isDraft,isPrerelease | ConvertFrom-Json
if($LASTEXITCODE -ne 0 -or $published.targetCommitish -cne $SourceCommit -or $published.isDraft -or [bool]$published.isPrerelease -ne $manifest.version.Contains('-')){throw 'Published release identity differs from the package.'}
$verify=Join-Path $assets ('published-'+[guid]::NewGuid().ToString('N'))
& gh release download $tag --repo $Repository --pattern OpenGuidePlatform-GuideSite.zip --pattern OpenGuidePlatform-PlatformBuild.zip --pattern release-manifest.json --dir $verify
if($LASTEXITCODE -ne 0){throw 'Cannot download published release assets.'}
foreach($name in @('release-manifest.json','OpenGuidePlatform-GuideSite.zip','OpenGuidePlatform-PlatformBuild.zip')){
if((Get-FileHash (Join-Path $verify $name)).Hash -cne (Get-FileHash (Join-Path $assets $name)).Hash){throw "Published $name differs from the tested package."}
}
Write-Host "Verified published release $tag from $SourceCommit."
19 changes: 12 additions & 7 deletions tests/Core/NativeModulePublication.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ BeforeAll {
$global:LASTEXITCODE=0
$global:OgpNativeTagCalls.Add(($args -join ' '))
if($args[0] -eq 'api'){
if($args[1] -like 'repos/*/git/matching-refs/tags/v'){return '[]'}
if($global:OgpNativeTagFailure){$global:LASTEXITCODE=1}
return
}
Expand All @@ -47,8 +48,10 @@ Describe 'Coordinated native module publication' {
It 'publishes a nested module tag before making its coordinated release available' {
& $publisher -WorkspaceRoot $root -Repository example/platform -OutputPath $assets
$global:OgpNativeTagCalls[0] | Should -Match 'refs/tags/system/OpenGuidePlatform.Hugo.Guides/v0.1.0-Preview.1'
$global:OgpNativeTagCalls[-1] | Should -Match '^release create v0.1.0-Preview.1 '
$global:OgpNativeTagCalls[-1] | Should -Match '--generate-notes'
$releaseCall=$global:OgpNativeTagCalls|Where-Object {$_ -like 'release create *'}|Select-Object -Last 1
$releaseCall | Should -Match '^release create v0.1.0-Preview.1 '
$releaseCall | Should -Match '--generate-notes'
$releaseCall | Should -Match '--title v0.1.0-Preview.1'
$notes=Get-Content "$assets/release-notes.md" -Raw
$notes|Should -Match 'Update -ring preview -PlatformRelease v0.1.0-Preview.1'
$notes|Should -Match 'First installation|OpenGuidePlatform-GuideSite.zip|settings.yaml'
Expand All @@ -59,13 +62,15 @@ Describe 'Coordinated native module publication' {
ConvertTo-PackageManifest $manifest|ConvertTo-Json -Depth 10|Set-Content "$assets/release-manifest.json"
& $publisher -WorkspaceRoot $root -OutputPath $assets
$global:OgpNativeTagCalls[0]|Should -Match 'refs/tags/system/OpenGuidePlatform.Hugo.Guides/v0.1.0'
$global:OgpNativeTagCalls[-1]|Should -Match '^release create v0.1.0 '
$global:OgpNativeTagCalls[-1]|Should -Not -Match '--prerelease|--latest=false'
$releaseCall=$global:OgpNativeTagCalls|Where-Object {$_ -like 'release create *'}|Select-Object -Last 1
$releaseCall|Should -Match '^release create v0.1.0 '
$releaseCall|Should -Not -Match '--prerelease|--latest=false'
$releaseCall|Should -Match '--title v0.1.0'
Get-Content "$assets/release-notes.md" -Raw|Should -Match 'Update -ring production -PlatformRelease v0.1.0'
}
It 'keeps prerelease versions as preview releases' {
& $publisher -WorkspaceRoot $root -OutputPath $assets
$global:OgpNativeTagCalls[-1]|Should -Match '--prerelease --latest=false'
($global:OgpNativeTagCalls|Where-Object {$_ -like 'release create *'}|Select-Object -Last 1)|Should -Match '--prerelease --latest=false'
}
It 'rejects a manifest channel inconsistent with its version' {
$manifest.channel='stable'
Expand All @@ -85,12 +90,12 @@ Describe 'Coordinated native module publication' {
try { & $publisher -WorkspaceRoot $workspace -Repository example/platform -OutputPath $relativeAssets }
finally { Pop-Location }
Test-Path "$assets/release-notes.md" | Should -BeTrue
$global:OgpNativeTagCalls[-1] | Should -Match ([regex]::Escape("$assets/OpenGuidePlatform-GuideSite.zip"))
($global:OgpNativeTagCalls|Where-Object {$_ -like 'release create *'}|Select-Object -Last 1) | Should -Match ([regex]::Escape("$assets/OpenGuidePlatform-GuideSite.zip"))
}
It 'reuses an existing matching tag without moving or recreating it' {
$global:OgpNativeTagExisting=@(('a'*40)+"`trefs/tags/system/OpenGuidePlatform.Hugo.Guides/v0.1.0-Preview.1")
& $publisher -WorkspaceRoot $root -Repository example/platform -OutputPath $assets
@($global:OgpNativeTagCalls|Where-Object {$_ -match '^api '}).Count | Should -Be 0
@($global:OgpNativeTagCalls|Where-Object {$_ -match '^api .*/git/refs --method POST.*system/OpenGuidePlatform.Hugo.Guides/' }).Count | Should -Be 0
}
It 'refuses a conflicting immutable module tag' {
$global:OgpNativeTagExisting=@(('b'*40)+"`trefs/tags/system/OpenGuidePlatform.Hugo.Guides/v0.1.0-Preview.1")
Expand Down
40 changes: 40 additions & 0 deletions tests/Core/PackagedRelease.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
BeforeAll {
$root=Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$launcher=Join-Path $root 'system/OpenGuidePlatform.PowerShell.PlatformBuild/Release/release.ps1'
function global:gh {
$global:LASTEXITCODE=0
if($args[0] -eq 'release' -and $args[1] -eq 'view'){
return (@{targetCommitish=('a'*40);isDraft=$false;isPrerelease=$false}|ConvertTo-Json)
}
if($args[0] -eq 'release' -and $args[1] -eq 'download'){
$index=[Array]::IndexOf($args,'--dir')
[IO.Directory]::CreateDirectory($args[$index+1])|Out-Null
foreach($name in @('release-manifest.json','OpenGuidePlatform-GuideSite.zip','OpenGuidePlatform-PlatformBuild.zip')){
Copy-Item (Join-Path $global:OgpPackagedReleaseAssets $name) $args[$index+1]
}
if($global:OgpPackagedReleaseCorrupt){Set-Content (Join-Path $args[$index+1] 'OpenGuidePlatform-GuideSite.zip') 'changed'}
return
}
throw 'Unexpected GitHub command'
}
}
Describe 'Packaged release launcher' {
BeforeEach {
$global:OgpPackagedReleaseAssets=Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
[IO.Directory]::CreateDirectory($global:OgpPackagedReleaseAssets)|Out-Null
$global:OgpPackagedReleaseCorrupt=$false
@{version='1.2.3';sourceCommit=('a'*40)}|ConvertTo-Json|Set-Content "$global:OgpPackagedReleaseAssets/release-manifest.json"
Set-Content "$global:OgpPackagedReleaseAssets/OpenGuidePlatform-GuideSite.zip" 'guide bytes'
Set-Content "$global:OgpPackagedReleaseAssets/OpenGuidePlatform-PlatformBuild.zip" 'platform bytes'
}
It 'rejects an artifact from another source commit before publication' {
{ & $launcher -Stage Validate -AssetsPath $global:OgpPackagedReleaseAssets -SourceCommit ('b'*40) }|Should -Throw '*source commit does not match*'
}
It 'validates published bytes using only the artifact and GitHub release' {
& $launcher -Stage Validate -AssetsPath $global:OgpPackagedReleaseAssets -SourceCommit ('a'*40) -ReleaseTag v1.2.3
}
It 'rejects changed published assets' {
$global:OgpPackagedReleaseCorrupt=$true
{ & $launcher -Stage Validate -AssetsPath $global:OgpPackagedReleaseAssets -SourceCommit ('a'*40) }|Should -Throw '*differs from the tested package*'
}
}
Loading
Loading