From e1270d3d5b94c95900ac6442eee9064fc633e88a Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 20 Sep 2026 10:58:35 -0700 Subject: [PATCH] feat(msix): publish Store assets on every release --- .github/workflows/ci.yml | 26 +++++------ docs/RELEASING.md | 44 +++++++++---------- scripts/Stage-StoreMsixReleaseAssets.ps1 | 12 +++-- scripts/test-ci-workflow-contract.ps1 | 30 ++++++------- scripts/test-msix-alpha-release.ps1 | 42 ++++++------------ .../ReleaseSigningWorkflowTests.cs | 11 ++--- 6 files changed, 68 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2108956d..cace93457 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,7 +174,6 @@ jobs: majorMinorPatch: ${{ steps.release_version.outputs.majorMinorPatch }} isPrerelease: ${{ steps.release_version.outputs.isPrerelease }} isStableCorrection: ${{ steps.release_version.outputs.isStableCorrection }} - isMsixAlpha: ${{ steps.release_version.outputs.isMsixAlpha }} msixVersionInfo: ${{ steps.msix_preview.outputs.versionInfo }} msixSourceVersion: ${{ steps.msix_preview.outputs.sourceVersion }} steps: @@ -235,9 +234,6 @@ jobs: "majorMinorPatch=$majorMinorPatch" >> $env:GITHUB_OUTPUT "isPrerelease=$($isPrerelease.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT "isStableCorrection=$($isStableCorrection.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT - $isMsixAlpha = $isPrerelease -and ($env:GITHUB_REF -cmatch '^refs/tags/v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-alpha\.(?:0|[1-9]\d*)$') - "isMsixAlpha=$($isMsixAlpha.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT - - name: Resolve MSIX preview version id: msix_preview if: ${{ !(github.repository == 'openclaw/openclaw-windows-node' && startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')) }} @@ -1301,16 +1297,14 @@ jobs: -Tag $env:RELEASE_TAG -GitHubToken $env:GH_TOKEN - - name: Download alpha Store MSIX artifacts - if: needs.metadata.outputs.isMsixAlpha == 'true' + - name: Download Store MSIX release artifacts uses: actions/download-artifact@v8 with: pattern: openclaw-msix-store-unsigned-* - path: artifacts/msix-alpha + path: artifacts/msix-release - - name: Stage alpha Store MSIX release assets - if: needs.metadata.outputs.isMsixAlpha == 'true' - id: msix_alpha + - name: Stage Store MSIX release assets + id: msix_release shell: pwsh env: RELEASE_VERSION: ${{ needs.metadata.outputs.semVer }} @@ -1321,13 +1315,13 @@ jobs: } $env:MSIX_VERSION_INFO | Set-Content -LiteralPath "$env:RUNNER_TEMP\openclaw-msix-version.json" -Encoding utf8 $assets = .\scripts\Stage-StoreMsixReleaseAssets.ps1 ` - -ArtifactDirectory 'artifacts\msix-alpha' ` - -OutputDirectory 'msix-alpha-release' ` + -ArtifactDirectory 'artifacts\msix-release' ` + -OutputDirectory 'msix-release' ` -Version $env:RELEASE_VERSION ` -VersionInfoPath "$env:RUNNER_TEMP\openclaw-msix-version.json" ` -ExpectedSourceCommit $env:GITHUB_SHA - @('files<> $env:GITHUB_OUTPUT - @('notes<> $env:GITHUB_OUTPUT + @('files<> $env:GITHUB_OUTPUT + @('notes<> $env:GITHUB_OUTPUT - name: Create Release uses: softprops/action-gh-release@v3 @@ -1338,7 +1332,7 @@ jobs: Output/OpenClawCompanion-Setup-arm64.exe OpenClawTray-${{ needs.metadata.outputs.semVer }}-win-x64.zip OpenClawTray-${{ needs.metadata.outputs.semVer }}-win-arm64.zip - ${{ steps.msix_alpha.outputs.files }} + ${{ steps.msix_release.outputs.files }} fail_on_unmatched_files: true prerelease: ${{ needs.metadata.outputs.isPrerelease }} make_latest: ${{ needs.metadata.outputs.isPrerelease == 'true' && 'false' || 'true' }} @@ -1351,7 +1345,7 @@ jobs: - **Portable x64**: `OpenClawTray-${{ needs.metadata.outputs.semVer }}-win-x64.zip` - **Portable ARM64**: `OpenClawTray-${{ needs.metadata.outputs.semVer }}-win-arm64.zip` - ${{ steps.msix_alpha.outputs.notes }} + ${{ steps.msix_release.outputs.notes }} ### Features - 🦞 System tray integration with gateway status diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 8d21d616a..0faaa70d2 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -13,10 +13,9 @@ infrastructure changes. Those fail-closed pull requests run the x64 publish smoke only; ARM64 portable publish remains required on `main` and tags. When either release-build lane is selected, CI also builds both architectures of Dev-signed and unsigned Store MSIX **workflow artifacts**. CI Gate requires -that MSIX job to succeed. Canonical alpha releases also attach the unsigned -Store MSIX bundle, standalone packages, and metadata for manual Partner Center -submission. -Stable releases do not include MSIX assets; Dev-signed packages stay in Actions. +that MSIX job to succeed. Every tag release also attaches the unsigned Store +MSIX bundle, standalone packages, and metadata for manual Partner Center +submission. Dev-signed packages stay in Actions. ## Release checklist @@ -37,8 +36,7 @@ Stable releases do not include MSIX assets; Dev-signed packages stay in Actions. "Verify Release Binary Signing Policy", ` "OpenClaw.Tray.WinUI.exe", ` "build-msix:", ` - "isMsixAlpha:", ` - "Stage alpha Store MSIX release assets" + "Stage Store MSIX release assets" ``` 3. Create a new stable, stable correction, or prerelease tag from `origin/main`. @@ -174,7 +172,7 @@ Current release artifacts are: - `OpenClawTray--win-x64.zip` - `OpenClawTray--win-arm64.zip` -Canonical alpha releases additionally contain: +Every stable, correction, and prerelease additionally contains: - `OpenClaw.msixbundle` (recommended Partner Center submission input) - `OpenClaw-x64.msix` and `OpenClaw-arm64.msix` @@ -184,19 +182,18 @@ Canonical alpha releases additionally contain: These are **unsigned Store submission inputs, not installers**. Upload the bundle to Partner Center for one architecture-selecting submission. The standalone packages remain available for inspection or fallback. Microsoft -signs accepted Store submissions. The alpha release step checks both +signs accepted Store submissions. The release step checks both architectures' clean source provenance, identity, version, and package hashes, then proves that the bundle embeds those exact bytes. It fails rather than publishing a partial or mismatched set. -Stable, stable-correction, and non-alpha prereleases retain the existing -EXE/ZIP asset set and do not receive MSIX download notes. Dev-signed tester -MSIX packages, public certificates, and instructions remain Actions artifacts -only. No production signing step is applied to the unsigned Store packages. +Dev-signed tester MSIX packages, public certificates, and instructions remain +Actions artifacts only. No production signing step is applied to the unsigned +Store packages. Store distribution remains paused: automatic Partner Center submission, Store-signed retrieval and publication, and official lifecycle acceptance -remain follow-up work in #1375. Alpha submission artifacts do not clear those +remain follow-up work in #1375. Release submission artifacts do not clear those rollout gates. Store versions still end in `.0`. Official tagged builds now reserve distinct @@ -268,20 +265,20 @@ source commit/ref, package base, allocation kind, and reservation ref. Preview candidates never reserve a number and can change between reruns; they must not be treated as official Store submissions. -The alpha stager requires `-VersionInfoPath` for the exact reserved result. It -checks the app alpha version, source commit, reserved allocation, package +The release stager requires `-VersionInfoPath` for the exact reserved result. It +checks the app version, source commit, reserved allocation, package version, and both architectures' metadata before copying any assets: ```powershell .\scripts\Stage-StoreMsixReleaseAssets.ps1 ` - -ArtifactDirectory 'artifacts\msix-alpha' ` - -OutputDirectory 'msix-alpha-release' ` + -ArtifactDirectory 'artifacts\msix-release' ` + -OutputDirectory 'msix-release' ` -Version $appVersion -ExpectedSourceCommit $sourceCommit ` -VersionInfoPath $reservedVersionInfoPath ``` -This does not change stable/alpha asset selection, bypass CI Gate or signing -approvals, move existing application tags, or automate Store submission. +This does not bypass CI Gate or signing approvals, move existing application +tags, or automate Store submission. ## Manual alpha releases @@ -412,9 +409,8 @@ validation. Release tags cannot enter the `release` job until **CI Gate** confirms classification, fast validation, tests, E2E, and release builds all succeeded. The `build-msix` and `build-msix-bundle` jobs must also succeed whenever release metadata is required. The release job downloads and attaches -its unsigned Store bundle, standalone packages, and metadata only for canonical -alpha tags. Stable releases and Dev tester distribution do not gain MSIX -release attachments. +its unsigned Store bundle, standalone packages, and metadata for every +canonical tag. Dev tester distribution stays workflow-only. The release job should: @@ -425,10 +421,10 @@ The release job should: 5. Create the portable x64 and ARM64 ZIPs. 6. Build Inno installers. 7. Sign installers. -8. For canonical alpha tags only, stage the validated unsigned Store MSIX +8. Stage the validated unsigned Store MSIX bundle, standalone packages, and metadata. 9. Create a GitHub release whose prerelease flag matches the tag, with installer - and portable ZIP assets plus any gated alpha submission assets. + and portable ZIP assets plus the Store submission assets. ## Post-release verification diff --git a/scripts/Stage-StoreMsixReleaseAssets.ps1 b/scripts/Stage-StoreMsixReleaseAssets.ps1 index 385273ef7..d9a2b19d3 100644 --- a/scripts/Stage-StoreMsixReleaseAssets.ps1 +++ b/scripts/Stage-StoreMsixReleaseAssets.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Stages validated unsigned Store MSIX packages for an alpha GitHub release. + Stages validated unsigned Store MSIX packages for a GitHub release. .DESCRIPTION Checks both architectures' provenance and hashes, then verifies that the multi-architecture bundle embeds those exact packages before copying files. @@ -13,8 +13,7 @@ param( [Parameter(Mandatory)][string]$ArtifactDirectory, [Parameter(Mandatory)][string]$OutputDirectory, [Parameter(Mandatory)] - [ValidatePattern('^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-alpha\.(?:0|[1-9]\d*)$')] - [string]$Version, + [ValidateNotNullOrEmpty()][string]$Version, [Parameter(Mandatory)][ValidatePattern('^[0-9a-fA-F]{40}$')][string]$ExpectedSourceCommit, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$VersionInfoPath ) @@ -27,7 +26,7 @@ $OutputDirectory = [IO.Path]::GetFullPath([IO.Path]::Combine($repositoryRoot, $O if ((Test-Path -LiteralPath $OutputDirectory) -and (-not (Test-Path -LiteralPath $OutputDirectory -PathType Container) -or @(Get-ChildItem -LiteralPath $OutputDirectory -Force).Count -gt 0)) { - throw "The alpha release output directory must be absent or empty: $OutputDirectory" + throw "The release output directory must be absent or empty: $OutputDirectory" } [xml]$manifest = Get-Content -LiteralPath (Join-Path $repositoryRoot 'src\OpenClaw.Tray.WinUI\Package.appxmanifest') -Raw @@ -163,7 +162,7 @@ $files += foreach ($package in $packages) { [pscustomobject]@{ Files = @($files) Notes = @" -### Unsigned Store submission packages (alpha only) +### Unsigned Store submission packages OpenClaw.msixbundle is the recommended unsigned Partner Center submission input. It contains the exact x64 and ARM64 packages selected by Windows. @@ -174,7 +173,6 @@ this workflow does not submit or retrieve Store packages. The Windows package version is $expectedVersion, reserved for $Version. Different official tags share the app patch's packaging counter; reruns of -this tag reuse its reservation. Stable releases do not include these -experimental submission assets. Dev-signed tester downloads remain in Actions. +this tag reuse its reservation. Dev-signed tester downloads remain in Actions. "@ } diff --git a/scripts/test-ci-workflow-contract.ps1 b/scripts/test-ci-workflow-contract.ps1 index 3ed6ae191..804c5f23e 100644 --- a/scripts/test-ci-workflow-contract.ps1 +++ b/scripts/test-ci-workflow-contract.ps1 @@ -811,26 +811,24 @@ foreach ($token in @( )) { Assert-Contains -Text $releaseJob -Expected $token -Message "Tag release is missing '$token'." } -$alphaDownload = Get-StepBlock -Text $releaseJob -Name 'Download alpha Store MSIX artifacts' -$alphaStage = Get-StepBlock -Text $releaseJob -Name 'Stage alpha Store MSIX release assets' -foreach ($step in @($alphaDownload, $alphaStage)) { - Assert-Contains -Text $step -Expected "if: needs.metadata.outputs.isMsixAlpha == 'true'" -Message "Store release assets must be alpha-only." -} -Assert-Contains -Text $alphaDownload -Expected 'pattern: openclaw-msix-store-unsigned-*' -Message "Alpha releases must use unsigned Store inputs." -Assert-NotContains -Text $alphaDownload -Unexpected 'openclaw-msix-dev-' -Message "Dev packages must stay workflow-only." -Assert-Contains -Text $alphaStage -Expected '-ExpectedSourceCommit $env:GITHUB_SHA' -Message "Release staging must bind artifacts to the tag's source." -Assert-Contains -Text $alphaStage -Expected '-Version $env:RELEASE_VERSION' -Message "Release staging must validate the alpha version." -Assert-Contains -Text $alphaStage -Expected '-VersionInfoPath "$env:RUNNER_TEMP\openclaw-msix-version.json"' -Message 'Release staging must require its exact reserved MSIX version.' -Assert-Contains -Text $alphaStage -Expected 'MSIX_VERSION_INFO: ${{ needs.reserve-msix-version.outputs.versionInfo }}' -Message 'Release staging must not accept a preview.' +$msixDownload = Get-StepBlock -Text $releaseJob -Name 'Download Store MSIX release artifacts' +$msixStage = Get-StepBlock -Text $releaseJob -Name 'Stage Store MSIX release assets' +foreach ($step in @($msixDownload, $msixStage)) { + Assert-NotContains -Text $step -Unexpected 'if:' -Message "Every tag release must publish Store MSIX assets." +} +Assert-Contains -Text $msixDownload -Expected 'pattern: openclaw-msix-store-unsigned-*' -Message "Tag releases must use unsigned Store inputs." +Assert-NotContains -Text $msixDownload -Unexpected 'openclaw-msix-dev-' -Message "Dev packages must stay workflow-only." +Assert-Contains -Text $msixStage -Expected '-ExpectedSourceCommit $env:GITHUB_SHA' -Message "Release staging must bind artifacts to the tag's source." +Assert-Contains -Text $msixStage -Expected '-Version $env:RELEASE_VERSION' -Message "Release staging must validate the release version." +Assert-Contains -Text $msixStage -Expected '-VersionInfoPath "$env:RUNNER_TEMP\openclaw-msix-version.json"' -Message 'Release staging must require its exact reserved MSIX version.' +Assert-Contains -Text $msixStage -Expected 'MSIX_VERSION_INFO: ${{ needs.reserve-msix-version.outputs.versionInfo }}' -Message 'Release staging must not accept a preview.' $createRelease = Get-StepBlock -Text $releaseJob -Name 'Create Release' -Assert-Contains -Text $createRelease -Expected '${{ steps.msix_alpha.outputs.files }}' -Message "Only the gated alpha stage may add MSIX release files." -Assert-Contains -Text $createRelease -Expected '${{ steps.msix_alpha.outputs.notes }}' -Message "Only alpha release notes may mention MSIX downloads." +Assert-Contains -Text $createRelease -Expected '${{ steps.msix_release.outputs.files }}' -Message "Every tag release must add validated MSIX release files." +Assert-Contains -Text $createRelease -Expected '${{ steps.msix_release.outputs.notes }}' -Message "Every tag release must include MSIX submission notes." Assert-Contains -Text $createRelease -Expected 'fail_on_unmatched_files: true' -Message "Missing release files must fail publication." Assert-Contains -Text $createRelease -Expected "make_latest: `${{ needs.metadata.outputs.isPrerelease == 'true' && 'false' || 'true' }}" -Message "Alpha releases must not become Latest." -Assert-NotContains -Text $createRelease -Unexpected 'OpenClaw-x64.msix' -Message "MSIX must not be an unconditional stable release asset." -Assert-NotContains -Text $createRelease -Unexpected 'OpenClaw-arm64.msix' -Message "MSIX must not be an unconditional stable release asset." Assert-Contains -Text $workflow -Expected "./scripts/test-msix-ci-artifacts.ps1" -Message "Fast validation must exercise the Dev artifact contracts." -Assert-Contains -Text $workflow -Expected "./scripts/test-msix-alpha-release.ps1" -Message "Fast validation must exercise alpha release staging." +Assert-Contains -Text $workflow -Expected "./scripts/test-msix-alpha-release.ps1" -Message "Fast validation must exercise Store release staging." Assert-Contains -Text $workflow -Expected "./scripts/test-msix-versioning.ps1" -Message 'Fast validation must exercise allocation races and boundaries.' Assert-Contains -Text $workflow -Expected "./scripts/test-msix-preview-source-version.ps1" -Message 'Fast validation must exercise latest-stable MSIX preview selection.' diff --git a/scripts/test-msix-alpha-release.ps1 b/scripts/test-msix-alpha-release.ps1 index be3f5ee1f..2ebcea3c6 100644 --- a/scripts/test-msix-alpha-release.ps1 +++ b/scripts/test-msix-alpha-release.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Exercises alpha-only Store asset staging and workflow release selection. + Exercises Store asset staging for tag releases and alpha scheduling. .DESCRIPTION Uses synthetic package bytes and metadata. Real package-content validation remains in Build-StoreMsix.ps1; no build, signing, or release API is invoked. @@ -31,13 +31,15 @@ function Assert-Fails { } function New-Fixture { + param([string]$Version = '2026.7.2-alpha.4') + $script:scenario++ $inputPath = Join-Path $temporaryRoot "input-$scenario" $allocation = [ordered]@{ schemaVersion = 1 - sourceVersion = '2026.7.2-alpha.4' + sourceVersion = $Version sourceCommit = $sourceCommit - sourceRef = 'refs/tags/v2026.7.2-alpha.4' + sourceRef = "refs/tags/v$Version" repository = 'openclaw/openclaw-windows-node' baseVersion = '2026.7.2' packageBaseVersion = '2026.7.202' @@ -97,7 +99,7 @@ function New-Fixture { @{ ArtifactDirectory = $inputPath OutputDirectory = Join-Path $temporaryRoot "output-$scenario" - Version = '2026.7.2-alpha.4' + Version = $Version ExpectedSourceCommit = $sourceCommit VersionInfoPath = $allocationPath } @@ -140,10 +142,14 @@ try { } Assert-Fails { & $stager @arguments } 'absent or empty' - foreach ($version in @('2026.7.2', '2026.7.2-3', '2026.7.2-beta.1', '2026.7.2-alpha', '2026.7.2-alpha.01', '2026.7.2-alpha.1+meta')) { + foreach ($version in @('2026.7.2', '2026.7.2-3', '2026.7.2-beta.1')) { + $arguments = New-Fixture -Version $version + & $stager @arguments | Out-Null + } + foreach ($version in @('v2026.7.2', '2026.7', '2026.7.2-alpha.01', '2026.7.2+meta+extra')) { $arguments = New-Fixture $arguments.Version = $version - Assert-Fails { & $stager @arguments } 'cannot validate argument' + Assert-Fails { & $stager @arguments } 'MSIX' } foreach ($mutation in @( @{ Field = 'sourceCommit'; Value = ('b' * 40); Error = 'expected clean source' }, @@ -221,28 +227,6 @@ try { $allocation | ConvertTo-Json | Set-Content -LiteralPath $arguments.VersionInfoPath Assert-Fails { & $stager @arguments } 'MSIX' - # Execute the actual metadata selector rather than a test-only copy of its regex. - $workflow = Get-Content -LiteralPath (Join-Path $RepoRoot '.github\workflows\ci.yml') -Raw - $selectorLine = [regex]::Match($workflow, '(?m)^\s*\$isMsixAlpha = .+$') - if (-not $selectorLine.Success) { throw 'The workflow is missing its alpha-only selector.' } - $selector = [scriptblock]::Create($selectorLine.Value + "`n`$isMsixAlpha") - foreach ($case in @( - @{ Ref = 'refs/tags/v2026.7.2-alpha.4'; Prerelease = $true; Expected = $true }, - @{ Ref = 'refs/tags/v2026.7.2-alpha.0'; Prerelease = $true; Expected = $true }, - @{ Ref = 'refs/tags/v2026.7.2-alpha.4'; Prerelease = $false; Expected = $false }, - @{ Ref = 'refs/tags/v2026.7.2'; Prerelease = $false; Expected = $false }, - @{ Ref = 'refs/tags/v2026.7.2-3'; Prerelease = $false; Expected = $false }, - @{ Ref = 'refs/tags/v2026.7.2-beta.1'; Prerelease = $true; Expected = $false }, - @{ Ref = 'refs/tags/v2026.7.2-alpha.04'; Prerelease = $true; Expected = $false }, - @{ Ref = 'refs/tags/v2026.7.2-Alpha.4'; Prerelease = $true; Expected = $false }, - @{ Ref = 'refs/tags/v2026.7.2-alpha.4+build'; Prerelease = $true; Expected = $false }, - @{ Ref = 'refs/heads/v2026.7.2-alpha.4'; Prerelease = $true; Expected = $false }, - @{ Ref = 'refs/pull/1403/merge'; Prerelease = $true; Expected = $false } - )) { - $env:GITHUB_REF = $case.Ref - $isPrerelease = $case.Prerelease - if ((& $selector) -ne $case.Expected) { throw "Incorrect alpha selection for $($case.Ref)." } - } $daily = Get-Content -LiteralPath (Join-Path $RepoRoot '.github\workflows\daily-alpha-release.yml') -Raw $scheduleBlock = [regex]::Match($daily, '(?ms)^ run: \|\r?\n(?