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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions .github/workflows/gateway-msix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ env:

jobs:
changes:
name: Classify pull request changes
name: Select Gateway MSIX validation
runs-on: ubuntu-latest
outputs:
packaging: ${{ steps.filter.outputs.packaging }}
Expand All @@ -65,12 +65,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
}

Expand Down Expand Up @@ -106,9 +116,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
Expand Down Expand Up @@ -610,7 +617,7 @@ jobs:
test-msix-upgrades:
cache-mode: none
name: Test MSIX upgrade (${{ matrix.name }})
if: ${{ !cancelled() && github.event_name == 'pull_request' && needs.changes.result == 'success' && needs.changes.outputs.versioning == 'true' && needs.test-host.result == 'success' && needs.build.result == 'success' }}
if: ${{ !cancelled() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && needs.changes.result == 'success' && needs.changes.outputs.versioning == 'true' && needs.test-host.result == 'success' && needs.build.result == 'success' }}
needs:
- changes
- test-host
Expand Down Expand Up @@ -730,10 +737,11 @@ jobs:
authorize-signing:
cache-mode: none
name: Authorize Gateway MSIX release
if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && (inputs.signing_mode == 'official' || inputs.signing_mode == 'store') && github.ref == 'refs/heads/main' && needs.test-host.result == 'success' && needs.build.result == 'success' }}
if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && (inputs.signing_mode == 'official' || inputs.signing_mode == 'store') && github.ref == 'refs/heads/main' && needs.test-host.result == 'success' && needs.build.result == 'success' && needs.test-msix-upgrades.result == 'success' }}
needs:
- test-host
- build
- test-msix-upgrades
runs-on: windows-latest
outputs:
package_version: ${{ steps.release.outputs.package_version }}
Expand Down Expand Up @@ -1134,11 +1142,12 @@ jobs:
with:
persist-credentials: false

- name: Require versioning upgrade tests
- name: Require candidate upgrade tests
if: ${{ always() }}
shell: pwsh
env:
EVENT_NAME: ${{ github.event_name }}
SIGNING_MODE: ${{ inputs.signing_mode || 'unsigned' }}
VERSIONING: ${{ needs.changes.outputs.versioning }}
CHANGES_RESULT: ${{ needs.changes.result }}
TEST_HOST_RESULT: ${{ needs.test-host.result }}
Expand All @@ -1147,6 +1156,7 @@ jobs:
run: |
./scripts/Assert-WorkflowJobResults.ps1 `
-EventName $env:EVENT_NAME `
-SigningMode $env:SIGNING_MODE `
-Versioning $env:VERSIONING `
-ChangesResult $env:CHANGES_RESULT `
-TestHostResult $env:TEST_HOST_RESULT `
Expand Down
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,11 @@ upload step logs before relying on the results.

The source-selection tests use offline npm and GitHub fixtures to exercise
snapshot capture/replay, bound workflow context, and trusted versus read-only
routes. `Test-WorkflowSigningConfiguration.ps1` executes the selected-build
owner: exactly one route must succeed and its outputs must match preflight.
It no longer inspects YAML fragments. The signing-input suite validates
routes. `Test-WorkflowSigningConfiguration.ps1` executes the workflow classifier
and selected-build owner: release-candidate dispatches select the full upgrade
matrix, exactly one build route must succeed, and its outputs must match preflight.
`Test-WorkflowJobResults.Tests.ps1` checks the final upgrade-proof requirement.
The signing-input suite validates
the actual synthetic packages and bundles against the captured source.

The shared build workflow inherits the caller's explicit cache token cap;
Expand Down
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,18 +372,19 @@ Package and payload metadata record the resolved source commit and version.
For a one-time source override in any signing mode, provide an OpenClaw tag,
branch, or full commit SHA in the manual `openclaw_ref` input. Empty means follow
stable. The resolver records the ref's exact commit and package version once;
retries use that immutable snapshot even if a branch later moves. Release modes
also capture the unmodified stable selector as the MSIX and GitHub release
identity source, so a hotfix payload does not create a second version owner.
retries use that immutable snapshot even if a branch later moves.
Release-candidate modes (`test`, `store`, and `official`) also capture the
unmodified stable selector as the MSIX and GitHub release identity source, so a
hotfix payload does not create a second version owner.
The override payload version may equal or precede that stable identity, but may
not be newer.
For a one-run rollback of the bundled payload in any signing mode, set
`gateway_version` to an older exact stable release. The payload uses that version,
while the MSIX and GitHub release identity continue from the unmodified stable
selector above so the package remains an upgrade. Both sources are verified and
captured in the immutable snapshot. The workflow rejects an override that is not
older than the release-identity source. Unsigned and test runs reject combining
it with `openclaw_ref`; Store and official runs preserve a full-SHA
older than the release-identity source. Unsigned runs reject combining it with
`openclaw_ref`; test, Store, and official runs preserve a full-SHA
`openclaw_ref` as an assertion that the version resolved to the intended commit.
If compatibility requires an older known-good stable release, a reviewed
`stableVersion` field in `release-policy.json` can pin its exact version, for
Expand Down Expand Up @@ -690,9 +691,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;
Expand All @@ -703,6 +705,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.
Expand Down Expand Up @@ -770,8 +776,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
Expand Down
46 changes: 29 additions & 17 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 captures one immutable source snapshot in `source-preflight`.
Expand All @@ -118,7 +126,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, signing mode, workflow
ref, Git ref, and event without
Expand All @@ -136,11 +145,14 @@ Observe these workflow outcomes:

- Exactly one build caller and the normalized `build` job succeed.
- `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
Expand Down Expand Up @@ -179,14 +191,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

Expand Down
2 changes: 1 addition & 1 deletion release-policy.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"repository": "https://github.com/openclaw/openclaw",
"channel": "stable",
"msixRevision": 1,
"msixRevision": 0,
"packageIdentityName": "OpenClawFoundation.OpenClawGateway",
"packageFamilyName": "OpenClawFoundation.OpenClawGateway_rfcbke2p71se2",
"publisher": "CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC",
Expand Down
11 changes: 8 additions & 3 deletions scripts/Assert-WorkflowJobResults.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ param(
[Parameter(Mandatory)]
[ValidateSet('pull_request', 'workflow_dispatch', 'push')]
[string]$EventName,
[ValidateSet('unsigned', 'test', 'store', 'official')]
[string]$SigningMode = 'unsigned',
[string]$Versioning,
[Parameter(Mandatory)]
[ValidateSet('success', 'failure', 'cancelled', 'skipped')]
Expand All @@ -25,12 +27,15 @@ if ($ChangesResult -cne 'success') {
throw "Change classification must succeed; result was '$ChangesResult'."
}

$upgradeRequired = (
($EventName -ceq 'pull_request' -and $Versioning -ceq 'true') -or
($EventName -ceq 'workflow_dispatch' -and $SigningMode -in @('test', 'store', 'official'))
)
if (
$EventName -ceq 'pull_request' -and
$Versioning -ceq 'true' -and
$upgradeRequired -and
$TestHostResult -ceq 'success' -and
$BuildResult -ceq 'success' -and
$UpgradeResult -cne 'success'
) {
throw "Versioning pull requests require successful MSIX upgrade tests; result was '$UpgradeResult'."
throw "Release candidates and versioning pull requests require successful MSIX upgrade tests; result was '$UpgradeResult'."
}
2 changes: 1 addition & 1 deletion scripts/Get-WorkflowSource.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if ($GatewayVersion -ne '') {
}
$null = Get-OpenClawBuildRoute -WorkflowRef $WorkflowRef -GitHubRef $GitHubRef `
-EventName $EventName -SigningMode $SigningMode -Channel ''
$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) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/Test-OpenClawSource.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,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'
Expand Down
23 changes: 22 additions & 1 deletion scripts/Test-WorkflowJobResults.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function Assert-Rejected {
}
return
}
throw 'An eligible versioning pull request accepted an unsuccessful upgrade job.'
throw 'An eligible release candidate or versioning pull request accepted an unsuccessful upgrade job.'
}

$base = @{
Expand Down Expand Up @@ -51,6 +51,27 @@ Invoke-Validator @{
Versioning = ''
UpgradeResult = 'skipped'
}
foreach ($mode in @('test', 'store', 'official')) {
Invoke-Validator @{
EventName = 'workflow_dispatch'
SigningMode = $mode
Versioning = 'true'
UpgradeResult = 'success'
}
foreach ($result in @('skipped', 'failure', 'cancelled')) {
$arguments = $base.Clone()
$arguments.EventName = 'workflow_dispatch'
$arguments.SigningMode = $mode
$arguments.Versioning = 'false'
$arguments.UpgradeResult = $result
Assert-Rejected $arguments
}
}
Invoke-Validator @{
EventName = 'push'
Versioning = 'false'
UpgradeResult = 'skipped'
}
Invoke-Validator @{
Versioning = 'false'
UpgradeResult = 'skipped'
Expand Down
Loading
Loading