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
148 changes: 137 additions & 11 deletions .github/workflows/gateway-msix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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
}

Expand All @@ -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/Test-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
Expand Down Expand Up @@ -164,6 +185,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: >
Expand Down Expand Up @@ -328,6 +354,7 @@ jobs:
build-msix:
name: Build unsigned ${{ matrix.architecture }} Gateway MSIX
needs:
- changes
- test-host
- build-package
runs-on: windows-latest
Expand Down Expand Up @@ -423,15 +450,22 @@ 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
$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
Expand Down Expand Up @@ -504,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
Expand All @@ -528,15 +564,22 @@ 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
$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
Expand All @@ -555,6 +598,86 @@ 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
- build-msix-bundle
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: 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: |
.\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 `
-CandidateBundlePath test-signed\bundle\OpenClawGateway.msixbundle `
-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' }}
Expand Down Expand Up @@ -612,15 +735,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
Expand Down Expand Up @@ -851,6 +976,7 @@ jobs:
- build-msix
- test-sign-msix
- build-msix-bundle
- test-msix-upgrades
- reject-untrusted-official-signing
- authorize-signing
- sign-msix
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -183,6 +184,19 @@ 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.
- 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 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.
Expand Down
75 changes: 59 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -292,23 +292,66 @@ 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
`<gateway-tag>-msix.<revision>`. The MSIX identity is
`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-<version>.msixbundle` as the recommended download, plus signed
`OpenClawGateway-<version>-x64.msix` and
`OpenClawGateway-<version>-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.
Microsoft Store submissions reserve the fourth version component as zero, so
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 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. 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
Expand Down
Loading
Loading