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
67 changes: 63 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ jobs:
shell: pwsh
run: ./scripts/test-msix-ci-artifacts.ps1

- name: Validate Store MSIX bundle construction
shell: pwsh
run: ./scripts/test-msix-bundle.ps1

- name: Validate MSIX version allocation
shell: pwsh
run: ./scripts/test-msix-versioning.ps1
Expand Down Expand Up @@ -976,10 +980,63 @@ jobs:
shell: pwsh
run: .\scripts\setup-dev-msix-cert.ps1 -Remove

build-msix-bundle:
name: Multi-architecture Store MSIX bundle
needs: [change-classification, metadata, reserve-msix-version, build-msix]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && (needs.reserve-msix-version.result == 'success' || needs.reserve-msix-version.result == 'skipped') && needs.build-msix.result == 'success' && (needs.change-classification.outputs.x64_release == 'true' || needs.change-classification.outputs.arm64_release == 'true') }}
runs-on: windows-latest
env:
MSIX_VERSION_INFO: ${{ needs.reserve-msix-version.outputs.versionInfo || needs.metadata.outputs.msixVersionInfo }}
MSIX_SOURCE_VERSION: ${{ needs.reserve-msix-version.outputs.sourceVersion || needs.metadata.outputs.msixSourceVersion }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Download unsigned x64 Store package
uses: actions/download-artifact@v8
with:
name: openclaw-msix-store-unsigned-x64
path: artifacts/msix/x64

- name: Download unsigned ARM64 Store package
uses: actions/download-artifact@v8
with:
name: openclaw-msix-store-unsigned-arm64
path: artifacts/msix/arm64

- name: Validate shared MSIX version allocation
id: version
shell: pwsh
run: |
if ([string]::IsNullOrWhiteSpace($env:MSIX_VERSION_INFO)) {
throw 'Missing MSIX version allocation; refusing to bundle with a fallback version.'
}
. .\scripts\MsixVersioning.ps1
$info = Assert-MsixVersionInfo -VersionInfo ($env:MSIX_VERSION_INFO | ConvertFrom-Json) `
-SourceCommit $env:GITHUB_SHA -SourceVersion $env:MSIX_SOURCE_VERSION
"packageVersion=$($info.storePackageVersion)" >> $env:GITHUB_OUTPUT

- name: Compose unsigned multi-architecture Store MSIX bundle
shell: pwsh
run: |
.\scripts\Build-StoreMsixBundle.ps1 `
-X64Package artifacts\msix\x64\OpenClaw-x64.msix `
-Arm64Package artifacts\msix\arm64\OpenClaw-arm64.msix `
-PackageVersion '${{ steps.version.outputs.packageVersion }}' `
-OutputPath artifacts\msix\bundle\OpenClaw.msixbundle

- name: Upload unsigned multi-architecture Store submission artifact
uses: actions/upload-artifact@v7
with:
name: openclaw-msix-store-unsigned-bundle
path: artifacts/msix/bundle/OpenClaw.msixbundle
if-no-files-found: error

ci-gate:
name: CI Gate
if: ${{ always() }}
needs: [change-classification, fast-validation, proof-pool-contracts, metadata, core-tests, tray-tests, ui-tests, setup-e2e, revocation-e2e, network-e2e, build-x64, build-arm64, build-msix]
needs: [change-classification, fast-validation, proof-pool-contracts, metadata, core-tests, tray-tests, ui-tests, setup-e2e, revocation-e2e, network-e2e, build-x64, build-arm64, build-msix, build-msix-bundle]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -1010,6 +1067,7 @@ jobs:
ARM64_RELEASE_RESULT: ${{ needs.build-arm64.result }}
METADATA_RESULT: ${{ needs.metadata.result }}
MSIX_RESULT: ${{ needs.build-msix.result }}
MSIX_BUNDLE_RESULT: ${{ needs.build-msix-bundle.result }}
run: |
$validatedMode = ./scripts/Assert-CiGateResults.ps1 `
-ClassificationResult $env:CLASSIFICATION_RESULT `
Expand All @@ -1034,12 +1092,13 @@ jobs:
-Arm64ReleaseRequired $env:ARM64_RELEASE_REQUIRED `
-Arm64ReleaseResult $env:ARM64_RELEASE_RESULT `
-MetadataResult $env:METADATA_RESULT `
-MsixResult $env:MSIX_RESULT
-MsixResult $env:MSIX_RESULT `
-MsixBundleResult $env:MSIX_BUNDLE_RESULT
"CI Gate passed $validatedMode validation." >> $env:GITHUB_STEP_SUMMARY

release:
needs: [change-classification, metadata, reserve-msix-version, build-x64, build-arm64, ci-gate]
if: startsWith(github.ref, 'refs/tags/v') && needs.ci-gate.result == 'success' && needs.change-classification.outputs.full == 'true' && needs.metadata.result == 'success' && needs.reserve-msix-version.result == 'success' && needs.build-x64.result == 'success' && needs.build-arm64.result == 'success' && !cancelled()
needs: [change-classification, metadata, reserve-msix-version, build-x64, build-arm64, build-msix-bundle, ci-gate]
if: startsWith(github.ref, 'refs/tags/v') && needs.ci-gate.result == 'success' && needs.change-classification.outputs.full == 'true' && needs.metadata.result == 'success' && needs.reserve-msix-version.result == 'success' && needs.build-x64.result == 'success' && needs.build-arm64.result == 'success' && needs.build-msix-bundle.result == 'success' && !cancelled()
runs-on: windows-latest
environment: release-signing
permissions:
Expand Down
7 changes: 5 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ runtime, the in-process SetupEngine UI, and the architecture-matched
the loose Visual C++ runtime files that the Inno payload ships but the MSIX
resolves through its VCLibs framework dependency).

Upload both `.msix` files to the same Partner Center submission. `Identity/@Name`,
CI combines both packages into `OpenClaw.msixbundle`, the recommended single
Partner Center submission input. The standalone `.msix` files remain available
for architecture-specific inspection or fallback. `Identity/@Name`,
`Identity/@Publisher`, and `Properties/PublisherDisplayName` in
`src\OpenClaw.Tray.WinUI\Package.appxmanifest` already hold the reserved
Partner Center values and must keep matching **Product management > Product
Expand All @@ -320,7 +322,8 @@ without it the build logs a warning and skips symbols.

#### CI MSIX downloads

The **Build and Test** workflow builds both x64 and ARM64 MSIX variants whenever
The **Build and Test** workflow builds both x64 and ARM64 MSIX variants and a
multi-architecture bundle whenever
the change classifier selects a release-build lane. This includes packaging,
build, and workflow PRs, pushes to `main`/`master`, tags, and manual workflow
dispatches. Ordinary targeted or documentation-only PRs intentionally skip them.
Expand Down
28 changes: 16 additions & 12 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ smoke only; ARM64 portable publish remains required on `main` and tags.
When either release-build lane is selected, CI also builds both architectures
of Dev-signed and unsigned Store MSIX **workflow artifacts**. CI Gate requires
that MSIX job to succeed. Canonical alpha releases also attach the unsigned
Store MSIX packages and metadata for manual Partner Center submission.
Store MSIX bundle, standalone packages, and metadata for manual Partner Center
submission.
Stable releases do not include MSIX assets; Dev-signed packages stay in Actions.

## Release checklist
Expand Down Expand Up @@ -175,16 +176,18 @@ Current release artifacts are:

Canonical alpha releases additionally contain:

- `OpenClaw.msixbundle` (recommended Partner Center submission input)
- `OpenClaw-x64.msix` and `OpenClaw-arm64.msix`
- `OpenClaw-x64.msix-metadata.json` and
`OpenClaw-arm64.msix-metadata.json`

These are **unsigned Store submission inputs, not installers**. Download the
MSIX files and upload them manually to Partner Center. Microsoft signs accepted
Store submissions. The alpha release step checks both architectures' clean
source provenance, identity, version, and package hashes before staging the
unchanged bytes built by `Build-StoreMsix.ps1`. It fails rather than publishing
a partial or mismatched set.
These are **unsigned Store submission inputs, not installers**. Upload the
bundle to Partner Center for one architecture-selecting submission. The
standalone packages remain available for inspection or fallback. Microsoft
signs accepted Store submissions. The alpha release step checks both
architectures' clean source provenance, identity, version, and package hashes,
then proves that the bundle embeds those exact bytes. It fails rather than
publishing a partial or mismatched set.

Stable, stable-correction, and non-alpha prereleases retain the existing
EXE/ZIP asset set and do not receive MSIX download notes. Dev-signed tester
Expand Down Expand Up @@ -407,10 +410,11 @@ proofs as skipped when the host is not MXC-capable; use
`.\scripts\validate-mxc-e2e.ps1` for required local/self-hosted MXC merge
validation. Release tags cannot enter the `release` job until **CI Gate**
confirms classification, fast validation, tests, E2E, and release builds all
succeeded. The `build-msix` job must also succeed whenever release metadata is
required. The release job downloads and attaches its unsigned Store packages
only for canonical alpha tags. Stable releases and Dev tester distribution
do not gain MSIX release attachments.
succeeded. The `build-msix` and `build-msix-bundle` jobs must also succeed
whenever release metadata is required. The release job downloads and attaches
its unsigned Store bundle, standalone packages, and metadata only for canonical
alpha tags. Stable releases and Dev tester distribution do not gain MSIX
release attachments.

The release job should:

Expand All @@ -422,7 +426,7 @@ The release job should:
6. Build Inno installers.
7. Sign installers.
8. For canonical alpha tags only, stage the validated unsigned Store MSIX
packages and metadata.
bundle, standalone packages, and metadata.
9. Create a GitHub release whose prerelease flag matches the tag, with installer
and portable ZIP assets plus any gated alpha submission assets.

Expand Down
4 changes: 3 additions & 1 deletion scripts/Assert-CiGateResults.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ param(
[Parameter(Mandatory)][string]$Arm64ReleaseRequired,
[Parameter(Mandatory)][string]$Arm64ReleaseResult,
[Parameter(Mandatory)][string]$MetadataResult,
[Parameter(Mandatory)][string]$MsixResult
[Parameter(Mandatory)][string]$MsixResult,
[Parameter(Mandatory)][string]$MsixBundleResult
)

Set-StrictMode -Version Latest
Expand Down Expand Up @@ -127,5 +128,6 @@ Assert-LaneResult "ARM64 release publish" $required.arm64_release $Arm64ReleaseR
$metadataRequired = $required.x64_release -or $required.arm64_release
Assert-LaneResult "release metadata" $metadataRequired $MetadataResult
Assert-LaneResult "MSIX workflow artifacts" $metadataRequired $MsixResult
Assert-LaneResult "multi-architecture MSIX bundle" $metadataRequired $MsixBundleResult

$Classification
4 changes: 2 additions & 2 deletions scripts/Build-StoreMsix.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
was dirty, the package version, publisher, and the package SHA-256.

.PARAMETER Architecture
Target architecture: x64 or arm64. Defaults to x64. The Store serves a
separate package per architecture; upload both to one submission.
Target architecture: x64 or arm64. Defaults to x64. CI combines both
packages into the recommended multi-architecture Store submission bundle.

.PARAMETER Configuration
Build configuration. Release is the only accepted value: Store
Expand Down
106 changes: 106 additions & 0 deletions scripts/Build-StoreMsixBundle.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<#
.SYNOPSIS
Builds one unsigned multi-architecture Store MSIX bundle.
.DESCRIPTION
Combines the validated x64 and ARM64 Store packages without changing their
bytes. The bundle receives the same four-part package version so Partner
Center can ingest one architecture-selecting submission asset.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$X64Package,
[Parameter(Mandatory)][string]$Arm64Package,
[Parameter(Mandatory)][string]$PackageVersion,
[Parameter(Mandatory)][string]$OutputPath,
[string]$MakeAppxPath
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Resolve-MakeAppx {
if (-not [string]::IsNullOrWhiteSpace($MakeAppxPath)) {
return (Resolve-Path -LiteralPath $MakeAppxPath).Path
}

$command = Get-Command MakeAppx.exe -CommandType Application -ErrorAction SilentlyContinue
if ($null -ne $command) {
return $command.Source
}

$windowsKits = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
$candidate = Get-ChildItem -LiteralPath $windowsKits -Filter MakeAppx.exe -File -Recurse `
-ErrorAction SilentlyContinue |
Where-Object { $_.Directory.Name -eq 'x64' } |
Sort-Object FullName -Descending |
Select-Object -First 1
if ($null -eq $candidate) {
throw 'MakeAppx.exe was not found in PATH or the Windows 10 SDK.'
}

$candidate.FullName
}

foreach ($package in @($X64Package, $Arm64Package)) {
if (-not (Test-Path -LiteralPath $package -PathType Leaf)) {
throw "Required MSIX package was not found: $package"
}
if ([IO.Path]::GetExtension($package) -ine '.msix') {
throw "Bundle input must be an MSIX package: $package"
}
}

$segments = @($PackageVersion.Split('.'))
if ($segments.Count -ne 4) {
throw 'PackageVersion must contain four numeric components.'
}
foreach ($segment in $segments) {
[uint16]$value = 0
if (-not [uint16]::TryParse($segment, [ref]$value)) {
throw "Invalid MSIX bundle version component: $segment"
}
}
if ($segments[3] -ne '0') {
throw "Store MSIX bundle versions must end in .0: $PackageVersion"
}

$resolvedX64Package = (Resolve-Path -LiteralPath $X64Package).Path
$resolvedArm64Package = (Resolve-Path -LiteralPath $Arm64Package).Path
if ($resolvedX64Package -eq $resolvedArm64Package) {
throw 'The x64 and ARM64 bundle inputs must be different packages.'
}

$resolvedOutputPath = [IO.Path]::GetFullPath($OutputPath)
if ([IO.Path]::GetExtension($resolvedOutputPath) -ine '.msixbundle') {
throw 'OutputPath must use the .msixbundle extension.'
}
if (Test-Path -LiteralPath $resolvedOutputPath) {
throw "MSIX bundle output already exists: $resolvedOutputPath"
}

$outputDirectory = Split-Path $resolvedOutputPath -Parent
New-Item -Path $outputDirectory -ItemType Directory -Force | Out-Null
$workRoot = Join-Path ([IO.Path]::GetTempPath()) "openclaw-msixbundle-$([guid]::NewGuid().ToString('N'))"
$bundleInput = Join-Path $workRoot 'packages'
New-Item -Path $bundleInput -ItemType Directory -Force | Out-Null

try {
Copy-Item -LiteralPath $resolvedX64Package -Destination (Join-Path $bundleInput 'OpenClaw-x64.msix')
Copy-Item -LiteralPath $resolvedArm64Package -Destination (Join-Path $bundleInput 'OpenClaw-arm64.msix')

$resolvedMakeAppx = Resolve-MakeAppx
& $resolvedMakeAppx bundle /v /bv $PackageVersion /d $bundleInput /p $resolvedOutputPath
if ($LASTEXITCODE -ne 0) {
throw "MakeAppx.exe failed to build the MSIX bundle. Exit code: $LASTEXITCODE."
}
if (-not (Test-Path -LiteralPath $resolvedOutputPath -PathType Leaf)) {
throw 'MakeAppx.exe completed without producing the requested bundle.'
}

Write-Host "Unsigned Store MSIX bundle is ready: $resolvedOutputPath"
}
finally {
if ([IO.Directory]::Exists($workRoot)) {
[IO.Directory]::Delete($workRoot, $true)
}
}
Loading
Loading