From 8b58e12a885bd680ed2d53f9f3810895ba927211 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 15 Oct 2026 19:40:14 -0700 Subject: [PATCH 1/2] fix: validate Gateway upgrades during release dispatch --- .github/workflows/gateway-msix.yml | 45 ++++- README.md | 16 +- docs/release-process.md | 46 +++-- scripts/Get-WorkflowSource.ps1 | 2 +- scripts/Test-OpenClawSource.Tests.ps1 | 2 +- scripts/Test-WorkflowSigningConfiguration.ps1 | 181 +++++++++++++++++- 6 files changed, 258 insertions(+), 34 deletions(-) diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index cedae83d..9d7f263e 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -47,7 +47,7 @@ env: jobs: changes: - name: Classify pull request changes + name: Select Gateway MSIX validation runs-on: ubuntu-latest outputs: packaging: ${{ steps.filter.outputs.packaging }} @@ -63,12 +63,22 @@ jobs: shell: pwsh env: GH_TOKEN: ${{ github.token }} + SIGNING_MODE: ${{ inputs.signing_mode || 'unsigned' }} run: | + $upgradeMatrix = .\scripts\Get-MSIXUpgradeMatrix.ps1 + "upgrade_matrix=$upgradeMatrix" >> $env:GITHUB_OUTPUT + if ($env:GITHUB_EVENT_NAME -ne 'pull_request') { 'packaging=true' >> $env:GITHUB_OUTPUT 'payload_artifact=true' >> $env:GITHUB_OUTPUT 'bundle_build=true' >> $env:GITHUB_OUTPUT - 'versioning=false' >> $env:GITHUB_OUTPUT + # Test dispatches prove the release candidate without publication. + # Push and unsigned dispatches retain development package versions. + $versioning = ( + $env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and + $env:SIGNING_MODE -in @('test', 'store', 'official') + ) + "versioning=$($versioning.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT return } @@ -104,9 +114,6 @@ jobs: } "bundle_build=$bundleBuild" >> $env:GITHUB_OUTPUT - $upgradeMatrix = .\scripts\Get-MSIXUpgradeMatrix.ps1 - "upgrade_matrix=$upgradeMatrix" >> $env:GITHUB_OUTPUT - # Advisory only. This job annotates documentation-reference findings but is # deliberately absent from ci-gate's needs, so it can never block a merge. # Run .\scripts\Test-DocReferences.ps1 without -Advisory locally for a @@ -827,7 +834,7 @@ jobs: test-msix-upgrades: name: Test MSIX upgrade (${{ matrix.name }}) - if: ${{ github.event_name == 'pull_request' && needs.changes.outputs.versioning == 'true' }} + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && needs.changes.outputs.versioning == 'true' }} needs: - changes - test-host @@ -947,12 +954,13 @@ jobs: authorize-signing: name: Authorize Gateway MSIX release - if: ${{ github.event_name == 'workflow_dispatch' && (inputs.signing_mode == 'official' || inputs.signing_mode == 'store') && github.ref == 'refs/heads/main' }} + if: ${{ github.event_name == 'workflow_dispatch' && (inputs.signing_mode == 'official' || inputs.signing_mode == 'store') && github.ref == 'refs/heads/main' && needs.test-msix-upgrades.result == 'success' }} needs: - test-host - build-package - build-msix - build-msix-bundle + - test-msix-upgrades runs-on: windows-latest outputs: package_version: ${{ steps.release.outputs.package_version }} @@ -1340,5 +1348,24 @@ jobs: exit 1 - name: Confirm workflow success - shell: bash - run: echo "All required workflow jobs succeeded or were intentionally skipped." + shell: pwsh + env: + WORKFLOW_EVENT: ${{ github.event_name }} + SIGNING_MODE: ${{ inputs.signing_mode || 'unsigned' }} + VERSIONING_CHANGE: ${{ needs.changes.outputs.versioning }} + UPGRADE_RESULT: ${{ needs.test-msix-upgrades.result }} + run: | + $upgradeRequired = ( + ( + $env:WORKFLOW_EVENT -eq 'workflow_dispatch' -and + $env:SIGNING_MODE -in @('test', 'store', 'official') + ) -or + ( + $env:WORKFLOW_EVENT -eq 'pull_request' -and + $env:VERSIONING_CHANGE -eq 'true' + ) + ) + if ($upgradeRequired -and $env:UPGRADE_RESULT -ne 'success') { + throw 'Required MSIX upgrade validation did not succeed; release authorization and publication must remain blocked.' + } + Write-Host 'All required workflow jobs succeeded or were intentionally skipped.' diff --git a/README.md b/README.md index fb025b86..89f326cc 100644 --- a/README.md +++ b/README.md @@ -669,9 +669,10 @@ validation. Manual runs support four modes: - `unsigned` follows stable or an `openclaw_ref` override and publishes unsigned MSIX packages; -- `test` uses the same source-selection rules and publishes MSIX packages signed with a - temporary self-signed certificate plus the public `.cer` needed for local - installation; +- `test` uses the same source-selection rules, derives the release-candidate + version, and runs the installed-upgrade matrix without release authorization + or Azure credentials. It publishes MSIX packages signed with a temporary + self-signed certificate plus the public `.cer` needed for local installation; - `store` follows stable, an `openclaw_ref` override, or an older payload pinned with `gateway_version`, may run only from `main`, and retains an unsigned Partner Center submission bundle for 90 days; @@ -682,6 +683,10 @@ validation. Manual runs support four modes: Following stable does not automatically sign or publish a new Gateway release; publication still requires a manual workflow dispatch. +Manual `store` and `official` runs require their own successful installed-upgrade +matrix before authorization. A new upstream stable release needs no throwaway +pull request to obtain current-source proof; use `test` mode for an optional +preflight. `unsigned` dispatches keep development versions and skip that matrix. Official signing uses the protected `release-signing` environment, Azure OIDC, and the existing OpenClaw Artifact Signing account and certificate profile. @@ -749,8 +754,9 @@ The signed `v0.0.0.0` and `v0.0.0.1` proof releases and the latest signed sideload release retain the `OpenClaw.Gateway` identity and remain immutable upgrade baselines. Partner Center requires the reserved `OpenClawFoundation.OpenClawGateway` identity, so Windows cannot update those -packages in place or retain their packaged LocalState. Pull requests that -change release policy download the hash-pinned standalone x64 and recommended +packages in place or retain their packaged LocalState. Versioning/source-selection +pull requests and manual `test`, `store`, and `official` runs download the +hash-pinned standalone x64 and recommended `.msixbundle` assets, install each one on a clean GitHub-hosted Windows runner, verify the legacy identity, remove it, install the Store candidate through the same delivery format, and prove that Windows registered the reserved package diff --git a/docs/release-process.md b/docs/release-process.md index e199facd..932bda77 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -44,11 +44,13 @@ payload rollback. The payload uses that older version, while package and release identity continue from the unmodified policy pin or npm latest so the MSIX remains an upgrade. Both verified sources are captured and replayed with the immutable snapshot; the input does not change release policy and must be older than the -release-identity source. In Store or official mode, an accompanying full-SHA +release-identity source. In test, Store, or official mode, an accompanying full-SHA `openclaw_ref` remains a commit assertion for that version-selected payload; branches and tags cannot be combined with `gateway_version`. The workflow also accepts `signing_mode`, whose choices are `unsigned`, `test`, -`store`, and `official`. Select `store` to authorize and retain an unsigned Partner Center +`store`, and `official`. Select `test` for a credential-free release-candidate +preflight, including the installed-upgrade matrix, from any branch. Select `store` +to authorize and retain an unsigned Partner Center submission bundle without signing. Select `official` to retain that Store bundle and sign the separate sideload-identity packages for GitHub Releases. Following stable does not automatically publish: both modes still require a @@ -83,12 +85,11 @@ Complete this checklist before dispatch. Merge any needed policy changes to 4. Review the release-facing pull request titles. The published release notes are generated from merged pull request titles; `CONTRIBUTING.md` owns the required title format. -5. Confirm every relevant versioning or source-selection pull request's - `test-msix-upgrades` matrix job succeeded - and retained its identity-transition or in-place-upgrade evidence. The - matrix runs only on pull requests that change versioning inputs or - source-selection scripts; it does not run during the later official - dispatch. +5. Confirm relevant packaging changes have passed their pull request checks. + A new upstream stable release needs no version-bump or validation-only pull + request: the release dispatch runs `test-msix-upgrades` against its own + source selection before authorization. Older pull request evidence does not + replace that gate. 6. Do not reuse or alter an accepted GitHub release tag. Update the latest production entries in `scripts\msix-upgrade-baselines.json` only in a reviewed policy pull request, using the verified digest of an immutable @@ -99,6 +100,13 @@ those inputs. A `store` or `official` run deliberately bypasses both the upstrea cache and the architecture-specific Windows dependency-tree cache, so its payload is rebuilt and revalidated. +For a preflight without release authorization or Azure credentials, dispatch +`signing_mode=test` with the desired source inputs. It uses the derived release +version and the same installed-upgrade matrix, then retains temporary test-signed +packages. A later `store` or `official` dispatch repeats that proof against its +own immutable source snapshot. `unsigned` dispatches and push builds keep +development versions and skip installed-upgrade validation. + ## Watch the release workflow The workflow first builds the validated upstream package, then builds the @@ -108,7 +116,8 @@ composes a multi-architecture bundle for each channel with `scripts\Build-MSIXBundle.ps1`. Both channels use the same validated payload and derived package version. -Before publication or Azure credentials, `authorize-signing` runs +After every `test-msix-upgrades` scenario succeeds, and before publication or +Azure credentials, `authorize-signing` runs `scripts\Test-SigningInputs.ps1`. It restores the same run's source snapshot and validates its workflow run ID, packaging commit, and signing mode without resolving latest again. That recorded source supplies the expected upstream @@ -124,11 +133,14 @@ and bundle artifacts; the Store bundle remains unsigned for Partner Center. Observe these workflow outcomes: - `build-msix` succeeds for **both** x64 and ARM64. -- `test-host` and `build-msix-bundle` succeed before `authorize-signing`. +- `test-host`, `build-msix-bundle`, and all 13 `test-msix-upgrades` scenarios + succeed before `authorize-signing`. Missing or skipped required upgrade + proof fails the final `Gateway MSIX CI` gate. - `authorize-signing` succeeds before Azure login and signing begins. - `sign-msix` verifies signatures and refreshes package metadata. -The pull request's upgrade matrix uses +The upgrade matrix runs for versioning/source-selection pull requests and +manual `test`, `store`, and `official` dispatches. It uses `scripts\Test-MSIXUpgrade.ps1` with the immutable, hash-pinned release fixtures in `scripts\msix-upgrade-baselines.json`. It requires an isolated clean Windows account per scenario and refuses to run when an OpenClaw Gateway package is @@ -167,14 +179,14 @@ bundle plus signed x64 and ARM64 standalone MSIX assets. After publication, verify the release has the derived permanent tag, generated notes, three signed sideload assets, and valid signatures. Separately verify -the run retained its unsigned Store submission bundle. Reconcile the release with the successful -identity-transition and in-place-upgrade evidence from the relevant pull request. -That evidence applies to the source it tested; when stable has advanced, record -the validation gap and obtain current-source upgrade proof before release. Verify the bundle and both +the run retained its unsigned Store submission bundle and all successful +identity-transition and in-place-upgrade evidence from that same release run. +The proof and packages share the run's captured upstream source and derived +release identity. Verify the bundle and both standalone packages are present; the bundle is the multi-architecture delivery, while the standalone packages support explicit architecture deployment. Keep -both the release workflow run and the relevant pull request's evidence available -as the release record. +the release workflow run, its immutable source snapshot, and its upgrade +evidence available as the release record. ## Failure and rollback boundaries diff --git a/scripts/Get-WorkflowSource.ps1 b/scripts/Get-WorkflowSource.ps1 index 4b68a93a..6ed3327b 100644 --- a/scripts/Get-WorkflowSource.ps1 +++ b/scripts/Get-WorkflowSource.ps1 @@ -24,7 +24,7 @@ if ($GatewayVersion -ne '') { $selectionPolicy | Add-Member -NotePropertyName stableVersion -NotePropertyValue $GatewayVersion -Force } -$releaseMode = $SigningMode -in @('official', 'store') +$releaseMode = $SigningMode -in @('test', 'official', 'store') $refAssertion = $releaseMode -and $GatewayVersion -ne '' -and $Ref -ne '' if ($Ref -ne '' -and $GatewayVersion -ne '') { if (-not $refAssertion) { diff --git a/scripts/Test-OpenClawSource.Tests.ps1 b/scripts/Test-OpenClawSource.Tests.ps1 index 7d4f8fc7..7cd913c7 100644 --- a/scripts/Test-OpenClawSource.Tests.ps1 +++ b/scripts/Test-OpenClawSource.Tests.ps1 @@ -389,7 +389,7 @@ try { Assert-Throws { Assert-OpenClawSource $source $policy } 'repository' Assert-Equal $http.Calls.Count 0 } - foreach ($mode in @('official', 'store')) { + foreach ($mode in @('test', 'official', 'store')) { Invoke-Test "$mode release follows stable and replays the captured release after latest advances" { Add-Release '2026.9.7' ('a' * 40) ('b' * 40) $http.Responses['Registry:latest'].version = '2026.9.7' diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index 56ed66e2..96035502 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -13,7 +13,7 @@ $workflow = Get-Content -LiteralPath $workflowPath -Raw $requiredFragments = @( 'group: gateway-msix-${{ github.event.pull_request.number || github.run_id }}' "cancel-in-progress: `${{ github.event_name == 'pull_request' }}" - 'name: Classify pull request changes' + 'name: Select Gateway MSIX validation' '.\scripts\Get-PackagingRelevance.ps1' 'name: Test packaging relevance' 'name: Test MSBuild property escaping' @@ -330,4 +330,183 @@ if ($workflow.Contains('publish-store-release:', [StringComparison]::Ordinal)) { throw 'Unsigned Store packages must not be published as GitHub Releases.' } +function Get-WorkflowStepScript { + param([Parameter(Mandatory)][string]$Name) + + $step = [regex]::Match( + $workflow, + '(?ms)^ - name: ' + [regex]::Escape($Name) + '\r?\n' + + '.*?^ run: \|\r?\n' + + '(?