From 2ca61f7f8da5976be1b491ee3e7f62475985025c Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 13 Sep 2026 15:55:14 -0700 Subject: [PATCH 01/13] feat(ci): derive Store-safe MSIX release versions --- .github/workflows/gateway-msix.yml | 25 +++- README.md | 39 ++++-- release-policy.json | 4 +- scripts/Get-MSIXReleaseIdentity.ps1 | 63 +++++++++ scripts/Test-MSIXReleaseIdentity.Tests.ps1 | 122 ++++++++++++++++++ scripts/Test-SigningInputs.Tests.ps1 | 20 ++- scripts/Test-SigningInputs.ps1 | 19 +-- scripts/Test-WorkflowSigningConfiguration.ps1 | 1 + 8 files changed, 256 insertions(+), 37 deletions(-) create mode 100644 scripts/Get-MSIXReleaseIdentity.ps1 create mode 100644 scripts/Test-MSIXReleaseIdentity.Tests.ps1 diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index 624919d0..47908461 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -164,6 +164,11 @@ jobs: run: > .\scripts\Test-Deploy-LocalPackage.Tests.ps1 + - name: Test MSIX release identity + shell: pwsh + run: > + .\scripts\Test-MSIXReleaseIdentity.Tests.ps1 + - name: Test MSIX bundle build shell: pwsh run: > @@ -431,7 +436,10 @@ jobs: if ($env:SIGNING_MODE -eq 'official') { $policy = Get-Content -LiteralPath .\release-policy.json -Raw | ConvertFrom-Json - $versionParameters.ReleaseVersion = [string]$policy.packageVersion + $identity = .\scripts\Get-MSIXReleaseIdentity.ps1 ` + -GatewayTag ([string]$policy.gatewayTag) ` + -MSIXRevision ([int]$policy.msixRevision) + $versionParameters.ReleaseVersion = $identity.PackageVersion } $packageVersion = .\scripts\Get-WorkflowPackageVersion.ps1 ` @versionParameters @@ -536,7 +544,10 @@ jobs: if ($env:SIGNING_MODE -eq 'official') { $policy = Get-Content -LiteralPath .\release-policy.json -Raw | ConvertFrom-Json - $versionParameters.ReleaseVersion = [string]$policy.packageVersion + $identity = .\scripts\Get-MSIXReleaseIdentity.ps1 ` + -GatewayTag ([string]$policy.gatewayTag) ` + -MSIXRevision ([int]$policy.msixRevision) + $versionParameters.ReleaseVersion = $identity.PackageVersion } $packageVersion = .\scripts\Get-WorkflowPackageVersion.ps1 ` @versionParameters @@ -612,15 +623,17 @@ jobs: run: | $policy = Get-Content -LiteralPath .\release-policy.json -Raw | ConvertFrom-Json - $releaseTag = ([string]$policy.releaseTag).Trim() + $identity = .\scripts\Get-MSIXReleaseIdentity.ps1 ` + -GatewayTag ([string]$policy.gatewayTag) ` + -MSIXRevision ([int]$policy.msixRevision) $packageVersion = .\scripts\Get-WorkflowPackageVersion.ps1 ` -RunNumber '${{ github.run_number }}' ` -RunAttempt '${{ github.run_attempt }}' ` - -ReleaseVersion ([string]$policy.packageVersion) + -ReleaseVersion $identity.PackageVersion "package_version=$packageVersion" >> $env:GITHUB_OUTPUT - "release_tag=$releaseTag" >> $env:GITHUB_OUTPUT - "release_version=$($releaseTag.Substring(1))" >> $env:GITHUB_OUTPUT + "release_tag=$($identity.ReleaseTag)" >> $env:GITHUB_OUTPUT + "release_version=$($identity.ReleaseVersion)" >> $env:GITHUB_OUTPUT - name: Enforce official signing policy shell: pwsh diff --git a/README.md b/README.md index 1a519484..b17a856a 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,9 @@ The payload artifact records the requested ref and resolved upstream commit in OpenClaw commit, while embedded `payload-files.json` records every packaged application file's path, length, and SHA-256. -`release-policy.json` records the immutable OpenClaw commit and payload version -approved for official signing, plus the independent MSIX package version and -release tag. Updating that +`release-policy.json` records the immutable OpenClaw commit and Gateway tag +approved for official signing, plus an independent MSIX packaging revision. +Updating that policy requires a reviewed repository change. Official signing runs only from `main` and verifies the workflow input, policy-approved package version, both architecture metadata files, both MSIX hashes, the embedded manifests, and @@ -292,23 +292,34 @@ Test-signing private keys are generated only on the temporary GitHub runner and are deleted before artifacts are uploaded. No signing secret or private key is stored in the repository. -Official releases use the independent four-part numeric `packageVersion` and -`releaseTag` from `release-policy.json`. The initial signing proof uses package -version `0.0.0.0` and tag `v0.0.0.0`; a later policy change can establish the -long-term Gateway-to-MSIX version mapping. The workflow creates the tag in this -repository and a GitHub Release with generated release notes. Each release -contains a signed, multi-architecture +Official releases derive their GitHub tag and four-part numeric MSIX identity +from `gatewayTag` and `msixRevision` in `release-policy.json`. The GitHub tag is +`-msix.`. The MSIX identity is +`year.month.(patch * 1000 + correction * 100 + revision).0`; an absent Gateway +correction is zero. For example: + +- Gateway `v2026.9.4`, MSIX revision `0` becomes release tag + `v2026.9.4-msix.0` and MSIX version `2026.9.4000.0`; +- Gateway `v2026.7.1-2`, MSIX revision `0` becomes release tag + `v2026.7.1-2-msix.0` and MSIX version `2026.7.1200.0`; +- rebuilding that same Gateway at MSIX revision `1` becomes release tag + `v2026.7.1-2-msix.1` and MSIX version `2026.7.1201.0`. + +This preserves Gateway and packaging-release order while leaving the fourth +component at zero for future Microsoft Store compatibility. The encoding +supports Gateway patch values through 64, correction values through 9, and up +to 100 MSIX revisions per Gateway release. The workflow creates the derived +tag in this repository and a GitHub Release with generated release notes. Each +release contains a signed, multi-architecture `OpenClawGateway-.msixbundle` as the recommended download, plus signed `OpenClawGateway--x64.msix` and `OpenClawGateway--arm64.msix` packages for architecture-specific deployment. The duplicate GitHub Actions artifacts remain short-lived transport and diagnostic copies. -For the all-zero proof only, MakeAppx assigns the outer bundle identity its -date/time-based version because it does not preserve `0.0.0.0` as a bundle -version. The two embedded architecture packages retain identity version -`0.0.0.0`; signing authorization verifies those versions and byte-compares both -embedded packages with the approved standalone inputs. +The one-time `v0.0.0.0` signing proof predates this version policy and is not an +upgrade-compatible production baseline. Devices used to install that proof +should uninstall it before testing a normally versioned release. An `.msixbundle` is a single installable container for the x64 and ARM64 MSIX packages; Windows selects the package appropriate for the device. An diff --git a/release-policy.json b/release-policy.json index 33c1adb7..9b7a30ca 100644 --- a/release-policy.json +++ b/release-policy.json @@ -1,7 +1,7 @@ { "repository": "https://github.com/openclaw/openclaw", - "releaseTag": "v0.0.0.1", - "packageVersion": "0.0.0.1", + "gatewayTag": "v2026.8.2", + "msixRevision": 0, "payloadPackageVersion": "2026.8.2", "approvedCommit": "0965053fe6b9341776df147a6934b7485c60b5ca", "publisher": "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US" diff --git a/scripts/Get-MSIXReleaseIdentity.ps1 b/scripts/Get-MSIXReleaseIdentity.ps1 new file mode 100644 index 00000000..4f7c2651 --- /dev/null +++ b/scripts/Get-MSIXReleaseIdentity.ps1 @@ -0,0 +1,63 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$GatewayTag, + + [Parameter(Mandatory)] + [int]$MSIXRevision +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$match = [regex]::Match( + $GatewayTag.Trim(), + '^v(?\d{4})\.(?\d{1,2})\.(?\d{1,2})(?:-(?\d+))?$' +) +if (-not $match.Success) { + throw ( + "GatewayTag '$GatewayTag' must be a stable OpenClaw release tag " + + 'such as v2026.9.4 or v2026.7.1-2.' + ) +} + +[int]$year = $match.Groups['year'].Value +[int]$month = $match.Groups['month'].Value +[int]$patch = $match.Groups['patch'].Value +[int]$correction = if ($match.Groups['correction'].Success) { + $match.Groups['correction'].Value +} +else { + 0 +} + +if ($year -lt 1 -or $year -gt 65534) { + throw 'The Gateway release year must be between 1 and 65534.' +} +if ($month -lt 1 -or $month -gt 12) { + throw 'The Gateway release month must be between 1 and 12.' +} +if ($patch -lt 0 -or $patch -gt 64) { + throw 'The Gateway patch must be between 0 and 64.' +} +if ($correction -lt 0 -or $correction -gt 9) { + throw 'The Gateway correction must be between 0 and 9.' +} +if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 99) { + throw 'MSIXRevision must be between 0 and 99.' +} + +# Keep the fourth component at zero for Microsoft Store compatibility. Pack +# the Gateway patch, optional correction, and independent packaging revision +# into the build component while preserving their upgrade ordering. +[int]$build = ($patch * 1000) + ($correction * 100) + $MSIXRevision +$packageVersion = "$year.$month.$build.0" +$releaseTag = "$($GatewayTag.Trim())-msix.$MSIXRevision" + +[pscustomobject]@{ + GatewayTag = $GatewayTag.Trim() + MSIXRevision = $MSIXRevision + PackageVersion = $packageVersion + ReleaseTag = $releaseTag + ReleaseVersion = $releaseTag.Substring(1) +} diff --git a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 new file mode 100644 index 00000000..700af04e --- /dev/null +++ b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 @@ -0,0 +1,122 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptPath = Join-Path $PSScriptRoot 'Get-MSIXReleaseIdentity.ps1' + +function Assert-Identity { + param( + [Parameter(Mandatory)] + [string]$GatewayTag, + + [Parameter(Mandatory)] + [int]$MSIXRevision, + + [Parameter(Mandatory)] + [string]$PackageVersion, + + [Parameter(Mandatory)] + [string]$ReleaseTag + ) + + $identity = & $scriptPath ` + -GatewayTag $GatewayTag ` + -MSIXRevision $MSIXRevision + if ($identity.PackageVersion -ne $PackageVersion) { + throw ( + "Expected $GatewayTag revision $MSIXRevision to produce " + + "$PackageVersion; received $($identity.PackageVersion)." + ) + } + if ($identity.ReleaseTag -ne $ReleaseTag) { + throw ( + "Expected $GatewayTag revision $MSIXRevision to produce " + + "$ReleaseTag; received $($identity.ReleaseTag)." + ) + } + if ($identity.ReleaseVersion -ne $ReleaseTag.Substring(1)) { + throw 'ReleaseVersion did not match the release tag without its v prefix.' + } + if (-not $identity.PackageVersion.EndsWith('.0')) { + throw 'The MSIX revision component must remain zero for Store compatibility.' + } +} + +function Assert-Fails { + param( + [Parameter(Mandatory)] + [scriptblock]$Action, + + [Parameter(Mandatory)] + [string]$MessagePattern + ) + + try { + & $Action + } + catch { + if ($_.Exception.Message -notmatch $MessagePattern) { + throw ( + "Expected failure matching '$MessagePattern'; received: " + + $_.Exception.Message + ) + } + return + } + + throw "Expected failure matching '$MessagePattern', but the action succeeded." +} + +Assert-Identity ` + -GatewayTag 'v2026.9.4' ` + -MSIXRevision 0 ` + -PackageVersion '2026.9.4000.0' ` + -ReleaseTag 'v2026.9.4-msix.0' +Assert-Identity ` + -GatewayTag 'v2026.7.1-2' ` + -MSIXRevision 0 ` + -PackageVersion '2026.7.1200.0' ` + -ReleaseTag 'v2026.7.1-2-msix.0' +Assert-Identity ` + -GatewayTag 'v2026.7.1-2' ` + -MSIXRevision 1 ` + -PackageVersion '2026.7.1201.0' ` + -ReleaseTag 'v2026.7.1-2-msix.1' + +$gatewayCorrection = & $scriptPath ` + -GatewayTag 'v2026.7.1-2' ` + -MSIXRevision 0 +$packagingCorrection = & $scriptPath ` + -GatewayTag 'v2026.7.1-2' ` + -MSIXRevision 1 +$nextGatewayPatch = & $scriptPath ` + -GatewayTag 'v2026.7.2' ` + -MSIXRevision 0 +if ( + [version]$packagingCorrection.PackageVersion -le + [version]$gatewayCorrection.PackageVersion -or + [version]$nextGatewayPatch.PackageVersion -le + [version]$packagingCorrection.PackageVersion +) { + throw 'Derived MSIX versions do not preserve release ordering.' +} + +Assert-Fails -MessagePattern 'stable OpenClaw release tag' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-beta.1' -MSIXRevision 0 +} +Assert-Fails -MessagePattern 'month must be between 1 and 12' -Action { + & $scriptPath -GatewayTag 'v2026.13.1' -MSIXRevision 0 +} +Assert-Fails -MessagePattern 'patch must be between 0 and 64' -Action { + & $scriptPath -GatewayTag 'v2026.9.65' -MSIXRevision 0 +} +Assert-Fails -MessagePattern 'correction must be between 0 and 9' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-10' -MSIXRevision 0 +} +Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 99' -Action { + & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 100 +} + +Write-Host 'MSIX release-identity tests passed.' diff --git a/scripts/Test-SigningInputs.Tests.ps1 b/scripts/Test-SigningInputs.Tests.ps1 index 003c6bb4..d840955e 100644 --- a/scripts/Test-SigningInputs.Tests.ps1 +++ b/scripts/Test-SigningInputs.Tests.ps1 @@ -8,12 +8,12 @@ $repositoryRoot = Split-Path $PSScriptRoot -Parent $policyPath = Join-Path $repositoryRoot 'release-policy.json' $policy = Get-Content -LiteralPath $policyPath -Raw | ConvertFrom-Json $approvedCommit = [string]$policy.approvedCommit -$approvedPackageVersion = & ( - Join-Path $PSScriptRoot 'Get-WorkflowPackageVersion.ps1' +$releaseIdentity = & ( + Join-Path $PSScriptRoot 'Get-MSIXReleaseIdentity.ps1' ) ` - -RunNumber 1 ` - -RunAttempt 1 ` - -ReleaseVersion ([string]$policy.packageVersion) + -GatewayTag ([string]$policy.gatewayTag) ` + -MSIXRevision ([int]$policy.msixRevision) +$approvedPackageVersion = $releaseIdentity.PackageVersion $approvedPayloadVersion = [string]$policy.payloadPackageVersion $packagingCommit = '1111111111111111111111111111111111111111' $testRoot = Join-Path $env:TEMP ( @@ -233,7 +233,7 @@ function New-TestBundle { $Root ` 'x64\OpenClawGateway-x64.msix'), - [string]$BundleVersion = '2026.912.815.0' + [string]$BundleVersion = $approvedPackageVersion ) $bundleDirectory = Join-Path $Root 'bundle' @@ -386,6 +386,14 @@ try { -MessagePattern 'Node.js runtime versions do not match' ` -Action { Invoke-PolicyValidation -Root $testRoot } + Reset-TestArtifacts + New-TestBundle -Root $testRoot -BundleVersion '2026.8.2001.0' + Assert-Fails ` + -MessagePattern 'bundle manifest identity is unexpected' ` + -Action { + Invoke-PolicyValidation -Root $testRoot -PreserveBundle + } + Reset-TestArtifacts Assert-Fails ` -MessagePattern 'approved immutable OpenClaw commit' ` diff --git a/scripts/Test-SigningInputs.ps1 b/scripts/Test-SigningInputs.ps1 index 14c6171b..8811f68f 100644 --- a/scripts/Test-SigningInputs.ps1 +++ b/scripts/Test-SigningInputs.ps1 @@ -153,22 +153,22 @@ $policy = Get-Content -LiteralPath $resolvedPolicyPath -Raw | if ( $policy.repository -ne 'https://github.com/openclaw/openclaw' -or - [string]::IsNullOrWhiteSpace([string]$policy.releaseTag) -or - $policy.packageVersion -notmatch '^\d+\.\d+\.\d+\.\d+$' -or - $policy.releaseTag -ne "v$($policy.packageVersion)" -or + [string]::IsNullOrWhiteSpace([string]$policy.gatewayTag) -or + $policy.msixRevision -isnot [int64] -or [string]::IsNullOrWhiteSpace([string]$policy.payloadPackageVersion) -or + $policy.gatewayTag -ne "v$($policy.payloadPackageVersion)" -or $policy.approvedCommit -notmatch '^[0-9a-fA-F]{40}$' -or [string]::IsNullOrWhiteSpace([string]$policy.publisher) ) { throw 'The Gateway MSIX release policy is invalid.' } -$approvedPackageVersion = & ( - Join-Path $PSScriptRoot 'Get-WorkflowPackageVersion.ps1' +$releaseIdentity = & ( + Join-Path $PSScriptRoot 'Get-MSIXReleaseIdentity.ps1' ) ` - -RunNumber 1 ` - -RunAttempt 1 ` - -ReleaseVersion ([string]$policy.packageVersion) + -GatewayTag ([string]$policy.gatewayTag) ` + -MSIXRevision ([int]$policy.msixRevision) +$approvedPackageVersion = $releaseIdentity.PackageVersion $approvedPayloadVersion = [string]$policy.payloadPackageVersion $approvedCommit = ([string]$policy.approvedCommit).ToLowerInvariant() $normalizedRequestedRef = $RequestedRef.Trim().ToLowerInvariant() @@ -446,7 +446,8 @@ try { $null -eq $bundleIdentity -or $bundleIdentity.Name -ne 'OpenClaw.Gateway' -or $bundleIdentity.Publisher -ne $policy.publisher -or - -not $bundleVersionIsValid + -not $bundleVersionIsValid -or + $bundleVersion -ne $approvedPackageVersion ) { throw 'The MSIX bundle manifest identity is unexpected.' } diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index 31c73bdf..a85e1bec 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -51,6 +51,7 @@ $requiredFragments = @( 'signing-account-name: openclaw' 'certificate-profile-name: openclaw' 'name: Publish signed Gateway MSIX release' + '.\scripts\Get-MSIXReleaseIdentity.ps1' 'contents: write' 'uses: softprops/action-gh-release@v3' 'tag_name: ${{ needs.authorize-signing.outputs.release_tag }}' From 240f1fa318b38838b53c5f281bace1e1cb9aff23 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 13 Sep 2026 22:19:50 -0700 Subject: [PATCH 02/13] fix(ci): use readable MSIX release versions --- README.md | 29 +++++++-------- scripts/Get-MSIXReleaseIdentity.ps1 | 31 +++++++++------- scripts/Test-MSIXReleaseIdentity.Tests.ps1 | 41 ++++++++++++---------- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b17a856a..79de5290 100644 --- a/README.md +++ b/README.md @@ -295,22 +295,23 @@ key is stored in the repository. Official releases derive their GitHub tag and four-part numeric MSIX identity from `gatewayTag` and `msixRevision` in `release-policy.json`. The GitHub tag is `-msix.`. The MSIX identity is -`year.month.(patch * 1000 + correction * 100 + revision).0`; an absent Gateway -correction is zero. For example: +`year.month.patch.revision`, where `revision` is an explicit monotonically +increasing package sequence for that Gateway year/month/patch line. For example: - Gateway `v2026.9.4`, MSIX revision `0` becomes release tag - `v2026.9.4-msix.0` and MSIX version `2026.9.4000.0`; -- Gateway `v2026.7.1-2`, MSIX revision `0` becomes release tag - `v2026.7.1-2-msix.0` and MSIX version `2026.7.1200.0`; -- rebuilding that same Gateway at MSIX revision `1` becomes release tag - `v2026.7.1-2-msix.1` and MSIX version `2026.7.1201.0`. - -This preserves Gateway and packaging-release order while leaving the fourth -component at zero for future Microsoft Store compatibility. The encoding -supports Gateway patch values through 64, correction values through 9, and up -to 100 MSIX revisions per Gateway release. The workflow creates the derived -tag in this repository and a GitHub Release with generated release notes. Each -release contains a signed, multi-architecture + `v2026.9.4-msix.0` and MSIX version `2026.9.4.0`; +- Gateway `v2026.7.1-2`, MSIX revision `2` becomes release tag + `v2026.7.1-2-msix.2` and MSIX version `2026.7.1.2`; +- rebuilding that same Gateway at MSIX revision `3` becomes release tag + `v2026.7.1-2-msix.3` and MSIX version `2026.7.1.3`. + +Set `msixRevision` at least as high as the Gateway correction suffix and higher +than every package already published for the same year/month/patch line. This +preserves upgrade order while keeping versions readable. Microsoft Store +submissions reserve the fourth component as zero, so Store publication will +need its own version policy when it is introduced. The workflow creates the +derived tag in this repository and a GitHub Release with generated release +notes. Each release contains a signed, multi-architecture `OpenClawGateway-.msixbundle` as the recommended download, plus signed `OpenClawGateway--x64.msix` and `OpenClawGateway--arm64.msix` packages for architecture-specific diff --git a/scripts/Get-MSIXReleaseIdentity.ps1 b/scripts/Get-MSIXReleaseIdentity.ps1 index 4f7c2651..b2056e68 100644 --- a/scripts/Get-MSIXReleaseIdentity.ps1 +++ b/scripts/Get-MSIXReleaseIdentity.ps1 @@ -31,27 +31,32 @@ else { 0 } -if ($year -lt 1 -or $year -gt 65534) { - throw 'The Gateway release year must be between 1 and 65534.' +if ($year -lt 1 -or $year -gt 65535) { + throw 'The Gateway release year must be between 1 and 65535.' } if ($month -lt 1 -or $month -gt 12) { throw 'The Gateway release month must be between 1 and 12.' } -if ($patch -lt 0 -or $patch -gt 64) { - throw 'The Gateway patch must be between 0 and 64.' +if ($patch -lt 0 -or $patch -gt 65535) { + throw 'The Gateway patch must be between 0 and 65535.' } -if ($correction -lt 0 -or $correction -gt 9) { - throw 'The Gateway correction must be between 0 and 9.' +if ($correction -lt 0 -or $correction -gt 65535) { + throw 'The Gateway correction must be between 0 and 65535.' } -if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 99) { - throw 'MSIXRevision must be between 0 and 99.' +if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 65535) { + throw 'MSIXRevision must be between 0 and 65535.' +} +if ($MSIXRevision -lt $correction) { + throw ( + "MSIXRevision $MSIXRevision must be at least the Gateway correction " + + "$correction so package versions remain monotonic." + ) } -# Keep the fourth component at zero for Microsoft Store compatibility. Pack -# the Gateway patch, optional correction, and independent packaging revision -# into the build component while preserving their upgrade ordering. -[int]$build = ($patch * 1000) + ($correction * 100) + $MSIXRevision -$packageVersion = "$year.$month.$build.0" +# Use the fourth component as the explicit, monotonically increasing package +# sequence for a Gateway year/month/patch line. This keeps the version legible; +# exact Gateway provenance remains in the release tag and package metadata. +$packageVersion = "$year.$month.$patch.$MSIXRevision" $releaseTag = "$($GatewayTag.Trim())-msix.$MSIXRevision" [pscustomobject]@{ diff --git a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 index 700af04e..620ae70d 100644 --- a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 +++ b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 @@ -39,9 +39,6 @@ function Assert-Identity { if ($identity.ReleaseVersion -ne $ReleaseTag.Substring(1)) { throw 'ReleaseVersion did not match the release tag without its v prefix.' } - if (-not $identity.PackageVersion.EndsWith('.0')) { - throw 'The MSIX revision component must remain zero for Store compatibility.' - } } function Assert-Fails { @@ -72,25 +69,30 @@ function Assert-Fails { Assert-Identity ` -GatewayTag 'v2026.9.4' ` -MSIXRevision 0 ` - -PackageVersion '2026.9.4000.0' ` + -PackageVersion '2026.9.4.0' ` -ReleaseTag 'v2026.9.4-msix.0' Assert-Identity ` - -GatewayTag 'v2026.7.1-2' ` + -GatewayTag 'v2026.7.12' ` -MSIXRevision 0 ` - -PackageVersion '2026.7.1200.0' ` - -ReleaseTag 'v2026.7.1-2-msix.0' + -PackageVersion '2026.7.12.0' ` + -ReleaseTag 'v2026.7.12-msix.0' +Assert-Identity ` + -GatewayTag 'v2026.7.1-2' ` + -MSIXRevision 2 ` + -PackageVersion '2026.7.1.2' ` + -ReleaseTag 'v2026.7.1-2-msix.2' Assert-Identity ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 1 ` - -PackageVersion '2026.7.1201.0' ` - -ReleaseTag 'v2026.7.1-2-msix.1' + -MSIXRevision 3 ` + -PackageVersion '2026.7.1.3' ` + -ReleaseTag 'v2026.7.1-2-msix.3' $gatewayCorrection = & $scriptPath ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 0 + -MSIXRevision 2 $packagingCorrection = & $scriptPath ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 1 + -MSIXRevision 3 $nextGatewayPatch = & $scriptPath ` -GatewayTag 'v2026.7.2' ` -MSIXRevision 0 @@ -109,14 +111,17 @@ Assert-Fails -MessagePattern 'stable OpenClaw release tag' -Action { Assert-Fails -MessagePattern 'month must be between 1 and 12' -Action { & $scriptPath -GatewayTag 'v2026.13.1' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'patch must be between 0 and 64' -Action { - & $scriptPath -GatewayTag 'v2026.9.65' -MSIXRevision 0 +Assert-Fails -MessagePattern 'patch must be between 0 and 65535' -Action { + & $scriptPath -GatewayTag 'v2026.9.65536' -MSIXRevision 0 +} +Assert-Fails -MessagePattern 'correction must be between 0 and 65535' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-65536' -MSIXRevision 65535 } -Assert-Fails -MessagePattern 'correction must be between 0 and 9' -Action { - & $scriptPath -GatewayTag 'v2026.9.4-10' -MSIXRevision 0 +Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 65535' -Action { + & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 65536 } -Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 99' -Action { - & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 100 +Assert-Fails -MessagePattern 'must be at least the Gateway correction 2' -Action { + & $scriptPath -GatewayTag 'v2026.7.1-2' -MSIXRevision 1 } Write-Host 'MSIX release-identity tests passed.' From a5dd261a03a6dacaf42a6ef42faca2fe05f6dbf9 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 13 Sep 2026 22:22:56 -0700 Subject: [PATCH 03/13] fix(ci): accept full MSIX patch range --- scripts/Get-MSIXReleaseIdentity.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/Get-MSIXReleaseIdentity.ps1 b/scripts/Get-MSIXReleaseIdentity.ps1 index b2056e68..46561959 100644 --- a/scripts/Get-MSIXReleaseIdentity.ps1 +++ b/scripts/Get-MSIXReleaseIdentity.ps1 @@ -12,7 +12,7 @@ $ErrorActionPreference = 'Stop' $match = [regex]::Match( $GatewayTag.Trim(), - '^v(?\d{4})\.(?\d{1,2})\.(?\d{1,2})(?:-(?\d+))?$' + '^v(?\d{4})\.(?\d{1,2})\.(?\d{1,5})(?:-(?\d+))?$' ) if (-not $match.Success) { throw ( @@ -31,8 +31,8 @@ else { 0 } -if ($year -lt 1 -or $year -gt 65535) { - throw 'The Gateway release year must be between 1 and 65535.' +if ($year -lt 1 -or $year -gt 9999) { + throw 'The Gateway release year must be between 1 and 9999.' } if ($month -lt 1 -or $month -gt 12) { throw 'The Gateway release month must be between 1 and 12.' From caf87115929479beabc9d27542b00c6137d479ac Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 13 Sep 2026 22:47:10 -0700 Subject: [PATCH 04/13] fix(ci): reserve MSIX rebuild slots per gateway correction --- README.md | 24 ++++++---- scripts/Get-MSIXReleaseIdentity.ps1 | 30 ++++++------ scripts/Test-MSIXReleaseIdentity.Tests.ps1 | 56 +++++++++++++++------- 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 79de5290..c7146513 100644 --- a/README.md +++ b/README.md @@ -295,19 +295,23 @@ key is stored in the repository. Official releases derive their GitHub tag and four-part numeric MSIX identity from `gatewayTag` and `msixRevision` in `release-policy.json`. The GitHub tag is `-msix.`. The MSIX identity is -`year.month.patch.revision`, where `revision` is an explicit monotonically -increasing package sequence for that Gateway year/month/patch line. For example: +`year.month.patch.(gateway-correction * 10 + msix-revision)`. Each Gateway +correction gets ten deterministic MSIX-only rebuild slots, so rebuilding one +Gateway release cannot shift the version assigned to a later correction. For +example: - Gateway `v2026.9.4`, MSIX revision `0` becomes release tag `v2026.9.4-msix.0` and MSIX version `2026.9.4.0`; -- Gateway `v2026.7.1-2`, MSIX revision `2` becomes release tag - `v2026.7.1-2-msix.2` and MSIX version `2026.7.1.2`; -- rebuilding that same Gateway at MSIX revision `3` becomes release tag - `v2026.7.1-2-msix.3` and MSIX version `2026.7.1.3`. - -Set `msixRevision` at least as high as the Gateway correction suffix and higher -than every package already published for the same year/month/patch line. This -preserves upgrade order while keeping versions readable. Microsoft Store +- Gateway `v2026.7.1`, MSIX revision `1` becomes release tag + `v2026.7.1-msix.1` and MSIX version `2026.7.1.1`; +- Gateway correction `v2026.7.1-2`, MSIX revision `0` becomes release tag + `v2026.7.1-2-msix.0` and MSIX version `2026.7.1.20`; +- rebuilding that correction at MSIX revision `1` becomes release tag + `v2026.7.1-2-msix.1` and MSIX version `2026.7.1.21`. + +Set `msixRevision` from `0` through `9`, incrementing it only when the same +Gateway tag is repackaged. The Gateway correction suffix is encoded separately, +so later Gateway corrections keep their deterministic version. Microsoft Store submissions reserve the fourth component as zero, so Store publication will need its own version policy when it is introduced. The workflow creates the derived tag in this repository and a GitHub Release with generated release diff --git a/scripts/Get-MSIXReleaseIdentity.ps1 b/scripts/Get-MSIXReleaseIdentity.ps1 index 46561959..67c6125e 100644 --- a/scripts/Get-MSIXReleaseIdentity.ps1 +++ b/scripts/Get-MSIXReleaseIdentity.ps1 @@ -37,26 +37,24 @@ if ($year -lt 1 -or $year -gt 9999) { if ($month -lt 1 -or $month -gt 12) { throw 'The Gateway release month must be between 1 and 12.' } -if ($patch -lt 0 -or $patch -gt 65535) { - throw 'The Gateway patch must be between 0 and 65535.' +if ($patch -lt 0 -or $patch -gt 65534) { + throw 'The Gateway patch must be between 0 and 65534.' } -if ($correction -lt 0 -or $correction -gt 65535) { - throw 'The Gateway correction must be between 0 and 65535.' +if ($correction -lt 0 -or $correction -gt 6553) { + throw 'The Gateway correction must be between 0 and 6553.' } -if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 65535) { - throw 'MSIXRevision must be between 0 and 65535.' -} -if ($MSIXRevision -lt $correction) { - throw ( - "MSIXRevision $MSIXRevision must be at least the Gateway correction " + - "$correction so package versions remain monotonic." - ) +if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 9) { + throw 'MSIXRevision must be between 0 and 9.' } -# Use the fourth component as the explicit, monotonically increasing package -# sequence for a Gateway year/month/patch line. This keeps the version legible; -# exact Gateway provenance remains in the release tag and package metadata. -$packageVersion = "$year.$month.$patch.$MSIXRevision" +# Give every Gateway correction ten deterministic MSIX revision slots. This +# prevents an MSIX-only rebuild from consuming the number assigned to a later +# Gateway correction while keeping the fourth component short and readable. +$packageRevision = ($correction * 10) + $MSIXRevision +if ($packageRevision -gt 65534) { + throw 'The combined Gateway correction and MSIXRevision must not exceed 65534.' +} +$packageVersion = "$year.$month.$patch.$packageRevision" $releaseTag = "$($GatewayTag.Trim())-msix.$MSIXRevision" [pscustomobject]@{ diff --git a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 index 620ae70d..b5bc4ed6 100644 --- a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 +++ b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 @@ -5,6 +5,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $scriptPath = Join-Path $PSScriptRoot 'Get-MSIXReleaseIdentity.ps1' +$workflowVersionScriptPath = Join-Path $PSScriptRoot 'Get-WorkflowPackageVersion.ps1' function Assert-Identity { param( @@ -78,29 +79,34 @@ Assert-Identity ` -ReleaseTag 'v2026.7.12-msix.0' Assert-Identity ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 2 ` - -PackageVersion '2026.7.1.2' ` - -ReleaseTag 'v2026.7.1-2-msix.2' + -MSIXRevision 0 ` + -PackageVersion '2026.7.1.20' ` + -ReleaseTag 'v2026.7.1-2-msix.0' Assert-Identity ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 3 ` - -PackageVersion '2026.7.1.3' ` - -ReleaseTag 'v2026.7.1-2-msix.3' + -MSIXRevision 1 ` + -PackageVersion '2026.7.1.21' ` + -ReleaseTag 'v2026.7.1-2-msix.1' $gatewayCorrection = & $scriptPath ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 2 + -MSIXRevision 0 $packagingCorrection = & $scriptPath ` -GatewayTag 'v2026.7.1-2' ` - -MSIXRevision 3 + -MSIXRevision 1 +$nextGatewayCorrection = & $scriptPath ` + -GatewayTag 'v2026.7.1-3' ` + -MSIXRevision 0 $nextGatewayPatch = & $scriptPath ` -GatewayTag 'v2026.7.2' ` -MSIXRevision 0 if ( [version]$packagingCorrection.PackageVersion -le [version]$gatewayCorrection.PackageVersion -or + [version]$nextGatewayCorrection.PackageVersion -le + [version]$packagingCorrection.PackageVersion -or [version]$nextGatewayPatch.PackageVersion -le - [version]$packagingCorrection.PackageVersion + [version]$nextGatewayCorrection.PackageVersion ) { throw 'Derived MSIX versions do not preserve release ordering.' } @@ -111,17 +117,33 @@ Assert-Fails -MessagePattern 'stable OpenClaw release tag' -Action { Assert-Fails -MessagePattern 'month must be between 1 and 12' -Action { & $scriptPath -GatewayTag 'v2026.13.1' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'patch must be between 0 and 65535' -Action { - & $scriptPath -GatewayTag 'v2026.9.65536' -MSIXRevision 0 +Assert-Fails -MessagePattern 'patch must be between 0 and 65534' -Action { + & $scriptPath -GatewayTag 'v2026.9.65535' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'correction must be between 0 and 65535' -Action { - & $scriptPath -GatewayTag 'v2026.9.4-65536' -MSIXRevision 65535 +Assert-Fails -MessagePattern 'correction must be between 0 and 6553' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-6554' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 65535' -Action { - & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 65536 +Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 9' -Action { + & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 10 } -Assert-Fails -MessagePattern 'must be at least the Gateway correction 2' -Action { - & $scriptPath -GatewayTag 'v2026.7.1-2' -MSIXRevision 1 +Assert-Fails -MessagePattern 'combined Gateway correction and MSIXRevision' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-6553' -MSIXRevision 5 +} +Assert-Identity ` + -GatewayTag 'v2026.9.4-6553' ` + -MSIXRevision 4 ` + -PackageVersion '2026.9.4.65534' ` + -ReleaseTag 'v2026.9.4-6553-msix.4' + +$maximumIdentity = & $scriptPath ` + -GatewayTag 'v2026.9.4-6553' ` + -MSIXRevision 4 +$validatedMaximumVersion = & $workflowVersionScriptPath ` + -RunNumber 1 ` + -RunAttempt 1 ` + -ReleaseVersion $maximumIdentity.PackageVersion +if ($validatedMaximumVersion -ne '2026.9.4.65534') { + throw 'The maximum derived identity did not pass workflow validation.' } Write-Host 'MSIX release-identity tests passed.' From 0baf4c7f9abf4f6eca7af4cab4b6a7f0afa966a3 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 18:55:28 -0700 Subject: [PATCH 05/13] fix(ci): reserve thousand-wide MSIX release slots --- README.md | 16 ++++---- scripts/Get-MSIXReleaseIdentity.ps1 | 26 +++++++------ scripts/Test-MSIXReleaseIdentity.Tests.ps1 | 44 +++++++++++++--------- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c7146513..2f6f7f7a 100644 --- a/README.md +++ b/README.md @@ -295,21 +295,23 @@ key is stored in the repository. Official releases derive their GitHub tag and four-part numeric MSIX identity from `gatewayTag` and `msixRevision` in `release-policy.json`. The GitHub tag is `-msix.`. The MSIX identity is -`year.month.patch.(gateway-correction * 10 + msix-revision)`. Each Gateway -correction gets ten deterministic MSIX-only rebuild slots, so rebuilding one +`year.month.patch.(gateway-release-sequence * 1000 + msix-revision)`. The +unsuffixed Gateway tag is release sequence 1; a correction suffix such as `-2` +is release sequence 2. Each Gateway release gets 1,000 deterministic MSIX-only +rebuild slots, so rebuilding one Gateway release cannot shift the version assigned to a later correction. For example: - Gateway `v2026.9.4`, MSIX revision `0` becomes release tag - `v2026.9.4-msix.0` and MSIX version `2026.9.4.0`; + `v2026.9.4-msix.0` and MSIX version `2026.9.4.1000`; - Gateway `v2026.7.1`, MSIX revision `1` becomes release tag - `v2026.7.1-msix.1` and MSIX version `2026.7.1.1`; + `v2026.7.1-msix.1` and MSIX version `2026.7.1.1001`; - Gateway correction `v2026.7.1-2`, MSIX revision `0` becomes release tag - `v2026.7.1-2-msix.0` and MSIX version `2026.7.1.20`; + `v2026.7.1-2-msix.0` and MSIX version `2026.7.1.2000`; - rebuilding that correction at MSIX revision `1` becomes release tag - `v2026.7.1-2-msix.1` and MSIX version `2026.7.1.21`. + `v2026.7.1-2-msix.1` and MSIX version `2026.7.1.2001`. -Set `msixRevision` from `0` through `9`, incrementing it only when the same +Set `msixRevision` from `0` through `999`, incrementing it only when the same Gateway tag is repackaged. The Gateway correction suffix is encoded separately, so later Gateway corrections keep their deterministic version. Microsoft Store submissions reserve the fourth component as zero, so Store publication will diff --git a/scripts/Get-MSIXReleaseIdentity.ps1 b/scripts/Get-MSIXReleaseIdentity.ps1 index 67c6125e..3730313a 100644 --- a/scripts/Get-MSIXReleaseIdentity.ps1 +++ b/scripts/Get-MSIXReleaseIdentity.ps1 @@ -24,11 +24,15 @@ if (-not $match.Success) { [int]$year = $match.Groups['year'].Value [int]$month = $match.Groups['month'].Value [int]$patch = $match.Groups['patch'].Value -[int]$correction = if ($match.Groups['correction'].Success) { - $match.Groups['correction'].Value +[int]$releaseSequence = if ($match.Groups['correction'].Success) { + [int]$correction = $match.Groups['correction'].Value + if ($correction -lt 2 -or $correction -gt 64) { + throw 'The Gateway correction suffix must be between 2 and 64.' + } + $correction } else { - 0 + 1 } if ($year -lt 1 -or $year -gt 9999) { @@ -40,19 +44,17 @@ if ($month -lt 1 -or $month -gt 12) { if ($patch -lt 0 -or $patch -gt 65534) { throw 'The Gateway patch must be between 0 and 65534.' } -if ($correction -lt 0 -or $correction -gt 6553) { - throw 'The Gateway correction must be between 0 and 6553.' -} -if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 9) { - throw 'MSIXRevision must be between 0 and 9.' +if ($MSIXRevision -lt 0 -or $MSIXRevision -gt 999) { + throw 'MSIXRevision must be between 0 and 999.' } -# Give every Gateway correction ten deterministic MSIX revision slots. This +# Give every Gateway release sequence 1,000 deterministic MSIX revision slots. The +# unsuffixed tag is sequence 1 and correction tags use their numeric suffix. This # prevents an MSIX-only rebuild from consuming the number assigned to a later -# Gateway correction while keeping the fourth component short and readable. -$packageRevision = ($correction * 10) + $MSIXRevision +# Gateway correction. +$packageRevision = ($releaseSequence * 1000) + $MSIXRevision if ($packageRevision -gt 65534) { - throw 'The combined Gateway correction and MSIXRevision must not exceed 65534.' + throw 'The combined Gateway release sequence and MSIXRevision must not exceed 65534.' } $packageVersion = "$year.$month.$patch.$packageRevision" $releaseTag = "$($GatewayTag.Trim())-msix.$MSIXRevision" diff --git a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 index b5bc4ed6..99c2be10 100644 --- a/scripts/Test-MSIXReleaseIdentity.Tests.ps1 +++ b/scripts/Test-MSIXReleaseIdentity.Tests.ps1 @@ -70,22 +70,32 @@ function Assert-Fails { Assert-Identity ` -GatewayTag 'v2026.9.4' ` -MSIXRevision 0 ` - -PackageVersion '2026.9.4.0' ` + -PackageVersion '2026.9.4.1000' ` -ReleaseTag 'v2026.9.4-msix.0' Assert-Identity ` -GatewayTag 'v2026.7.12' ` -MSIXRevision 0 ` - -PackageVersion '2026.7.12.0' ` + -PackageVersion '2026.7.12.1000' ` -ReleaseTag 'v2026.7.12-msix.0' +Assert-Identity ` + -GatewayTag 'v2026.7.2' ` + -MSIXRevision 0 ` + -PackageVersion '2026.7.2.1000' ` + -ReleaseTag 'v2026.7.2-msix.0' +Assert-Identity ` + -GatewayTag 'v2026.7.1' ` + -MSIXRevision 1 ` + -PackageVersion '2026.7.1.1001' ` + -ReleaseTag 'v2026.7.1-msix.1' Assert-Identity ` -GatewayTag 'v2026.7.1-2' ` -MSIXRevision 0 ` - -PackageVersion '2026.7.1.20' ` + -PackageVersion '2026.7.1.2000' ` -ReleaseTag 'v2026.7.1-2-msix.0' Assert-Identity ` -GatewayTag 'v2026.7.1-2' ` -MSIXRevision 1 ` - -PackageVersion '2026.7.1.21' ` + -PackageVersion '2026.7.1.2001' ` -ReleaseTag 'v2026.7.1-2-msix.1' $gatewayCorrection = & $scriptPath ` @@ -120,29 +130,29 @@ Assert-Fails -MessagePattern 'month must be between 1 and 12' -Action { Assert-Fails -MessagePattern 'patch must be between 0 and 65534' -Action { & $scriptPath -GatewayTag 'v2026.9.65535' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'correction must be between 0 and 6553' -Action { - & $scriptPath -GatewayTag 'v2026.9.4-6554' -MSIXRevision 0 +Assert-Fails -MessagePattern 'correction suffix must be between 2 and 64' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-1' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 9' -Action { - & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 10 +Assert-Fails -MessagePattern 'correction suffix must be between 2 and 64' -Action { + & $scriptPath -GatewayTag 'v2026.9.4-65' -MSIXRevision 0 } -Assert-Fails -MessagePattern 'combined Gateway correction and MSIXRevision' -Action { - & $scriptPath -GatewayTag 'v2026.9.4-6553' -MSIXRevision 5 +Assert-Fails -MessagePattern 'MSIXRevision must be between 0 and 999' -Action { + & $scriptPath -GatewayTag 'v2026.9.4' -MSIXRevision 1000 } Assert-Identity ` - -GatewayTag 'v2026.9.4-6553' ` - -MSIXRevision 4 ` - -PackageVersion '2026.9.4.65534' ` - -ReleaseTag 'v2026.9.4-6553-msix.4' + -GatewayTag 'v2026.9.4-64' ` + -MSIXRevision 999 ` + -PackageVersion '2026.9.4.64999' ` + -ReleaseTag 'v2026.9.4-64-msix.999' $maximumIdentity = & $scriptPath ` - -GatewayTag 'v2026.9.4-6553' ` - -MSIXRevision 4 + -GatewayTag 'v2026.9.4-64' ` + -MSIXRevision 999 $validatedMaximumVersion = & $workflowVersionScriptPath ` -RunNumber 1 ` -RunAttempt 1 ` -ReleaseVersion $maximumIdentity.PackageVersion -if ($validatedMaximumVersion -ne '2026.9.4.65534') { +if ($validatedMaximumVersion -ne '2026.9.4.64999') { throw 'The maximum derived identity did not pass workflow validation.' } From 1575053d6a2611e5e588041d21df30bfa800e2e4 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 19:10:06 -0700 Subject: [PATCH 06/13] docs: explain Gateway MSIX versioning policy --- CONTRIBUTING.md | 7 ++++++ README.md | 59 ++++++++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87afd115..abe65115 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,7 @@ or package version logic: .\scripts\Test-PackagingRelevance.Tests.ps1 .\scripts\Test-OpenClawCacheKey.Tests.ps1 .\scripts\Test-OpenClawPackage.Tests.ps1 +.\scripts\Test-MSIXReleaseIdentity.Tests.ps1 .\scripts\Test-WorkflowPackageVersion.Tests.ps1 .\scripts\Test-GitHooks.Tests.ps1 ``` @@ -183,6 +184,12 @@ bypassable, and required CI checks remain authoritative. - Metadata files are part of the release trust chain. Coordinate changes across payload creation, MSIX creation, signing validation, workflow artifacts, and tests. +- Keep official release identity derived from `gatewayTag` and `msixRevision`. + The unsuffixed Gateway tag owns revision block `1000-1999`; correction tags + `-2` through `-64` own their corresponding 1,000-number blocks. Use revision + `0` for the first MSIX of a Gateway tag and increment only for packaging-only + rebuilds of that exact tag. Do not assign package versions or release tags by + hand. - Use source-generated `System.Text.Json` metadata through `OpenClawJsonContext`. The launcher is NativeAOT and must not introduce reflection-based serialization. diff --git a/README.md b/README.md index 2f6f7f7a..5256c80a 100644 --- a/README.md +++ b/README.md @@ -295,35 +295,48 @@ key is stored in the repository. Official releases derive their GitHub tag and four-part numeric MSIX identity from `gatewayTag` and `msixRevision` in `release-policy.json`. The GitHub tag is `-msix.`. The MSIX identity is -`year.month.patch.(gateway-release-sequence * 1000 + msix-revision)`. The -unsuffixed Gateway tag is release sequence 1; a correction suffix such as `-2` -is release sequence 2. Each Gateway release gets 1,000 deterministic MSIX-only -rebuild slots, so rebuilding one -Gateway release cannot shift the version assigned to a later correction. For -example: - -- Gateway `v2026.9.4`, MSIX revision `0` becomes release tag - `v2026.9.4-msix.0` and MSIX version `2026.9.4.1000`; -- Gateway `v2026.7.1`, MSIX revision `1` becomes release tag - `v2026.7.1-msix.1` and MSIX version `2026.7.1.1001`; -- Gateway correction `v2026.7.1-2`, MSIX revision `0` becomes release tag - `v2026.7.1-2-msix.0` and MSIX version `2026.7.1.2000`; -- rebuilding that correction at MSIX revision `1` becomes release tag - `v2026.7.1-2-msix.1` and MSIX version `2026.7.1.2001`. - -Set `msixRevision` from `0` through `999`, incrementing it only when the same -Gateway tag is repackaged. The Gateway correction suffix is encoded separately, -so later Gateway corrections keep their deterministic version. Microsoft Store -submissions reserve the fourth component as zero, so Store publication will -need its own version policy when it is introduced. The workflow creates the -derived tag in this repository and a GitHub Release with generated release -notes. Each release contains a signed, multi-architecture +`year.month.patch.(gateway-release-sequence * 1000 + msix-revision)`. + +| Gateway tag | MSIX revision | GitHub release tag | MSIX version | +|---|---:|---|---| +| `v2026.7.1` | `0` | `v2026.7.1-msix.0` | `2026.7.1.1000` | +| `v2026.7.1-2` | `0` | `v2026.7.1-2-msix.0` | `2026.7.1.2000` | +| `v2026.7.1-2` | `1` | `v2026.7.1-2-msix.1` | `2026.7.1.2001` | +| `v2026.7.2` | `0` | `v2026.7.2-msix.0` | `2026.7.2.1000` | + +The unsuffixed Gateway tag is release sequence `1`; correction suffixes `-2` +through `-64` use their numeric suffix as the sequence. A `-1` suffix is +rejected because it would collide with the unsuffixed tag. Set `msixRevision` +from `0` through `999`, starting at `0` for each Gateway tag and incrementing it +only when that exact Gateway tag is repackaged. Each Gateway release therefore +owns a deterministic 1,000-number block, and an MSIX-only rebuild cannot shift +the version assigned to a later Gateway correction or patch. + +To prepare an official release, update these policy inputs together in a +reviewed pull request: + +1. `gatewayTag` to the stable upstream Gateway tag; +2. `approvedCommit` to the immutable commit resolved from that tag; +3. `payloadPackageVersion` to the version reported by the pinned payload; +4. `msixRevision` to `0`, or increment it for a packaging-only rebuild of the + same Gateway tag; +5. the workflow's `openclaw_ref` default and non-manual fallback to the same + `approvedCommit`. + +After that pull request merges, manually run **Build OpenClaw Gateway MSIX** on +`main` with `openclaw_ref` set to the approved commit and `signing_mode` set to +`official`. The workflow derives the package version and release tag, creates +the tag in this repository, and publishes a GitHub Release with generated +release notes. Each release contains a signed, multi-architecture `OpenClawGateway-.msixbundle` as the recommended download, plus signed `OpenClawGateway--x64.msix` and `OpenClawGateway--arm64.msix` packages for architecture-specific deployment. The duplicate GitHub Actions artifacts remain short-lived transport and diagnostic copies. +Microsoft Store submissions reserve the fourth version component as zero, so +Store publication will need its own version policy when it is introduced. + The one-time `v0.0.0.0` signing proof predates this version policy and is not an upgrade-compatible production baseline. Devices used to install that proof should uninstall it before testing a normally versioned release. From 51c31d208e8cffe61581aefb9035737144de79e9 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 19:18:01 -0700 Subject: [PATCH 07/13] test: prove proof-release upgrade compatibility --- .github/workflows/gateway-msix.yml | 111 +++++++- CONTRIBUTING.md | 4 + README.md | 11 +- scripts/Test-MSIXUpgrade.ps1 | 237 ++++++++++++++++++ scripts/Test-WorkflowSigningConfiguration.ps1 | 6 + scripts/msix-upgrade-baselines.json | 16 ++ 6 files changed, 379 insertions(+), 6 deletions(-) create mode 100644 scripts/Test-MSIXUpgrade.ps1 create mode 100644 scripts/msix-upgrade-baselines.json diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index 47908461..bcaaecbe 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -40,6 +40,7 @@ jobs: runs-on: ubuntu-latest outputs: packaging: ${{ steps.filter.outputs.packaging }} + versioning: ${{ steps.filter.outputs.versioning }} steps: - uses: actions/checkout@v6 @@ -51,6 +52,7 @@ jobs: run: | if ($env:GITHUB_EVENT_NAME -ne 'pull_request') { 'packaging=true' >> $env:GITHUB_OUTPUT + 'versioning=false' >> $env:GITHUB_OUTPUT return } @@ -68,6 +70,25 @@ jobs: -FileListPath $fileListPath "packaging=$packaging" >> $env:GITHUB_OUTPUT + $pages = Get-Content -LiteralPath $fileListPath -Raw | + ConvertFrom-Json + $versioningPaths = @( + 'release-policy.json' + 'scripts/Get-MSIXReleaseIdentity.ps1' + 'scripts/Get-MSIXReleaseIdentity.Tests.ps1' + 'scripts/Test-MSIXUpgrade.ps1' + 'scripts/msix-upgrade-baselines.json' + ) + $versioning = 'false' + foreach ($page in @($pages)) { + foreach ($file in @($page)) { + if ([string]$file.filename -in $versioningPaths) { + $versioning = 'true' + } + } + } + "versioning=$versioning" >> $env:GITHUB_OUTPUT + test-host: name: Test Gateway MSIX host runs-on: windows-latest @@ -333,6 +354,7 @@ jobs: build-msix: name: Build unsigned ${{ matrix.architecture }} Gateway MSIX needs: + - changes - test-host - build-package runs-on: windows-latest @@ -428,12 +450,16 @@ jobs: shell: pwsh env: SIGNING_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode || 'unsigned' }} + VERSIONING_CHANGE: ${{ needs.changes.outputs.versioning }} run: | $versionParameters = @{ RunNumber = '${{ github.run_number }}' RunAttempt = '${{ github.run_attempt }}' } - if ($env:SIGNING_MODE -eq 'official') { + if ( + $env:SIGNING_MODE -eq 'official' -or + $env:VERSIONING_CHANGE -eq 'true' + ) { $policy = Get-Content -LiteralPath .\release-policy.json -Raw | ConvertFrom-Json $identity = .\scripts\Get-MSIXReleaseIdentity.ps1 ` @@ -512,7 +538,9 @@ jobs: build-msix-bundle: name: Build unsigned multi-architecture Gateway MSIX bundle - needs: build-msix + needs: + - changes + - build-msix runs-on: windows-latest steps: - name: Check out repository @@ -536,12 +564,16 @@ jobs: shell: pwsh env: SIGNING_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode || 'unsigned' }} + VERSIONING_CHANGE: ${{ needs.changes.outputs.versioning }} run: | $versionParameters = @{ RunNumber = '${{ github.run_number }}' RunAttempt = '${{ github.run_attempt }}' } - if ($env:SIGNING_MODE -eq 'official') { + if ( + $env:SIGNING_MODE -eq 'official' -or + $env:VERSIONING_CHANGE -eq 'true' + ) { $policy = Get-Content -LiteralPath .\release-policy.json -Raw | ConvertFrom-Json $identity = .\scripts\Get-MSIXReleaseIdentity.ps1 ` @@ -566,6 +598,78 @@ jobs: if-no-files-found: error retention-days: 7 + test-msix-upgrades: + name: Test proof-release MSIX upgrades + if: ${{ github.event_name == 'pull_request' && needs.changes.outputs.versioning == 'true' }} + needs: + - changes + - build-msix + runs-on: windows-latest + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Download unsigned x64 candidate + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-unsigned-x64 + path: artifacts\x64 + + - name: Apply temporary test signature + shell: pwsh + run: | + .\scripts\Sign-TestMSIX.ps1 ` + -ArtifactsDirectory artifacts ` + -OutputDirectory test-signed + + - name: Download hash-pinned proof releases + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $baselines = Get-Content ` + -LiteralPath .\scripts\msix-upgrade-baselines.json ` + -Raw | + ConvertFrom-Json + New-Item -Path baselines -ItemType Directory -Force | Out-Null + foreach ($baseline in $baselines.baselines) { + & gh release download ([string]$baseline.releaseTag) ` + --repo $env:GITHUB_REPOSITORY ` + --pattern ([string]$baseline.assetName) ` + --dir baselines + if ($LASTEXITCODE -ne 0) { + throw "Unable to download $($baseline.releaseTag) upgrade baseline." + } + } + + - name: Test installed-package upgrades and retained LocalState + shell: pwsh + run: | + $policy = Get-Content -LiteralPath .\release-policy.json -Raw | + ConvertFrom-Json + $identity = .\scripts\Get-MSIXReleaseIdentity.ps1 ` + -GatewayTag ([string]$policy.gatewayTag) ` + -MSIXRevision ([int]$policy.msixRevision) + .\scripts\Test-MSIXUpgrade.ps1 ` + -BaselinesPath .\scripts\msix-upgrade-baselines.json ` + -BaselinesDirectory baselines ` + -CandidatePackagePath test-signed\x64\OpenClawGateway-x64.msix ` + -CandidateCertificatePath test-signed\x64\OpenClawGateway-test-signing.cer ` + -ExpectedCandidateVersion $identity.PackageVersion ` + -EvidencePath evidence\msix-upgrade-evidence.json + + - name: Upload upgrade evidence + uses: actions/upload-artifact@v7 + with: + name: openclaw-gateway-msix-upgrade-evidence + path: evidence\msix-upgrade-evidence.json + if-no-files-found: error + retention-days: 90 + reject-untrusted-official-signing: name: Reject official signing outside main if: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode == 'official' && github.ref != 'refs/heads/main' }} @@ -864,6 +968,7 @@ jobs: - build-msix - test-sign-msix - build-msix-bundle + - test-msix-upgrades - reject-untrusted-official-signing - authorize-signing - sign-msix diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abe65115..46ea1ca2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,6 +190,10 @@ bypassable, and required CI checks remain authoritative. `0` for the first MSIX of a Gateway tag and increment only for packaging-only rebuilds of that exact tag. Do not assign package versions or release tags by hand. +- Treat published proof releases in `scripts/msix-upgrade-baselines.json` as + immutable transition fixtures. Keep their release asset names and SHA-256 + digests pinned. Version-policy changes must pass the installed-package + upgrade job from both baselines and retain package LocalState. - Use source-generated `System.Text.Json` metadata through `OpenClawJsonContext`. The launcher is NativeAOT and must not introduce reflection-based serialization. diff --git a/README.md b/README.md index 5256c80a..19fbdc79 100644 --- a/README.md +++ b/README.md @@ -337,9 +337,14 @@ and diagnostic copies. Microsoft Store submissions reserve the fourth version component as zero, so Store publication will need its own version policy when it is introduced. -The one-time `v0.0.0.0` signing proof predates this version policy and is not an -upgrade-compatible production baseline. Devices used to install that proof -should uninstall it before testing a normally versioned release. +The signed `v0.0.0.0` and `v0.0.0.1` proof releases are not production version +identities, but they are retained as transition baselines. Pull requests that +change release versioning download the hash-pinned x64 packages, install each +one on a Windows runner, upgrade it in place to the proposed package identity, +and verify that the package family remains stable and a LocalState marker is +retained. The resulting JSON evidence is retained as a workflow artifact for +90 days. Future versioning schemes must keep this transition gate green or +explicitly document and obtain approval for a breaking reset. An `.msixbundle` is a single installable container for the x64 and ARM64 MSIX packages; Windows selects the package appropriate for the device. An diff --git a/scripts/Test-MSIXUpgrade.ps1 b/scripts/Test-MSIXUpgrade.ps1 new file mode 100644 index 00000000..0a317813 --- /dev/null +++ b/scripts/Test-MSIXUpgrade.ps1 @@ -0,0 +1,237 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$BaselinesPath, + + [Parameter(Mandatory)] + [string]$BaselinesDirectory, + + [Parameter(Mandatory)] + [string]$CandidatePackagePath, + + [Parameter(Mandatory)] + [string]$CandidateCertificatePath, + + [Parameter(Mandatory)] + [string]$ExpectedCandidateVersion, + + [Parameter(Mandatory)] + [string]$EvidencePath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not $IsWindows) { + throw 'MSIX upgrade validation requires Windows.' +} + +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function Read-MSIXIdentity { + param([Parameter(Mandatory)][string]$Path) + + $archive = [IO.Compression.ZipFile]::OpenRead( + (Resolve-Path -LiteralPath $Path).Path + ) + try { + $entry = $archive.GetEntry('AppxManifest.xml') + if ($null -eq $entry) { + throw "MSIX '$Path' does not contain AppxManifest.xml." + } + $reader = [IO.StreamReader]::new($entry.Open()) + try { + [xml]$manifest = $reader.ReadToEnd() + } + finally { + $reader.Dispose() + } + $identity = $manifest.Package.Identity + [pscustomobject]@{ + Name = [string]$identity.Name + Publisher = [string]$identity.Publisher + Architecture = [string]$identity.ProcessorArchitecture + Version = [string]$identity.Version + } + } + finally { + $archive.Dispose() + } +} + +function Remove-TestPackage { + Get-AppxPackage -Name 'OpenClaw.Gateway' -ErrorAction SilentlyContinue | + ForEach-Object { + Remove-AppxPackage ` + -Package $_.PackageFullName ` + -ErrorAction Stop + } +} + +$resolvedBaselinesPath = (Resolve-Path -LiteralPath $BaselinesPath).Path +$resolvedBaselinesDirectory = ( + Resolve-Path -LiteralPath $BaselinesDirectory +).Path +$resolvedCandidatePath = ( + Resolve-Path -LiteralPath $CandidatePackagePath +).Path +$resolvedCertificatePath = ( + Resolve-Path -LiteralPath $CandidateCertificatePath +).Path +$candidateIdentity = Read-MSIXIdentity -Path $resolvedCandidatePath +if ( + $candidateIdentity.Name -cne 'OpenClaw.Gateway' -or + $candidateIdentity.Architecture -cne 'x64' -or + $candidateIdentity.Version -cne $ExpectedCandidateVersion +) { + throw 'The candidate MSIX identity is unexpected.' +} + +$policy = Get-Content ` + -LiteralPath (Join-Path (Split-Path $PSScriptRoot -Parent) 'release-policy.json') ` + -Raw | + ConvertFrom-Json +if ($candidateIdentity.Publisher -cne [string]$policy.publisher) { + throw 'The candidate MSIX publisher does not match release policy.' +} + +$baselineManifest = Get-Content -LiteralPath $resolvedBaselinesPath -Raw | + ConvertFrom-Json +if ($baselineManifest.baselines.Count -ne 2) { + throw 'Upgrade validation requires exactly the two published proof releases.' +} + +$certificate = Import-Certificate ` + -FilePath $resolvedCertificatePath ` + -CertStoreLocation 'Cert:\CurrentUser\TrustedPeople' +$results = [Collections.Generic.List[object]]::new() +$freshInstall = $null + +try { + foreach ($baseline in $baselineManifest.baselines) { + Remove-TestPackage + $baselinePath = Join-Path ` + $resolvedBaselinesDirectory ` + ([string]$baseline.assetName) + if (-not (Test-Path -LiteralPath $baselinePath -PathType Leaf)) { + throw "Missing upgrade baseline '$($baseline.assetName)'." + } + $actualHash = ( + Get-FileHash -LiteralPath $baselinePath -Algorithm SHA256 + ).Hash.ToLowerInvariant() + if ($actualHash -cne [string]$baseline.sha256) { + throw "Upgrade baseline '$($baseline.assetName)' failed hash validation." + } + + $baselineIdentity = Read-MSIXIdentity -Path $baselinePath + if ( + $baselineIdentity.Name -cne $candidateIdentity.Name -or + $baselineIdentity.Publisher -cne $candidateIdentity.Publisher -or + $baselineIdentity.Architecture -cne 'x64' -or + $baselineIdentity.Version -cne [string]$baseline.packageVersion + ) { + throw "Upgrade baseline '$($baseline.assetName)' has an unexpected identity." + } + if ( + [version]$candidateIdentity.Version -le + [version]$baselineIdentity.Version + ) { + throw 'The candidate MSIX must be newer than every upgrade baseline.' + } + + Add-AppxPackage -Path $baselinePath -ErrorAction Stop + $installedBaseline = Get-AppxPackage -Name $candidateIdentity.Name + if ( + $null -eq $installedBaseline -or + [string]$installedBaseline.Version -cne $baselineIdentity.Version -or + [string]$installedBaseline.Status -cne 'Ok' + ) { + throw "Windows did not install '$($baseline.assetName)' successfully." + } + + $localState = Join-Path ` + $env:LOCALAPPDATA ` + "Packages\$($installedBaseline.PackageFamilyName)\LocalState" + New-Item -Path $localState -ItemType Directory -Force | Out-Null + $markerPath = Join-Path $localState 'msix-upgrade-proof.txt' + $marker = "upgrade-from-$($baseline.packageVersion)" + Set-Content -LiteralPath $markerPath -Value $marker -Encoding utf8 + + Add-AppxPackage ` + -Path $resolvedCandidatePath ` + -ForceApplicationShutdown ` + -ErrorAction Stop + $installedCandidate = Get-AppxPackage -Name $candidateIdentity.Name + if ( + $null -eq $installedCandidate -or + [string]$installedCandidate.Version -cne $candidateIdentity.Version -or + [string]$installedCandidate.Status -cne 'Ok' + ) { + throw "Windows did not upgrade from '$($baseline.assetName)'." + } + $candidateLocalState = Join-Path ` + $env:LOCALAPPDATA ` + "Packages\$($installedCandidate.PackageFamilyName)\LocalState" + $retainedMarkerPath = Join-Path ` + $candidateLocalState ` + 'msix-upgrade-proof.txt' + if ( + $installedCandidate.PackageFamilyName -cne + $installedBaseline.PackageFamilyName -or + -not (Test-Path -LiteralPath $retainedMarkerPath -PathType Leaf) -or + (Get-Content -LiteralPath $retainedMarkerPath -Raw).Trim() -cne $marker + ) { + throw "LocalState was not retained across the $($baseline.packageVersion) upgrade." + } + + $results.Add([pscustomobject]@{ + baselineRelease = [string]$baseline.releaseTag + baselineVersion = $baselineIdentity.Version + candidateVersion = $candidateIdentity.Version + packageFamilyName = [string]$installedCandidate.PackageFamilyName + status = [string]$installedCandidate.Status + localStateRetained = $true + }) + } + + Remove-TestPackage + Add-AppxPackage -Path $resolvedCandidatePath -ErrorAction Stop + $installedFresh = Get-AppxPackage -Name $candidateIdentity.Name + if ( + $null -eq $installedFresh -or + [string]$installedFresh.Version -cne $candidateIdentity.Version -or + [string]$installedFresh.Status -cne 'Ok' + ) { + throw 'Windows did not accept a fresh candidate installation.' + } + $freshInstall = [pscustomobject]@{ + candidateVersion = $candidateIdentity.Version + packageFamilyName = [string]$installedFresh.PackageFamilyName + status = [string]$installedFresh.Status + } +} +finally { + Remove-TestPackage + if ($null -ne $certificate) { + Remove-Item ` + -LiteralPath "Cert:\CurrentUser\TrustedPeople\$($certificate.Thumbprint)" ` + -Force ` + -ErrorAction SilentlyContinue + } +} + +$evidenceDirectory = Split-Path -Parent $EvidencePath +if (-not [string]::IsNullOrWhiteSpace($evidenceDirectory)) { + New-Item -Path $evidenceDirectory -ItemType Directory -Force | Out-Null +} +[pscustomobject]@{ + testedAt = (Get-Date).ToUniversalTime().ToString('o') + runner = [Environment]::OSVersion.VersionString + candidateVersion = $candidateIdentity.Version + freshInstall = $freshInstall + transitions = $results +} | + ConvertTo-Json -Depth 4 | + Set-Content -LiteralPath $EvidencePath -Encoding utf8 + +Write-Host "MSIX upgrade compatibility passed for $($results.Count) proof releases." diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index a85e1bec..e6bd8d90 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -43,6 +43,12 @@ $requiredFragments = @( 'uses: azure/artifact-signing-action@v2' 'name: Compose unsigned multi-architecture MSIX bundle' 'name: Upload unsigned multi-architecture MSIX bundle' + 'name: Test proof-release MSIX upgrades' + "needs.changes.outputs.versioning == 'true'" + '.\scripts\msix-upgrade-baselines.json' + '.\scripts\Test-MSIXUpgrade.ps1' + 'openclaw-gateway-msix-upgrade-evidence' + 'retention-days: 90' '-BundlePath artifacts\bundle\OpenClawGateway.msixbundle' 'files-folder-recurse: true' 'files: ${{ github.workspace }}\artifacts\bundle\OpenClawGateway.msixbundle' diff --git a/scripts/msix-upgrade-baselines.json b/scripts/msix-upgrade-baselines.json new file mode 100644 index 00000000..ca1c903b --- /dev/null +++ b/scripts/msix-upgrade-baselines.json @@ -0,0 +1,16 @@ +{ + "baselines": [ + { + "releaseTag": "v0.0.0.0", + "assetName": "OpenClawGateway-0.0.0.0-x64.msix", + "packageVersion": "0.0.0.0", + "sha256": "3f288e267de01f8f4e25897a6c5bb3db9faaa09253360fb83e2247dcfffaf76b" + }, + { + "releaseTag": "v0.0.0.1", + "assetName": "OpenClawGateway-0.0.0.1-x64.msix", + "packageVersion": "0.0.0.1", + "sha256": "098eae798413f3b831126d9806c0cb47695280cc9b2b4d52696bfde075aa1747" + } + ] +} From 9bd9eb706f1ecac42c0a938c35314493d0ca017e Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 19:29:45 -0700 Subject: [PATCH 08/13] test(ci): prove proof-release bundle upgrades --- .github/workflows/gateway-msix.yml | 10 +- CONTRIBUTING.md | 5 +- README.md | 12 +- scripts/Sign-TestMSIX.ps1 | 54 +++++- scripts/Test-MSIXUpgrade.ps1 | 171 +++++++++++++++--- scripts/Test-Sign-TestMSIX.Tests.ps1 | 4 +- scripts/Test-WorkflowSigningConfiguration.ps1 | 3 + scripts/msix-upgrade-baselines.json | 16 ++ 8 files changed, 236 insertions(+), 39 deletions(-) diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index bcaaecbe..35971319 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -75,7 +75,7 @@ jobs: $versioningPaths = @( 'release-policy.json' 'scripts/Get-MSIXReleaseIdentity.ps1' - 'scripts/Get-MSIXReleaseIdentity.Tests.ps1' + 'scripts/Test-MSIXReleaseIdentity.Tests.ps1' 'scripts/Test-MSIXUpgrade.ps1' 'scripts/msix-upgrade-baselines.json' ) @@ -604,6 +604,7 @@ jobs: needs: - changes - build-msix + - build-msix-bundle runs-on: windows-latest permissions: contents: read @@ -619,6 +620,12 @@ jobs: name: openclaw-gateway-msix-unsigned-x64 path: artifacts\x64 + - name: Download unsigned bundle candidate + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-unsigned-bundle + path: artifacts\bundle + - name: Apply temporary test signature shell: pwsh run: | @@ -658,6 +665,7 @@ jobs: -BaselinesPath .\scripts\msix-upgrade-baselines.json ` -BaselinesDirectory baselines ` -CandidatePackagePath test-signed\x64\OpenClawGateway-x64.msix ` + -CandidateBundlePath test-signed\bundle\OpenClawGateway.msixbundle ` -CandidateCertificatePath test-signed\x64\OpenClawGateway-test-signing.cer ` -ExpectedCandidateVersion $identity.PackageVersion ` -EvidencePath evidence\msix-upgrade-evidence.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46ea1ca2..e28271a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,7 +193,10 @@ bypassable, and required CI checks remain authoritative. - Treat published proof releases in `scripts/msix-upgrade-baselines.json` as immutable transition fixtures. Keep their release asset names and SHA-256 digests pinned. Version-policy changes must pass the installed-package - upgrade job from both baselines and retain package LocalState. + upgrade job from every standalone and bundle baseline, retain package + LocalState, and prove both candidate delivery formats install fresh. Run the + harness only on an isolated clean Windows account; it refuses pre-existing + OpenClaw Gateway registrations and cleans up only its own installation. - Use source-generated `System.Text.Json` metadata through `OpenClawJsonContext`. The launcher is NativeAOT and must not introduce reflection-based serialization. diff --git a/README.md b/README.md index 19fbdc79..09e7660a 100644 --- a/README.md +++ b/README.md @@ -339,10 +339,14 @@ Store publication will need its own version policy when it is introduced. The signed `v0.0.0.0` and `v0.0.0.1` proof releases are not production version identities, but they are retained as transition baselines. Pull requests that -change release versioning download the hash-pinned x64 packages, install each -one on a Windows runner, upgrade it in place to the proposed package identity, -and verify that the package family remains stable and a LocalState marker is -retained. The resulting JSON evidence is retained as a workflow artifact for +change release versioning download the hash-pinned standalone x64 and +recommended `.msixbundle` assets, install each one on a clean GitHub-hosted +Windows runner, upgrade it in place through the same delivery format, and +verify that the package family remains stable and a LocalState marker is +retained. The gate also proves fresh installation of both the standalone and +bundle candidates. It refuses to run when an OpenClaw Gateway package is +already registered and removes only packages installed by that test +invocation. The resulting JSON evidence is retained as a workflow artifact for 90 days. Future versioning schemes must keep this transition gate green or explicitly document and obtain approval for a breaking reset. diff --git a/scripts/Sign-TestMSIX.ps1 b/scripts/Sign-TestMSIX.ps1 index fa1d19f9..5f3ac672 100644 --- a/scripts/Sign-TestMSIX.ps1 +++ b/scripts/Sign-TestMSIX.ps1 @@ -39,11 +39,13 @@ $architectureDirectories = @( } } ) -if ($architectureDirectories.Count -eq 0) { +$bundleDirectory = Join-Path $resolvedArtifactsDirectory 'bundle' +$hasBundleDirectory = Test-Path -LiteralPath $bundleDirectory -PathType Container +if ($architectureDirectories.Count -eq 0 -and -not $hasBundleDirectory) { throw ( - "No architecture directories were found under " + + "No signable package directories were found under " + "'$resolvedArtifactsDirectory'. Expected at least one of: " + - ($architectures -join ', ') + '.' + ($architectures -join ', ') + ', bundle.' ) } @@ -176,6 +178,52 @@ try { ) ` -Encoding utf8 } + + if ($hasBundleDirectory) { + $sourceBundles = @( + Get-ChildItem ` + -LiteralPath $bundleDirectory ` + -Filter '*.msixbundle' ` + -File + ) + if ($sourceBundles.Count -ne 1) { + throw ( + "Expected one unsigned MSIX bundle in '$bundleDirectory'; " + + "found $($sourceBundles.Count)." + ) + } + + $destinationDirectory = Join-Path $OutputDirectory 'bundle' + New-Item ` + -Path $destinationDirectory ` + -ItemType Directory ` + -Force | + Out-Null + $signedBundlePath = Join-Path ` + $destinationDirectory ` + $sourceBundles[0].Name + Copy-Item ` + -LiteralPath $sourceBundles[0].FullName ` + -Destination $signedBundlePath ` + -Force + + & $signtool.FullName sign ` + /fd SHA256 ` + /f $temporaryPfx ` + /p $passwordText ` + $signedBundlePath + if ($LASTEXITCODE -ne 0) { + throw "Test signing failed for the bundle with exit code $LASTEXITCODE." + } + + $signature = Get-AuthenticodeSignature -LiteralPath $signedBundlePath + if ( + $null -eq $signature.SignerCertificate -or + $signature.SignerCertificate.Thumbprint -ne $certificate.Thumbprint + ) { + throw 'The bundle test signature was not applied.' + } + } } finally { Remove-Item -LiteralPath $temporaryPfx -Force -ErrorAction SilentlyContinue diff --git a/scripts/Test-MSIXUpgrade.ps1 b/scripts/Test-MSIXUpgrade.ps1 index 0a317813..8d451224 100644 --- a/scripts/Test-MSIXUpgrade.ps1 +++ b/scripts/Test-MSIXUpgrade.ps1 @@ -9,6 +9,9 @@ param( [Parameter(Mandatory)] [string]$CandidatePackagePath, + [Parameter(Mandatory)] + [string]$CandidateBundlePath, + [Parameter(Mandatory)] [string]$CandidateCertificatePath, @@ -35,9 +38,16 @@ function Read-MSIXIdentity { (Resolve-Path -LiteralPath $Path).Path ) try { - $entry = $archive.GetEntry('AppxManifest.xml') + $isBundle = [IO.Path]::GetExtension($Path) -ieq '.msixbundle' + $manifestPath = if ($isBundle) { + 'AppxMetadata/AppxBundleManifest.xml' + } + else { + 'AppxManifest.xml' + } + $entry = $archive.GetEntry($manifestPath) if ($null -eq $entry) { - throw "MSIX '$Path' does not contain AppxManifest.xml." + throw "Package '$Path' does not contain $manifestPath." } $reader = [IO.StreamReader]::new($entry.Open()) try { @@ -46,12 +56,32 @@ function Read-MSIXIdentity { finally { $reader.Dispose() } + if ($isBundle) { + $identity = $manifest.Bundle.Identity + $x64Package = @($manifest.Bundle.Packages.Package) | + Where-Object { [string]$_.Architecture -ceq 'x64' } | + Select-Object -First 1 + if ($null -eq $x64Package) { + throw "MSIX bundle '$Path' does not contain an x64 package." + } + return [pscustomobject]@{ + Name = [string]$identity.Name + Publisher = [string]$identity.Publisher + Architecture = 'x64' + Version = [string]$x64Package.Version + BundleVersion = [string]$identity.Version + DeliveryType = 'bundle' + } + } + $identity = $manifest.Package.Identity [pscustomobject]@{ Name = [string]$identity.Name Publisher = [string]$identity.Publisher Architecture = [string]$identity.ProcessorArchitecture Version = [string]$identity.Version + BundleVersion = $null + DeliveryType = 'standalone' } } finally { @@ -59,13 +89,55 @@ function Read-MSIXIdentity { } } +function Get-GatewayPackages { + @(Get-AppxPackage -Name 'OpenClaw.Gateway' -ErrorAction SilentlyContinue) +} + +$testOwnsPackage = $false +$testPackageFamilyName = $null + function Remove-TestPackage { - Get-AppxPackage -Name 'OpenClaw.Gateway' -ErrorAction SilentlyContinue | - ForEach-Object { - Remove-AppxPackage ` - -Package $_.PackageFullName ` - -ErrorAction Stop + if (-not $script:testOwnsPackage) { + return + } + + $packages = Get-GatewayPackages + if ($packages.Count -gt 1) { + throw 'More than one OpenClaw.Gateway registration exists during cleanup.' + } + if ($packages.Count -eq 1) { + if ( + $null -ne $script:testPackageFamilyName -and + [string]$packages[0].PackageFamilyName -cne + $script:testPackageFamilyName + ) { + throw 'Refusing to remove an OpenClaw package not owned by this test.' } + Remove-AppxPackage ` + -Package $packages[0].PackageFullName ` + -ErrorAction Stop + } + + $script:testOwnsPackage = $false + $script:testPackageFamilyName = $null +} + +function Install-TestPackage { + param([Parameter(Mandatory)][string]$Path) + + if ((Get-GatewayPackages).Count -ne 0) { + throw 'Refusing to install over an OpenClaw package not owned by this test.' + } + # The clean-machine guard above establishes ownership before installation, + # allowing finally cleanup even if installation only partially succeeds. + $script:testOwnsPackage = $true + Add-AppxPackage -Path $Path -ErrorAction Stop + $packages = Get-GatewayPackages + if ($packages.Count -ne 1) { + throw 'Windows did not create exactly one OpenClaw.Gateway registration.' + } + $script:testPackageFamilyName = [string]$packages[0].PackageFamilyName + $packages[0] } $resolvedBaselinesPath = (Resolve-Path -LiteralPath $BaselinesPath).Path @@ -75,10 +147,14 @@ $resolvedBaselinesDirectory = ( $resolvedCandidatePath = ( Resolve-Path -LiteralPath $CandidatePackagePath ).Path +$resolvedCandidateBundlePath = ( + Resolve-Path -LiteralPath $CandidateBundlePath +).Path $resolvedCertificatePath = ( Resolve-Path -LiteralPath $CandidateCertificatePath ).Path $candidateIdentity = Read-MSIXIdentity -Path $resolvedCandidatePath +$candidateBundleIdentity = Read-MSIXIdentity -Path $resolvedCandidateBundlePath if ( $candidateIdentity.Name -cne 'OpenClaw.Gateway' -or $candidateIdentity.Architecture -cne 'x64' -or @@ -86,6 +162,15 @@ if ( ) { throw 'The candidate MSIX identity is unexpected.' } +if ( + $candidateBundleIdentity.Name -cne $candidateIdentity.Name -or + $candidateBundleIdentity.Publisher -cne $candidateIdentity.Publisher -or + $candidateBundleIdentity.Architecture -cne 'x64' -or + $candidateBundleIdentity.Version -cne $ExpectedCandidateVersion -or + $candidateBundleIdentity.DeliveryType -cne 'bundle' +) { + throw 'The candidate MSIX bundle identity is unexpected.' +} $policy = Get-Content ` -LiteralPath (Join-Path (Split-Path $PSScriptRoot -Parent) 'release-policy.json') ` @@ -97,8 +182,14 @@ if ($candidateIdentity.Publisher -cne [string]$policy.publisher) { $baselineManifest = Get-Content -LiteralPath $resolvedBaselinesPath -Raw | ConvertFrom-Json -if ($baselineManifest.baselines.Count -ne 2) { - throw 'Upgrade validation requires exactly the two published proof releases.' +if ($baselineManifest.baselines.Count -ne 4) { + throw 'Upgrade validation requires standalone and bundle proof-release baselines.' +} +if ((Get-GatewayPackages).Count -ne 0) { + throw ( + 'Refusing to run MSIX upgrade validation while OpenClaw.Gateway is ' + + 'already installed. Use an isolated clean test account.' + ) } $certificate = Import-Certificate ` @@ -123,12 +214,17 @@ try { throw "Upgrade baseline '$($baseline.assetName)' failed hash validation." } + $deliveryType = [string]$baseline.deliveryType + if ($deliveryType -notin @('standalone', 'bundle')) { + throw "Unknown delivery type '$deliveryType'." + } $baselineIdentity = Read-MSIXIdentity -Path $baselinePath if ( $baselineIdentity.Name -cne $candidateIdentity.Name -or $baselineIdentity.Publisher -cne $candidateIdentity.Publisher -or $baselineIdentity.Architecture -cne 'x64' -or - $baselineIdentity.Version -cne [string]$baseline.packageVersion + $baselineIdentity.Version -cne [string]$baseline.packageVersion -or + $baselineIdentity.DeliveryType -cne $deliveryType ) { throw "Upgrade baseline '$($baseline.assetName)' has an unexpected identity." } @@ -139,8 +235,7 @@ try { throw 'The candidate MSIX must be newer than every upgrade baseline.' } - Add-AppxPackage -Path $baselinePath -ErrorAction Stop - $installedBaseline = Get-AppxPackage -Name $candidateIdentity.Name + $installedBaseline = Install-TestPackage -Path $baselinePath if ( $null -eq $installedBaseline -or [string]$installedBaseline.Version -cne $baselineIdentity.Version -or @@ -157,8 +252,14 @@ try { $marker = "upgrade-from-$($baseline.packageVersion)" Set-Content -LiteralPath $markerPath -Value $marker -Encoding utf8 + $candidatePath = if ($deliveryType -ceq 'bundle') { + $resolvedCandidateBundlePath + } + else { + $resolvedCandidatePath + } Add-AppxPackage ` - -Path $resolvedCandidatePath ` + -Path $candidatePath ` -ForceApplicationShutdown ` -ErrorAction Stop $installedCandidate = Get-AppxPackage -Name $candidateIdentity.Name @@ -186,6 +287,7 @@ try { $results.Add([pscustomobject]@{ baselineRelease = [string]$baseline.releaseTag + deliveryType = $deliveryType baselineVersion = $baselineIdentity.Version candidateVersion = $candidateIdentity.Version packageFamilyName = [string]$installedCandidate.PackageFamilyName @@ -194,21 +296,34 @@ try { }) } - Remove-TestPackage - Add-AppxPackage -Path $resolvedCandidatePath -ErrorAction Stop - $installedFresh = Get-AppxPackage -Name $candidateIdentity.Name - if ( - $null -eq $installedFresh -or - [string]$installedFresh.Version -cne $candidateIdentity.Version -or - [string]$installedFresh.Status -cne 'Ok' - ) { - throw 'Windows did not accept a fresh candidate installation.' - } - $freshInstall = [pscustomobject]@{ - candidateVersion = $candidateIdentity.Version - packageFamilyName = [string]$installedFresh.PackageFamilyName - status = [string]$installedFresh.Status + $freshInstalls = [Collections.Generic.List[object]]::new() + foreach ($candidate in @( + [pscustomobject]@{ + deliveryType = 'standalone' + path = $resolvedCandidatePath + }, + [pscustomobject]@{ + deliveryType = 'bundle' + path = $resolvedCandidateBundlePath + } + )) { + Remove-TestPackage + $installedFresh = Install-TestPackage -Path $candidate.path + if ( + $null -eq $installedFresh -or + [string]$installedFresh.Version -cne $candidateIdentity.Version -or + [string]$installedFresh.Status -cne 'Ok' + ) { + throw "Windows did not accept a fresh $($candidate.deliveryType) installation." + } + $freshInstalls.Add([pscustomobject]@{ + deliveryType = $candidate.deliveryType + candidateVersion = $candidateIdentity.Version + packageFamilyName = [string]$installedFresh.PackageFamilyName + status = [string]$installedFresh.Status + }) } + $freshInstall = $freshInstalls } finally { Remove-TestPackage @@ -234,4 +349,4 @@ if (-not [string]::IsNullOrWhiteSpace($evidenceDirectory)) { ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $EvidencePath -Encoding utf8 -Write-Host "MSIX upgrade compatibility passed for $($results.Count) proof releases." +Write-Host "MSIX upgrade compatibility passed for $($results.Count) proof-release paths." diff --git a/scripts/Test-Sign-TestMSIX.Tests.ps1 b/scripts/Test-Sign-TestMSIX.Tests.ps1 index 88338e1c..e305019c 100644 --- a/scripts/Test-Sign-TestMSIX.Tests.ps1 +++ b/scripts/Test-Sign-TestMSIX.Tests.ps1 @@ -35,8 +35,8 @@ try { if ($exitCode -eq 0) { throw 'Signing unexpectedly succeeded without an architecture directory.' } - if ($message -notmatch 'No architecture directories were found') { - throw "Signing failure did not identify the missing architecture directories. Output: $message" + if ($message -notmatch 'No signable package directories were found') { + throw "Signing failure did not identify the missing package directories. Output: $message" } if (Test-Path -LiteralPath $outputDirectory) { throw 'Signing created an output directory despite finding no architecture directory.' diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index e6bd8d90..aae8fced 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -45,8 +45,11 @@ $requiredFragments = @( 'name: Upload unsigned multi-architecture MSIX bundle' 'name: Test proof-release MSIX upgrades' "needs.changes.outputs.versioning == 'true'" + 'scripts/Test-MSIXReleaseIdentity.Tests.ps1' '.\scripts\msix-upgrade-baselines.json' '.\scripts\Test-MSIXUpgrade.ps1' + 'name: Download unsigned bundle candidate' + '-CandidateBundlePath test-signed\bundle\OpenClawGateway.msixbundle' 'openclaw-gateway-msix-upgrade-evidence' 'retention-days: 90' '-BundlePath artifacts\bundle\OpenClawGateway.msixbundle' diff --git a/scripts/msix-upgrade-baselines.json b/scripts/msix-upgrade-baselines.json index ca1c903b..2b50caa9 100644 --- a/scripts/msix-upgrade-baselines.json +++ b/scripts/msix-upgrade-baselines.json @@ -2,15 +2,31 @@ "baselines": [ { "releaseTag": "v0.0.0.0", + "deliveryType": "standalone", "assetName": "OpenClawGateway-0.0.0.0-x64.msix", "packageVersion": "0.0.0.0", "sha256": "3f288e267de01f8f4e25897a6c5bb3db9faaa09253360fb83e2247dcfffaf76b" }, { "releaseTag": "v0.0.0.1", + "deliveryType": "standalone", "assetName": "OpenClawGateway-0.0.0.1-x64.msix", "packageVersion": "0.0.0.1", "sha256": "098eae798413f3b831126d9806c0cb47695280cc9b2b4d52696bfde075aa1747" + }, + { + "releaseTag": "v0.0.0.0", + "deliveryType": "bundle", + "assetName": "OpenClawGateway-0.0.0.0.msixbundle", + "packageVersion": "0.0.0.0", + "sha256": "6dba82cc6cc0bf475463368be9d3899dff31addb840663d66587ad1267f3bb6e" + }, + { + "releaseTag": "v0.0.0.1", + "deliveryType": "bundle", + "assetName": "OpenClawGateway-0.0.0.1.msixbundle", + "packageVersion": "0.0.0.1", + "sha256": "48a65dd7bdc515acac96f73129aed9a343604bbfb3ceed8be4e5bd5367fd85c4" } ] } From adefcce39361f6e01e120c579f39ca0d76594bb4 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 19:54:58 -0700 Subject: [PATCH 09/13] fix(ci): normalize empty package queries --- scripts/Test-MSIXUpgrade.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/Test-MSIXUpgrade.ps1 b/scripts/Test-MSIXUpgrade.ps1 index 8d451224..5919a035 100644 --- a/scripts/Test-MSIXUpgrade.ps1 +++ b/scripts/Test-MSIXUpgrade.ps1 @@ -101,7 +101,7 @@ function Remove-TestPackage { return } - $packages = Get-GatewayPackages + $packages = @(Get-GatewayPackages) if ($packages.Count -gt 1) { throw 'More than one OpenClaw.Gateway registration exists during cleanup.' } @@ -125,14 +125,14 @@ function Remove-TestPackage { function Install-TestPackage { param([Parameter(Mandatory)][string]$Path) - if ((Get-GatewayPackages).Count -ne 0) { + if (@(Get-GatewayPackages).Count -ne 0) { throw 'Refusing to install over an OpenClaw package not owned by this test.' } # The clean-machine guard above establishes ownership before installation, # allowing finally cleanup even if installation only partially succeeds. $script:testOwnsPackage = $true Add-AppxPackage -Path $Path -ErrorAction Stop - $packages = Get-GatewayPackages + $packages = @(Get-GatewayPackages) if ($packages.Count -ne 1) { throw 'Windows did not create exactly one OpenClaw.Gateway registration.' } @@ -185,7 +185,7 @@ $baselineManifest = Get-Content -LiteralPath $resolvedBaselinesPath -Raw | if ($baselineManifest.baselines.Count -ne 4) { throw 'Upgrade validation requires standalone and bundle proof-release baselines.' } -if ((Get-GatewayPackages).Count -ne 0) { +if (@(Get-GatewayPackages).Count -ne 0) { throw ( 'Refusing to run MSIX upgrade validation while OpenClaw.Gateway is ' + 'already installed. Use an isolated clean test account.' From b4a4b86990d79582133cf0df546a597adde5056a Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 20:48:58 -0700 Subject: [PATCH 10/13] fix(ci): trust test signing cert machine-wide --- README.md | 9 ++++++--- scripts/Test-MSIXUpgrade.ps1 | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 09e7660a..fc690f30 100644 --- a/README.md +++ b/README.md @@ -346,9 +346,12 @@ verify that the package family remains stable and a LocalState marker is retained. The gate also proves fresh installation of both the standalone and bundle candidates. It refuses to run when an OpenClaw Gateway package is already registered and removes only packages installed by that test -invocation. The resulting JSON evidence is retained as a workflow artifact for -90 days. Future versioning schemes must keep this transition gate green or -explicitly document and obtain approval for a breaking reset. +invocation. It temporarily trusts the ephemeral test-signing certificate in +the local-machine Trusted People store, as required by Windows deployment, and +removes that certificate in `finally`. The resulting JSON evidence is retained +as a workflow artifact for 90 days. Future versioning schemes must keep this +transition gate green or explicitly document and obtain approval for a +breaking reset. An `.msixbundle` is a single installable container for the x64 and ARM64 MSIX packages; Windows selects the package appropriate for the device. An diff --git a/scripts/Test-MSIXUpgrade.ps1 b/scripts/Test-MSIXUpgrade.ps1 index 5919a035..077f595c 100644 --- a/scripts/Test-MSIXUpgrade.ps1 +++ b/scripts/Test-MSIXUpgrade.ps1 @@ -194,7 +194,7 @@ if (@(Get-GatewayPackages).Count -ne 0) { $certificate = Import-Certificate ` -FilePath $resolvedCertificatePath ` - -CertStoreLocation 'Cert:\CurrentUser\TrustedPeople' + -CertStoreLocation 'Cert:\LocalMachine\TrustedPeople' $results = [Collections.Generic.List[object]]::new() $freshInstall = $null @@ -329,7 +329,7 @@ finally { Remove-TestPackage if ($null -ne $certificate) { Remove-Item ` - -LiteralPath "Cert:\CurrentUser\TrustedPeople\$($certificate.Thumbprint)" ` + -LiteralPath "Cert:\LocalMachine\TrustedPeople\$($certificate.Thumbprint)" ` -Force ` -ErrorAction SilentlyContinue } From ada6a07c718a9b26ecb0f7f81ae09fa8c51d1c8e Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 22:22:48 -0700 Subject: [PATCH 11/13] chore(release): advance Gateway baseline to 2026.9.4 --- .github/workflows/gateway-msix.yml | 4 ++-- README.md | 4 ++-- release-policy.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index 35971319..63f9cb20 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -10,7 +10,7 @@ on: openclaw_ref: description: openclaw/openclaw tag, branch, or commit to package required: true - default: 0965053fe6b9341776df147a6934b7485c60b5ca + default: 3a9d69db306cd7f081e06254cb89c4bcc14a7107 type: string signing_mode: description: Package signing mode @@ -31,7 +31,7 @@ permissions: pull-requests: read env: - OPENCLAW_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.openclaw_ref || '0965053fe6b9341776df147a6934b7485c60b5ca' }} + OPENCLAW_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.openclaw_ref || '3a9d69db306cd7f081e06254cb89c4bcc14a7107' }} PACKAGING_ROOT: . jobs: diff --git a/README.md b/README.md index fc690f30..d73701b2 100644 --- a/README.md +++ b/README.md @@ -200,8 +200,8 @@ they do not represent the default-disabled state of a normal install. Full selected-theme cohesion requires the generic plugin-frame theme forwarding merged by [`openclaw/openclaw#145409`](https://github.com/openclaw/openclaw/pull/145409). -The current workflow remains on the release-approved OpenClaw baseline -`0965053fe6b9341776df147a6934b7485c60b5ca` while this plugin is disabled by +The current workflow remains on the release-approved OpenClaw `v2026.9.4` +baseline (`3a9d69db306cd7f081e06254cb89c4bcc14a7107`) while this plugin is disabled by default. That baseline packages and inspects the plugin safely but does not forward selected Control UI themes into plugin frames. The future launcher enablement change must also advance and qualify the runtime to the merged theme diff --git a/release-policy.json b/release-policy.json index 9b7a30ca..12ad1d78 100644 --- a/release-policy.json +++ b/release-policy.json @@ -1,8 +1,8 @@ { "repository": "https://github.com/openclaw/openclaw", - "gatewayTag": "v2026.8.2", + "gatewayTag": "v2026.9.4", "msixRevision": 0, - "payloadPackageVersion": "2026.8.2", - "approvedCommit": "0965053fe6b9341776df147a6934b7485c60b5ca", + "payloadPackageVersion": "2026.9.4", + "approvedCommit": "3a9d69db306cd7f081e06254cb89c4bcc14a7107", "publisher": "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US" } From 56de7401eea154e6af9002ab99f3c48b888e0e0f Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 22:26:39 -0700 Subject: [PATCH 12/13] test(signing): keep payload mismatch fixture invalid --- scripts/Test-SigningInputs.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Test-SigningInputs.Tests.ps1 b/scripts/Test-SigningInputs.Tests.ps1 index d840955e..b6bd73b2 100644 --- a/scripts/Test-SigningInputs.Tests.ps1 +++ b/scripts/Test-SigningInputs.Tests.ps1 @@ -421,7 +421,7 @@ try { New-TestArtifact ` -Root $testRoot ` -Architecture x64 ` - -PayloadPackageVersion '2026.9.4' + -PayloadPackageVersion '2026.9.3' New-TestArtifact -Root $testRoot -Architecture arm64 Assert-Fails ` -MessagePattern 'metadata is not eligible' ` From b0bbae78871c730a13b98a9c53ace3f5b3aa4f6b Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 16 Sep 2026 22:45:37 -0700 Subject: [PATCH 13/13] fix(payload): allow disabled plugin migration entries --- scripts/Build-Payload.ps1 | 19 ++++++++++++++----- scripts/Test-GatewayIsolationPlugin.Tests.ps1 | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/Build-Payload.ps1 b/scripts/Build-Payload.ps1 index 7d8166d4..1e768ebb 100644 --- a/scripts/Build-Payload.ps1 +++ b/scripts/Build-Payload.ps1 @@ -247,13 +247,22 @@ try { -LiteralPath $validationConfigPath ` -Raw | ConvertFrom-Json - $validationEntryNames = @( - $validationConfig.plugins.entries.PSObject.Properties.Name + $explicitlyEnabledEntryNames = @( + foreach ( + $entry in + $validationConfig.plugins.entries.PSObject.Properties + ) { + if ( + $entry.Value.PSObject.Properties.Name -contains 'enabled' -and + $entry.Value.enabled -eq $true + ) { + $entry.Name + } + } ) if ( - $validationEntryNames.Count -ne 1 -or - $validationEntryNames[0] -ne 'gateway-isolation' -or - $validationConfig.plugins.entries.'gateway-isolation'.enabled -ne $true + $explicitlyEnabledEntryNames.Count -ne 1 -or + $explicitlyEnabledEntryNames[0] -ne 'gateway-isolation' ) { throw ( 'The isolated validation configuration must explicitly enable ' + diff --git a/scripts/Test-GatewayIsolationPlugin.Tests.ps1 b/scripts/Test-GatewayIsolationPlugin.Tests.ps1 index fe985925..31690c6b 100644 --- a/scripts/Test-GatewayIsolationPlugin.Tests.ps1 +++ b/scripts/Test-GatewayIsolationPlugin.Tests.ps1 @@ -89,6 +89,20 @@ if (args[0] === "plugins" && args[1] === "enable") { entries: { "gateway-isolation": { enabled: true + }, + anthropic: { + config: { + sessionCatalog: { + enabled: false + } + } + }, + codex: { + config: { + sessionCatalog: { + enabled: false + } + } } } }