Skip to content
Merged
101 changes: 72 additions & 29 deletions .github/workflows/gateway-msix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ on:
workflow_dispatch:
inputs:
openclaw_ref:
description: openclaw/openclaw tag, branch, or commit to package
required: true
default: 3a9d69db306cd7f081e06254cb89c4bcc14a7107
description: Optional stable-source ref; empty follows npm latest (official signing still requires policy approval)
required: false
default: ''
type: string
signing_mode:
description: Package signing mode
Expand All @@ -31,7 +31,6 @@ permissions:
pull-requests: read

env:
OPENCLAW_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.openclaw_ref || '3a9d69db306cd7f081e06254cb89c4bcc14a7107' }}
PACKAGING_ROOT: .

jobs:
Expand Down Expand Up @@ -74,6 +73,9 @@ jobs:
ConvertFrom-Json
$versioningPaths = @(
'release-policy.json'
'scripts/OpenClawSource.ps1'
'scripts/Get-WorkflowSource.ps1'
'scripts/Test-OpenClawSource.Tests.ps1'
'scripts/Get-MSIXReleaseIdentity.ps1'
'scripts/Test-MSIXReleaseIdentity.Tests.ps1'
'scripts/Test-MSIXUpgrade.ps1'
Expand Down Expand Up @@ -201,6 +203,10 @@ jobs:
run: >
.\scripts\Test-OpenClawPackage.Tests.ps1

- name: Test stable source selection
shell: pwsh
run: .\scripts\Test-OpenClawSource.Tests.ps1

- name: Test local package deployment
shell: pwsh
run: >
Expand Down Expand Up @@ -233,6 +239,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
source_sha: ${{ steps.resolve.outputs.sha }}
source_tag: ${{ steps.resolve.outputs.tag }}
package_cache_key: ${{ steps.resolve.outputs.package_cache_key }}
package_version: ${{ steps.package.outputs.version }}
node_version: ${{ steps.package.outputs.node_version }}
Expand All @@ -243,29 +250,47 @@ jobs:
with:
persist-credentials: false

- name: Restore source selection for a retry
if: ${{ github.run_attempt != 1 }}
uses: actions/download-artifact@v8
with:
name: openclaw-source-resolution
path: ${{ runner.temp }}/openclaw-source

- name: Resolve immutable OpenClaw source
id: resolve
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REF: ${{ inputs.openclaw_ref }}
SIGNING_MODE: ${{ inputs.signing_mode || 'unsigned' }}
run: |
$shaLines = @(
gh api `
"repos/openclaw/openclaw/commits/$env:OPENCLAW_REF" `
--jq .sha
)
if ($LASTEXITCODE -ne 0) {
throw "Unable to resolve OpenClaw ref '$env:OPENCLAW_REF'."
}
$sha = [string]::Join('', [string[]]$shaLines).Trim().ToLowerInvariant()
if ($sha -notmatch '^[0-9a-f]{40}$') {
throw "OpenClaw ref resolved to an invalid commit SHA: '$sha'."
}
$snapshotPath = Join-Path $env:RUNNER_TEMP 'openclaw-source/source-resolution.json'
$source = ./scripts/Get-WorkflowSource.ps1 `
-PolicyPath ./release-policy.json `
-OutputPath $snapshotPath `
-Ref $env:SOURCE_REF `
-SigningMode $env:SIGNING_MODE `
-WorkflowRunId $env:GITHUB_RUN_ID `
-PackagingCommit $env:GITHUB_SHA `
-ReuseSnapshot:($env:GITHUB_RUN_ATTEMPT -ne '1')
$cacheKey = .\scripts\Get-OpenClawCacheKey.ps1 `
-Layer package `
-Commit $sha
"sha=$sha" >> $env:GITHUB_OUTPUT
-Commit $source.resolvedCommit
"sha=$($source.resolvedCommit)" >> $env:GITHUB_OUTPUT
"tag=v$($source.packageVersion)" >> $env:GITHUB_OUTPUT
"version=$($source.packageVersion)" >> $env:GITHUB_OUTPUT
"package_cache_key=$cacheKey" >> $env:GITHUB_OUTPUT
"OpenClaw $($source.packageVersion) ($($source.resolvedCommit)), selected by $($source.requestedRef)." >> $env:GITHUB_STEP_SUMMARY

- name: Save immutable source selection
if: ${{ github.run_attempt == 1 }}
uses: actions/upload-artifact@v7
with:
name: openclaw-source-resolution
path: ${{ runner.temp }}/openclaw-source/source-resolution.json
if-no-files-found: error
retention-days: 90

- name: Restore cached OpenClaw package
id: package-cache
Expand Down Expand Up @@ -314,6 +339,8 @@ jobs:

- name: Pack npm package
if: ${{ steps.package-cache.outputs.cache-hit != 'true' }}
env:
OPENCLAW_REF: ${{ steps.resolve.outputs.sha }}
run: |
set -euo pipefail
artifact_dir="${RUNNER_TEMP}/openclaw-package"
Expand Down Expand Up @@ -350,7 +377,8 @@ jobs:
& "$env:RUNNER_TEMP/Test-OpenClawPackage.ps1" `
-PackageDirectory $packageDirectory `
-ExpectedCommit '${{ steps.resolve.outputs.sha }}' `
-RequestedRef $env:OPENCLAW_REF
-ExpectedVersion '${{ steps.resolve.outputs.version }}' `
-RequestedRef '${{ steps.resolve.outputs.sha }}'
$metadata = Get-Content -LiteralPath $metadataPath -Raw |
ConvertFrom-Json
"version=$([string]$metadata.packageVersion)" >> $env:GITHUB_OUTPUT
Expand Down Expand Up @@ -378,13 +406,15 @@ jobs:
- changes
- test-host
- build-package
runs-on: windows-latest
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
architecture:
- x64
- arm64
include:
- architecture: x64
runner: windows-latest
- architecture: arm64
runner: windows-11-arm
steps:
- name: Check out repository
uses: actions/checkout@v7
Expand All @@ -395,6 +425,11 @@ jobs:
uses: actions/setup-node@v6
with:
node-version: ${{ needs.build-package.outputs.node_version }}
architecture: ${{ matrix.architecture }}

- name: Test native payload installation
shell: pwsh
run: .\scripts\Test-NodeRuntimeInputs.Tests.ps1

- name: Download intermediate package
uses: actions/download-artifact@v8
Expand Down Expand Up @@ -477,6 +512,7 @@ jobs:
env:
SIGNING_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode || 'unsigned' }}
VERSIONING_CHANGE: ${{ needs.changes.outputs.versioning }}
GATEWAY_TAG: ${{ needs.build-package.outputs.source_tag }}
run: |
$versionParameters = @{
RunNumber = '${{ github.run_number }}'
Expand All @@ -489,7 +525,7 @@ jobs:
$policy = Get-Content -LiteralPath .\release-policy.json -Raw |
ConvertFrom-Json
$identity = .\scripts\Get-MSIXReleaseIdentity.ps1 `
-GatewayTag ([string]$policy.gatewayTag) `
-GatewayTag $env:GATEWAY_TAG `
-MSIXRevision ([int]$policy.msixRevision)
$versionParameters.ReleaseVersion = $identity.PackageVersion
}
Expand Down Expand Up @@ -566,6 +602,7 @@ jobs:
name: Build unsigned multi-architecture Gateway MSIX bundle
needs:
- changes
- build-package
- build-msix
runs-on: windows-latest
steps:
Expand All @@ -591,6 +628,7 @@ jobs:
env:
SIGNING_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode || 'unsigned' }}
VERSIONING_CHANGE: ${{ needs.changes.outputs.versioning }}
GATEWAY_TAG: ${{ needs.build-package.outputs.source_tag }}
run: |
$versionParameters = @{
RunNumber = '${{ github.run_number }}'
Expand All @@ -603,7 +641,7 @@ jobs:
$policy = Get-Content -LiteralPath .\release-policy.json -Raw |
ConvertFrom-Json
$identity = .\scripts\Get-MSIXReleaseIdentity.ps1 `
-GatewayTag ([string]$policy.gatewayTag) `
-GatewayTag $env:GATEWAY_TAG `
-MSIXRevision ([int]$policy.msixRevision)
$versionParameters.ReleaseVersion = $identity.PackageVersion
}
Expand All @@ -629,6 +667,7 @@ jobs:
if: ${{ github.event_name == 'pull_request' && needs.changes.outputs.versioning == 'true' }}
needs:
- changes
- build-package
- build-msix
- build-msix-bundle
runs-on: windows-latest
Expand Down Expand Up @@ -681,11 +720,13 @@ jobs:

- name: Test installed-package upgrades and retained LocalState
shell: pwsh
env:
GATEWAY_TAG: ${{ needs.build-package.outputs.source_tag }}
run: |
$policy = Get-Content -LiteralPath .\release-policy.json -Raw |
ConvertFrom-Json
$identity = .\scripts\Get-MSIXReleaseIdentity.ps1 `
-GatewayTag ([string]$policy.gatewayTag) `
-GatewayTag $env:GATEWAY_TAG `
-MSIXRevision ([int]$policy.msixRevision)
.\scripts\Test-MSIXUpgrade.ps1 `
-BaselinesPath .\scripts\msix-upgrade-baselines.json `
Expand Down Expand Up @@ -721,6 +762,7 @@ jobs:
name: Authorize official Gateway MSIX signing
if: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode == 'official' && github.ref == 'refs/heads/main' }}
needs:
- build-package
- build-msix
- build-msix-bundle
runs-on: windows-latest
Expand Down Expand Up @@ -776,7 +818,7 @@ jobs:
- name: Enforce official signing policy
shell: pwsh
env:
OPENCLAW_REF: ${{ inputs.openclaw_ref }}
OPENCLAW_REF: ${{ inputs.openclaw_ref || needs.build-package.outputs.source_sha }}
PACKAGING_COMMIT: ${{ github.sha }}
run: |
.\scripts\Test-SigningInputs.ps1 `
Expand Down Expand Up @@ -929,6 +971,7 @@ jobs:
name: Publish signed Gateway MSIX release
if: ${{ needs.sign-msix.result == 'success' }}
needs:
- build-package
- authorize-signing
- sign-msix
runs-on: ubuntu-latest
Expand Down Expand Up @@ -982,8 +1025,8 @@ jobs:
release-assets/*.msix
release-assets/*.msixbundle
body: |
Packages OpenClaw Gateway `${{ needs.authorize-signing.outputs.release_tag }}`
from [`openclaw/openclaw@${{ inputs.openclaw_ref }}`](https://github.com/openclaw/openclaw/commit/${{ inputs.openclaw_ref }}).
Packages OpenClaw `${{ needs.build-package.outputs.package_version }}`
from [`openclaw/openclaw@${{ needs.build-package.outputs.source_sha }}`](https://github.com/openclaw/openclaw/commit/${{ needs.build-package.outputs.source_sha }}).

### Downloads
- **Recommended:** `OpenClawGateway-${{ needs.authorize-signing.outputs.release_version }}.msixbundle`
Expand Down
9 changes: 8 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,19 @@ or package version logic:
.\scripts\Test-OpenClawCacheKey.Tests.ps1
.\scripts\Test-OpenClawPackage.Tests.ps1
.\scripts\Test-MSIXReleaseIdentity.Tests.ps1
.\scripts\Test-OpenClawSource.Tests.ps1
.\scripts\Test-WorkflowPackageVersion.Tests.ps1
.\scripts\Test-GitHooks.Tests.ps1
```

The source-selection tests use offline npm and GitHub fixtures.

The Node.js input suite requires Node.js and npm. It builds a dependency-free
local fixture; it does not download or build OpenClaw.
local fixture, including its install script, for the running Node.js
architecture; it does not download or build OpenClaw. CI also runs this suite
on the native x64 and ARM64 packaging runners. Payload installation requires
Node.js to match the target architecture; npm's CPU flags alone do not change
the architecture seen by dependency install scripts.

Run the NativeAOT publish when you change host JSON, reflection, interop, or
anything else that is trimming-sensitive. A JIT `dotnet build` does not
Expand Down
63 changes: 42 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,17 +281,33 @@ place so an update does not remove a running process's runtime.

## Selecting the OpenClaw revision

`.github\workflows\gateway-msix.yml` resolves an explicit OpenClaw ref before
building. Pull-request and `main` push runs use the pinned commit configured in
both:

- `workflow_dispatch.inputs.openclaw_ref.default`;
- the non-manual fallback in `env.OPENCLAW_REF`.

Changing only the workflow-dispatch default does not change automatic builds.
For a one-time override, run **Build OpenClaw Gateway MSIX** manually and
provide a tag, branch, or preferably a full 40-character commit SHA in
`openclaw_ref`. Payload composition validates that the selected OpenClaw
`.github\workflows\gateway-msix.yml` selects **stable** through public npm
`openclaw@latest` whenever a new packaging run starts. The resolver checks the
exact published version, its signed upstream tag and commit, and the source
package version before building. There is no automatic fallback to another
version or channel; extended-stable and named prereleases are rejected.
Source selection also checks the MSIX release-version rules before building:
numeric correction suffixes must be `-2` through `-9`. Unsupported corrections
are rejected for channel selection, explicit refs, policy pins, and retries.

The `openclaw-source-resolution` artifact records this choice once per run.
Retries reuse it without querying the moving channel again. If the snapshot
is missing or expired (90-day retention), start a new run instead of retrying.
Package and payload metadata record the resolved source commit and version.

For a one-time unsigned/test override, provide a stable-source tag, branch, or
full commit SHA in the manual `openclaw_ref` input. Empty means follow stable.
If compatibility requires an older known-good stable release, a reviewed
`stableVersion` field in `release-policy.json` can pin its exact version, for
example `"stableVersion": "2026.9.4"`. A pin is not automatic fallback and does
not grant official-signing approval.

For official signing, the selected source must match `approvedCommit`,
`gatewayTag`, and `payloadPackageVersion` in `release-policy.json`. An empty
input selects stable and checks that approval; an explicit input must be the
full approved commit SHA.

Payload composition validates that the selected OpenClaw
runtime discovers the packaging-owned Windows Launcher plugin in its
default-disabled state, then explicitly enables only that plugin in an isolated
temporary validation profile before using OpenClaw's runtime inspection pass to
Expand All @@ -310,7 +326,7 @@ runtime-support policy.
Non-official workflows cache the packed OpenClaw tarball by its resolved
upstream commit. They also cache each architecture's Windows dependency tree by
the resolved commit, tarball SHA-256, Node.js version, and payload-build script.
A tarball cache hit still verifies the recorded commit and SHA-256; a
A tarball cache hit still verifies the recorded version, commit and SHA-256; a
dependency-tree hit still runs every payload validation and smoke test.
Official-signing workflows bypass
both caches and always rebuild upstream source and Windows dependencies.
Expand Down Expand Up @@ -341,7 +357,13 @@ dotnet test .\OpenClaw.Gateway.MSIX.slnx `
```

`scripts\Build-Payload.ps1` npm-installs an OpenClaw package into an expanded,
architecture-specific application tree. It validates the Gateway and Control UI
architecture-specific application tree. Run it with Node.js matching both
the selected upstream version and target architecture: native install scripts
can use `process.arch` instead of npm's target-CPU flag. CI builds x64 on
`windows-latest` and ARM64 on `windows-11-arm`, using matching Node.js binaries.
Both payloads run their CLI smoke test. Cross-architecture Node.js execution
is rejected before staging or npm installation, including when reusing a tree.
It validates the Gateway and Control UI
build identities on the installed tree, including reused staged installs, then
provisions the packaging-owned Windows Launcher plugin into the payload copy's
bundled plugin directory. Its internal package, path, and plugin ID remain
Expand All @@ -365,7 +387,7 @@ 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 `v2026.9.4`
The official-signing policy 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
Expand Down Expand Up @@ -442,9 +464,9 @@ never official-signing inputs.
Normal pull-request and push workflows publish unsigned packages for
validation. Manual runs support three signing modes:

- `unsigned` accepts any OpenClaw branch, tag, or commit and publishes unsigned
- `unsigned` follows stable or a stable-source override and publishes unsigned
MSIX packages;
- `test` accepts any OpenClaw ref and publishes MSIX packages signed with a
- `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;
- `official` requires the approved immutable commit from
Expand Down Expand Up @@ -489,9 +511,7 @@ reviewed pull request:
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`.
same Gateway tag.

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
Expand All @@ -514,8 +534,9 @@ 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
retained. Changes to source-selection scripts also trigger this check against
the selected release. 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
Expand Down
Loading
Loading