From b152cd55588e8cb30a44acc3099065ba10964027 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Fri, 11 Sep 2026 18:09:03 -0700 Subject: [PATCH 1/3] feat(ci): publish durable signed MSIX releases --- .github/copilot-instructions.md | 4 +- .github/workflows/gateway-msix.yml | 90 ++++++++++++++++++- README.md | 23 ++++- scripts/Build-MSIX.ps1 | 2 + scripts/Get-WorkflowPackageVersion.ps1 | 39 +++++++- scripts/Test-SigningInputs.Tests.ps1 | 27 +++++- scripts/Test-SigningInputs.ps1 | 10 +++ scripts/Test-WorkflowPackageVersion.Tests.ps1 | 34 +++++++ scripts/Test-WorkflowSigningConfiguration.ps1 | 6 ++ 9 files changed, 226 insertions(+), 9 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3790db31..a39f613c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -125,7 +125,9 @@ and ARM64 separately. MSIX creation, signing validation, workflow artifacts, and tests. - Keep the workflow's manual `openclaw_ref` default and automatic `env.OPENCLAW_REF` fallback identical. Official-release changes also update - the reviewed immutable commit in `release-policy.json`. + the reviewed immutable commit and stable or correction tag in + `release-policy.json`. The tag determines the four-part MSIX identity + version and the permanent GitHub Release tag. - The launcher is NativeAOT. `dotnet build` and the xUnit suite exercise a JIT build, so run the NativeAOT publish path when changing reflection, interop, or trimming-sensitive code. diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index 2aff1cb8..b17e02f4 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -259,10 +259,20 @@ jobs: - name: Compose unsigned MSIX shell: pwsh + env: + SIGNING_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode || 'unsigned' }} run: | + $versionParameters = @{ + RunNumber = '${{ github.run_number }}' + RunAttempt = '${{ github.run_attempt }}' + } + if ($env:SIGNING_MODE -eq 'official') { + $policy = Get-Content -LiteralPath .\release-policy.json -Raw | + ConvertFrom-Json + $versionParameters.ReleaseTag = [string]$policy.releaseTag + } $packageVersion = .\scripts\Get-WorkflowPackageVersion.ps1 ` - -RunNumber '${{ github.run_number }}' ` - -RunAttempt '${{ github.run_attempt }}' + @versionParameters .\scripts\Build-MSIX.ps1 ` -PayloadDirectory '${{ runner.temp }}\openclaw-payload' ` @@ -348,6 +358,10 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode == 'official' && github.ref == 'refs/heads/main' }} needs: build-msix runs-on: windows-latest + outputs: + package_version: ${{ steps.release.outputs.package_version }} + release_tag: ${{ steps.release.outputs.release_tag }} + release_version: ${{ steps.release.outputs.release_version }} permissions: actions: read contents: read @@ -369,6 +383,22 @@ jobs: name: openclaw-gateway-msix-unsigned-arm64 path: artifacts\arm64 + - name: Resolve official release metadata + id: release + shell: pwsh + run: | + $policy = Get-Content -LiteralPath .\release-policy.json -Raw | + ConvertFrom-Json + $releaseTag = ([string]$policy.releaseTag).Trim() + $packageVersion = .\scripts\Get-WorkflowPackageVersion.ps1 ` + -RunNumber '${{ github.run_number }}' ` + -RunAttempt '${{ github.run_attempt }}' ` + -ReleaseTag $releaseTag + + "package_version=$packageVersion" >> $env:GITHUB_OUTPUT + "release_tag=$releaseTag" >> $env:GITHUB_OUTPUT + "release_version=$($releaseTag.Substring(1))" >> $env:GITHUB_OUTPUT + - name: Enforce official signing policy shell: pwsh env: @@ -475,3 +505,59 @@ jobs: path: artifacts\arm64\ if-no-files-found: error retention-days: 7 + + publish-release: + name: Publish signed Gateway MSIX release + if: ${{ needs.sign-msix.result == 'success' }} + needs: + - authorize-signing + - sign-msix + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download signed x64 package + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-x64 + path: signed/x64 + + - name: Download signed ARM64 package + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-arm64 + path: signed/arm64 + + - name: Stage versioned release assets + shell: bash + env: + RELEASE_VERSION: ${{ needs.authorize-signing.outputs.release_version }} + run: | + set -euo pipefail + mkdir release-assets + cp signed/x64/OpenClawGateway-x64.msix \ + "release-assets/OpenClawGateway-${RELEASE_VERSION}-x64.msix" + cp signed/arm64/OpenClawGateway-arm64.msix \ + "release-assets/OpenClawGateway-${RELEASE_VERSION}-arm64.msix" + + - name: Create permanent GitHub release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ needs.authorize-signing.outputs.release_tag }} + target_commitish: ${{ github.sha }} + name: OpenClaw Gateway MSIX ${{ needs.authorize-signing.outputs.release_tag }} + generate_release_notes: true + prerelease: false + make_latest: true + overwrite_files: false + fail_on_unmatched_files: true + files: release-assets/*.msix + 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 }}). + + ### Downloads + - **x64:** `OpenClawGateway-${{ needs.authorize-signing.outputs.release_version }}-x64.msix` + - **ARM64:** `OpenClawGateway-${{ needs.authorize-signing.outputs.release_version }}-arm64.msix` + + The packages are signed by OpenClaw Foundation through Azure Artifact Signing. diff --git a/README.md b/README.md index b874ae82..9c1e3583 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,9 @@ 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 approved for -official signing. Updating that policy requires a reviewed repository change. -Official signing runs only from `main` and verifies the workflow input, both +official signing and its corresponding stable Gateway tag. Updating that +policy requires a reviewed repository change. Official signing runs only from +`main` and verifies the workflow input, release-derived package version, both architecture metadata files, both MSIX hashes, the embedded manifests, and every file against the embedded application inventory before requesting Azure credentials. @@ -162,7 +163,9 @@ validation. Manual runs support three signing modes: temporary self-signed certificate plus the public `.cer` needed for local installation; - `official` requires the approved immutable commit from - `release-policy.json` and may run only from `main`. + `release-policy.json`, may run only from `main`, and publishes the signed + packages as permanent assets on a GitHub Release named for the approved + Gateway tag. Official signing uses the protected `release-signing` environment, Azure OIDC, and the existing OpenClaw Artifact Signing account and certificate profile. @@ -170,6 +173,20 @@ 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 Gateway tag from `release-policy.json` for the +GitHub Release tag and MSIX identity version. Stable tags map directly to a +four-part Windows version, and numbered corrections use the fourth component: + +- `v2026.9.4` becomes `2026.9.4.0`; +- `v2026.7.1-2` becomes `2026.7.1.2`. + +Prerelease tags such as `v2026.9.1-beta.1` are not eligible for official MSIX +publication because mapping them directly would make Windows upgrade ordering +incorrect when the stable package is installed. The release contains durable +`OpenClawGateway--x64.msix` and +`OpenClawGateway--arm64.msix` assets. The duplicate GitHub Actions +artifacts remain short-lived transport and diagnostic copies. + ### Official signing setup The `release-signing` GitHub environment must define these environment diff --git a/scripts/Build-MSIX.ps1 b/scripts/Build-MSIX.ps1 index d690f864..6bab046d 100644 --- a/scripts/Build-MSIX.ps1 +++ b/scripts/Build-MSIX.ps1 @@ -166,6 +166,7 @@ if ( $payloadInfo.repository -ne 'https://github.com/openclaw/openclaw' -or $payloadInfo.architecture -ne $Architecture -or $payloadInfo.layout -ne 'expanded-directory' -or + [string]::IsNullOrWhiteSpace([string]$payloadInfo.packageVersion) -or $payloadInfo.requestedRef -isnot [string] -or [string]::IsNullOrWhiteSpace($payloadInfo.requestedRef) -or $payloadInfo.resolvedCommit -notmatch '^[0-9a-fA-F]{40}$' @@ -461,6 +462,7 @@ try { payloadRepository = $payloadInfo.repository payloadRequestedRef = $payloadInfo.requestedRef payloadResolvedCommit = $payloadInfo.resolvedCommit.ToLowerInvariant() + payloadPackageVersion = [string]$payloadInfo.packageVersion payloadLayout = 'immutable-package' payloadFileCount = $payloadFiles.Count architecture = $Architecture diff --git a/scripts/Get-WorkflowPackageVersion.ps1 b/scripts/Get-WorkflowPackageVersion.ps1 index 11376077..b961d452 100644 --- a/scripts/Get-WorkflowPackageVersion.ps1 +++ b/scripts/Get-WorkflowPackageVersion.ps1 @@ -4,7 +4,9 @@ param( [long]$RunNumber, [Parameter(Mandatory)] - [long]$RunAttempt + [long]$RunAttempt, + + [string]$ReleaseTag ) Set-StrictMode -Version Latest @@ -13,6 +15,41 @@ $ErrorActionPreference = 'Stop' $maximumComponent = 65534L $componentBase = $maximumComponent + 1L +if (-not [string]::IsNullOrWhiteSpace($ReleaseTag)) { + $releaseMatch = [regex]::Match( + $ReleaseTag.Trim(), + '^v(?\d+)\.(?\d+)\.(?\d+)(?:-(?[1-9]\d*))?$' + ) + if (-not $releaseMatch.Success) { + throw ( + "ReleaseTag '$ReleaseTag' must be a stable Gateway tag such as " + + 'v2026.9.4 or a correction tag such as v2026.7.1-2.' + ) + } + + $components = @( + [long]::Parse($releaseMatch.Groups['major'].Value), + [long]::Parse($releaseMatch.Groups['minor'].Value), + [long]::Parse($releaseMatch.Groups['build'].Value), + $(if ($releaseMatch.Groups['revision'].Success) { + [long]::Parse($releaseMatch.Groups['revision'].Value) + } + else { + 0L + }) + ) + foreach ($component in $components) { + if ($component -gt $maximumComponent) { + throw ( + "ReleaseTag '$ReleaseTag' contains a component greater than " + + "$maximumComponent." + ) + } + } + + return ($components -join '.') +} + if ($RunNumber -lt 1) { throw 'RunNumber must be greater than zero.' } diff --git a/scripts/Test-SigningInputs.Tests.ps1 b/scripts/Test-SigningInputs.Tests.ps1 index e31b3e01..b93895bb 100644 --- a/scripts/Test-SigningInputs.Tests.ps1 +++ b/scripts/Test-SigningInputs.Tests.ps1 @@ -8,6 +8,13 @@ $repositoryRoot = Split-Path $PSScriptRoot -Parent $policyPath = Join-Path $repositoryRoot 'release-policy.json' $policy = Get-Content -LiteralPath $policyPath -Raw | ConvertFrom-Json $approvedCommit = [string]$policy.approvedCommit +$approvedPackageVersion = & ( + Join-Path $PSScriptRoot 'Get-WorkflowPackageVersion.ps1' +) ` + -RunNumber 1 ` + -RunAttempt 1 ` + -ReleaseTag ([string]$policy.releaseTag) +$approvedPayloadVersion = ([string]$policy.releaseTag).Substring(1) -replace '-\d+$', '' $packagingCommit = '1111111111111111111111111111111111111111' $testRoot = Join-Path $env:TEMP ( "openclaw-signing-policy-$([guid]::NewGuid().ToString('N'))" @@ -24,6 +31,8 @@ function New-TestArtifact { [string]$PayloadCommit = $approvedCommit, + [string]$PayloadPackageVersion = $approvedPayloadVersion, + [bool]$SourceTreeDirty = $false, [bool]$IncludeBundledNode = $false, @@ -110,7 +119,7 @@ function New-TestArtifact { "@ | Set-Content ` @@ -141,13 +150,14 @@ function New-TestArtifact { payloadRepository = $policy.repository payloadRequestedRef = $PayloadCommit payloadResolvedCommit = $PayloadCommit + payloadPackageVersion = $PayloadPackageVersion payloadLayout = 'immutable-package' payloadFileCount = $payloadFiles.Count architecture = $Architecture archive = $msixName sha256 = $msixHash signed = $false - packageVersion = '0.1.1.0' + packageVersion = $approvedPackageVersion publisher = $policy.publisher } | ConvertTo-Json | @@ -275,6 +285,19 @@ try { Invoke-PolicyValidation -Root $testRoot } + Remove-Item -LiteralPath $testRoot -Recurse -Force + New-Item -Path $testRoot -ItemType Directory | Out-Null + New-TestArtifact ` + -Root $testRoot ` + -Architecture x64 ` + -PayloadPackageVersion '2026.9.4' + New-TestArtifact -Root $testRoot -Architecture arm64 + Assert-Fails ` + -MessagePattern 'metadata is not eligible' ` + -Action { + Invoke-PolicyValidation -Root $testRoot + } + Remove-Item -LiteralPath $testRoot -Recurse -Force New-Item -Path $testRoot -ItemType Directory | Out-Null New-TestArtifact -Root $testRoot -Architecture x64 diff --git a/scripts/Test-SigningInputs.ps1 b/scripts/Test-SigningInputs.ps1 index 9b9cd52a..64366646 100644 --- a/scripts/Test-SigningInputs.ps1 +++ b/scripts/Test-SigningInputs.ps1 @@ -117,12 +117,20 @@ $policy = Get-Content -LiteralPath $resolvedPolicyPath -Raw | if ( $policy.repository -ne 'https://github.com/openclaw/openclaw' -or + [string]::IsNullOrWhiteSpace([string]$policy.releaseTag) -or $policy.approvedCommit -notmatch '^[0-9a-fA-F]{40}$' -or [string]::IsNullOrWhiteSpace([string]$policy.publisher) ) { throw 'The Gateway MSIX release policy is invalid.' } +$approvedPackageVersion = & ( + Join-Path $PSScriptRoot 'Get-WorkflowPackageVersion.ps1' +) ` + -RunNumber 1 ` + -RunAttempt 1 ` + -ReleaseTag ([string]$policy.releaseTag) +$approvedPayloadVersion = ([string]$policy.releaseTag).Substring(1) -replace '-\d+$', '' $approvedCommit = ([string]$policy.approvedCommit).ToLowerInvariant() $normalizedRequestedRef = $RequestedRef.Trim().ToLowerInvariant() if ( @@ -163,6 +171,7 @@ foreach ($architecture in @('x64', 'arm64')) { $metadata.payloadRepository -ne $policy.repository -or $metadata.payloadRequestedRef -ine $approvedCommit -or $metadata.payloadResolvedCommit -ine $approvedCommit -or + $metadata.payloadPackageVersion -ne $approvedPayloadVersion -or $metadata.payloadLayout -ne 'immutable-package' -or $metadata.payloadFileCount -isnot [int64] -or $metadata.payloadFileCount -le 0 -or @@ -170,6 +179,7 @@ foreach ($architecture in @('x64', 'arm64')) { $metadata.archive -ne $msix.Name -or $metadata.sha256 -notmatch '^[0-9a-fA-F]{64}$' -or $metadata.signed -ne $false -or + $metadata.packageVersion -ne $approvedPackageVersion -or $metadata.publisher -ne $policy.publisher ) { throw "The $architecture MSIX metadata is not eligible for signing." diff --git a/scripts/Test-WorkflowPackageVersion.Tests.ps1 b/scripts/Test-WorkflowPackageVersion.Tests.ps1 index 36091276..1262a61d 100644 --- a/scripts/Test-WorkflowPackageVersion.Tests.ps1 +++ b/scripts/Test-WorkflowPackageVersion.Tests.ps1 @@ -54,6 +54,24 @@ function Assert-Fails { throw "Expected failure matching '$MessagePattern', but the action succeeded." } +function Assert-ReleaseVersion { + param( + [Parameter(Mandatory)] + [string]$ReleaseTag, + + [Parameter(Mandatory)] + [string]$Expected + ) + + $actual = & $scriptPath ` + -RunNumber 1 ` + -RunAttempt 1 ` + -ReleaseTag $ReleaseTag + if ($actual -ne $Expected) { + throw "Expected $ReleaseTag to produce $Expected; received $actual." + } +} + Assert-Version -RunNumber 1 -RunAttempt 1 -Expected '0.1.1.1' Assert-Version -RunNumber 65534 -RunAttempt 1 -Expected '0.1.65534.1' Assert-Version -RunNumber 65535 -RunAttempt 1 -Expected '0.2.0.1' @@ -63,6 +81,22 @@ Assert-Version -RunNumber 131069 -RunAttempt 1 -Expected '0.2.65534.1' Assert-Version -RunNumber 131070 -RunAttempt 1 -Expected '0.3.0.1' Assert-Version -RunNumber 1 -RunAttempt 65534 -Expected '0.1.1.65534' +Assert-ReleaseVersion -ReleaseTag 'v2026.9.4' -Expected '2026.9.4.0' +Assert-ReleaseVersion -ReleaseTag 'v2026.7.1-2' -Expected '2026.7.1.2' + +Assert-Fails -MessagePattern 'must be a stable Gateway tag' -Action { + & $scriptPath ` + -RunNumber 1 ` + -RunAttempt 1 ` + -ReleaseTag 'v2026.9.1-beta.1' +} +Assert-Fails -MessagePattern 'greater than 65534' -Action { + & $scriptPath ` + -RunNumber 1 ` + -RunAttempt 1 ` + -ReleaseTag 'v2026.9.4-65535' +} + $maximumRunNumber = (65534L * 65535L) - 1L Assert-Version ` -RunNumber $maximumRunNumber ` diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index b47146f9..a2a7fcd7 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -22,6 +22,12 @@ $requiredFragments = @( 'endpoint: https://eus.codesigning.azure.net/' 'signing-account-name: openclaw' 'certificate-profile-name: openclaw' + 'name: Publish signed Gateway MSIX release' + 'contents: write' + 'uses: softprops/action-gh-release@v3' + 'tag_name: ${{ needs.authorize-signing.outputs.release_tag }}' + 'overwrite_files: false' + 'fail_on_unmatched_files: true' ) foreach ($fragment in $requiredFragments) { From eaf67d6c8311bf7533c51bb21689e3d7b6fbc3d8 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Fri, 11 Sep 2026 18:43:42 -0700 Subject: [PATCH 2/3] feat(ci): publish multi-architecture MSIX bundle --- .github/copilot-instructions.md | 4 + .github/workflows/gateway-msix.yml | 117 +++++++++++++++- README.md | 16 ++- scripts/Build-MSIXBundle.ps1 | 126 ++++++++++++++++++ scripts/Test-Build-MSIXBundle.Tests.ps1 | 110 +++++++++++++++ scripts/Test-WorkflowSigningConfiguration.ps1 | 7 + 6 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 scripts/Build-MSIXBundle.ps1 create mode 100644 scripts/Test-Build-MSIXBundle.Tests.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a39f613c..851251ea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -120,6 +120,10 @@ and ARM64 separately. - Keep x64 and ARM64 behavior synchronized across the workflow matrix, scripts, project runtime identifiers, manifest content, payload metadata, and signing validation. +- Official releases combine the x64 and ARM64 packages into one signed + `.msixbundle` while retaining signed standalone packages for explicit + architecture-specific deployment. Compose the bundle before signing; bundle + signing recursively covers its contained packages. - Metadata files are part of the release trust chain, not incidental build output. Changes to their fields must be coordinated across payload creation, MSIX creation, signing validation, workflow artifacts, and tests. diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index b17e02f4..93cff8c5 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -94,6 +94,11 @@ jobs: run: > .\scripts\Test-WorkflowPackageVersion.Tests.ps1 + - name: Test MSIX bundle build + shell: pwsh + run: > + .\scripts\Test-Build-MSIXBundle.Tests.ps1 + - name: Test Git hooks shell: pwsh run: > @@ -340,6 +345,59 @@ jobs: if-no-files-found: error retention-days: 7 + build-msix-bundle: + name: Build unsigned multi-architecture Gateway MSIX bundle + needs: build-msix + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Download unsigned x64 package + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-unsigned-x64 + path: artifacts\x64 + + - name: Download unsigned ARM64 package + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-unsigned-arm64 + path: artifacts\arm64 + + - name: Compose unsigned multi-architecture MSIX bundle + shell: pwsh + env: + SIGNING_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode || 'unsigned' }} + run: | + $versionParameters = @{ + RunNumber = '${{ github.run_number }}' + RunAttempt = '${{ github.run_attempt }}' + } + if ($env:SIGNING_MODE -eq 'official') { + $policy = Get-Content -LiteralPath .\release-policy.json -Raw | + ConvertFrom-Json + $versionParameters.ReleaseTag = [string]$policy.releaseTag + } + $packageVersion = .\scripts\Get-WorkflowPackageVersion.ps1 ` + @versionParameters + + .\scripts\Build-MSIXBundle.ps1 ` + -X64Package artifacts\x64\OpenClawGateway-x64.msix ` + -Arm64Package artifacts\arm64\OpenClawGateway-arm64.msix ` + -PackageVersion $packageVersion ` + -OutputPath artifacts\bundle\OpenClawGateway.msixbundle + + - name: Upload unsigned multi-architecture MSIX bundle + uses: actions/upload-artifact@v7 + with: + name: openclaw-gateway-msix-unsigned-bundle + path: artifacts\bundle\OpenClawGateway.msixbundle + if-no-files-found: error + retention-days: 7 + 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' }} @@ -416,6 +474,7 @@ jobs: if: ${{ needs.authorize-signing.result == 'success' }} needs: - build-msix + - build-msix-bundle - authorize-signing runs-on: windows-latest environment: release-signing @@ -436,6 +495,12 @@ jobs: name: openclaw-gateway-msix-unsigned-arm64 path: artifacts\arm64 + - name: Download unsigned multi-architecture bundle + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-unsigned-bundle + path: artifacts\bundle + - name: Azure login uses: azure/login@v3 with: @@ -443,7 +508,7 @@ jobs: tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Sign MSIX packages + - name: Sign standalone MSIX packages uses: azure/artifact-signing-action@v2 with: endpoint: https://eus.codesigning.azure.net/ @@ -457,6 +522,17 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 + - name: Sign multi-architecture MSIX bundle + uses: azure/artifact-signing-action@v2 + with: + endpoint: https://eus.codesigning.azure.net/ + signing-account-name: openclaw + certificate-profile-name: openclaw + files: ${{ github.workspace }}\artifacts\bundle\OpenClawGateway.msixbundle + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + - name: Verify signatures and refresh metadata shell: pwsh run: | @@ -490,6 +566,24 @@ jobs: Set-Content -LiteralPath $metadataPath -Encoding utf8 } + $bundle = Get-Item ` + -LiteralPath artifacts\bundle\OpenClawGateway.msixbundle + $bundleSignature = Get-AuthenticodeSignature ` + -LiteralPath $bundle.FullName + if ($bundleSignature.Status -ne 'Valid') { + throw "$($bundle.Name) signature status was $($bundleSignature.Status)." + } + if (-not [string]::Equals( + $bundleSignature.SignerCertificate.Subject, + $expectedSubject, + [StringComparison]::OrdinalIgnoreCase + )) { + throw ( + "$($bundle.Name) signer was unexpected: " + + $bundleSignature.SignerCertificate.Subject + ) + } + - name: Upload signed x64 MSIX uses: actions/upload-artifact@v7 with: @@ -506,6 +600,14 @@ jobs: if-no-files-found: error retention-days: 7 + - name: Upload signed multi-architecture MSIX bundle + uses: actions/upload-artifact@v7 + with: + name: openclaw-gateway-msix-bundle + path: artifacts\bundle\OpenClawGateway.msixbundle + if-no-files-found: error + retention-days: 7 + publish-release: name: Publish signed Gateway MSIX release if: ${{ needs.sign-msix.result == 'success' }} @@ -528,6 +630,12 @@ jobs: name: openclaw-gateway-msix-arm64 path: signed/arm64 + - name: Download signed multi-architecture bundle + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-bundle + path: signed/bundle + - name: Stage versioned release assets shell: bash env: @@ -539,6 +647,8 @@ jobs: "release-assets/OpenClawGateway-${RELEASE_VERSION}-x64.msix" cp signed/arm64/OpenClawGateway-arm64.msix \ "release-assets/OpenClawGateway-${RELEASE_VERSION}-arm64.msix" + cp signed/bundle/OpenClawGateway.msixbundle \ + "release-assets/OpenClawGateway-${RELEASE_VERSION}.msixbundle" - name: Create permanent GitHub release uses: softprops/action-gh-release@v3 @@ -551,12 +661,15 @@ jobs: make_latest: true overwrite_files: false fail_on_unmatched_files: true - files: release-assets/*.msix + files: | + 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 }}). ### Downloads + - **Recommended:** `OpenClawGateway-${{ needs.authorize-signing.outputs.release_version }}.msixbundle` - **x64:** `OpenClawGateway-${{ needs.authorize-signing.outputs.release_version }}-x64.msix` - **ARM64:** `OpenClawGateway-${{ needs.authorize-signing.outputs.release_version }}-arm64.msix` diff --git a/README.md b/README.md index 9c1e3583..7616e133 100644 --- a/README.md +++ b/README.md @@ -182,10 +182,20 @@ four-part Windows version, and numbered corrections use the fourth component: Prerelease tags such as `v2026.9.1-beta.1` are not eligible for official MSIX publication because mapping them directly would make Windows upgrade ordering -incorrect when the stable package is installed. The release contains durable +incorrect when the stable package is installed. The workflow creates the +Gateway tag in this repository and a GitHub Release with generated release +notes. Each release contains a signed, multi-architecture +`OpenClawGateway-.msixbundle` as the recommended download, plus signed `OpenClawGateway--x64.msix` and -`OpenClawGateway--arm64.msix` assets. The duplicate GitHub Actions -artifacts remain short-lived transport and diagnostic copies. +`OpenClawGateway--arm64.msix` packages for architecture-specific +deployment. The duplicate GitHub Actions artifacts remain short-lived transport +and diagnostic copies. + +An `.msixbundle` is a single installable container for the x64 and ARM64 MSIX +packages; Windows selects the package appropriate for the device. An +`.appinstaller` file is separate update-channel metadata rather than an +alternative package format. This repository does not publish one yet, so GitHub +Release installs do not opt devices into automatic update checks. ### Official signing setup diff --git a/scripts/Build-MSIXBundle.ps1 b/scripts/Build-MSIXBundle.ps1 new file mode 100644 index 00000000..a1a9727a --- /dev/null +++ b/scripts/Build-MSIXBundle.ps1 @@ -0,0 +1,126 @@ +[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" + } +} + +$version = $null +if (-not [version]::TryParse($PackageVersion, [ref]$version)) { + throw "PackageVersion is not a valid four-part version: $PackageVersion" +} +$versionSegments = @($PackageVersion.Split('.')) +if ($versionSegments.Count -ne 4) { + throw 'PackageVersion must contain four numeric components.' +} +foreach ($segment in $versionSegments) { + [uint16]$value = 0 + if (-not [uint16]::TryParse($segment, [ref]$value) -or $value -gt 65534) { + throw "Invalid MSIX bundle version component: $segment" + } +} + +$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 'OpenClawGateway-x64.msix') + Copy-Item ` + -LiteralPath $resolvedArm64Package ` + -Destination (Join-Path $bundleInput 'OpenClawGateway-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 MSIX bundle is ready: $resolvedOutputPath" +} +finally { + if ([IO.Directory]::Exists($workRoot)) { + [IO.Directory]::Delete($workRoot, $true) + } +} diff --git a/scripts/Test-Build-MSIXBundle.Tests.ps1 b/scripts/Test-Build-MSIXBundle.Tests.ps1 new file mode 100644 index 00000000..4cc51698 --- /dev/null +++ b/scripts/Test-Build-MSIXBundle.Tests.ps1 @@ -0,0 +1,110 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptPath = Join-Path $PSScriptRoot 'Build-MSIXBundle.ps1' +$testRoot = Join-Path $env:TEMP ` + "openclaw-bundle-tests-$([guid]::NewGuid().ToString('N'))" +New-Item -Path $testRoot -ItemType Directory -Force | Out-Null + +function Assert-Fails { + param( + [Parameter(Mandatory)] + [scriptblock]$Action, + + [Parameter(Mandatory)] + [string]$MessagePattern + ) + + try { + & $Action + } + catch { + if ($_.Exception.Message -notmatch $MessagePattern) { + throw ( + "Expected failure matching '$MessagePattern'; received: " + + $_.Exception.Message + ) + } + return + } + + throw "Expected failure matching '$MessagePattern', but the action succeeded." +} + +try { + $x64Package = Join-Path $testRoot 'x64.msix' + $arm64Package = Join-Path $testRoot 'arm64.msix' + Set-Content -LiteralPath $x64Package -Value 'x64' -Encoding ascii + Set-Content -LiteralPath $arm64Package -Value 'arm64' -Encoding ascii + + $fakeMakeAppx = Join-Path $testRoot 'MakeAppx.cmd' + $makeAppxArguments = Join-Path $testRoot 'makeappx-arguments.txt' + $env:OPENCLAW_BUNDLE_TEST_ARGUMENTS = $makeAppxArguments + @' +@echo off +echo %* > "%OPENCLAW_BUNDLE_TEST_ARGUMENTS%" +set output= +:parse +if "%~1"=="" goto done +if /I "%~1"=="/p" ( + set output=%~2 + shift +) +shift +goto parse +:done +if "%output%"=="" exit /b 2 +type nul > "%output%" +'@ | Set-Content -LiteralPath $fakeMakeAppx -Encoding ascii + + $bundle = Join-Path $testRoot 'OpenClawGateway.msixbundle' + & $scriptPath ` + -X64Package $x64Package ` + -Arm64Package $arm64Package ` + -PackageVersion '2026.9.4.0' ` + -OutputPath $bundle ` + -MakeAppxPath $fakeMakeAppx + if (-not (Test-Path -LiteralPath $bundle -PathType Leaf)) { + throw 'The bundle builder did not preserve the MakeAppx output.' + } + $arguments = Get-Content -LiteralPath $makeAppxArguments -Raw + if ($arguments -notmatch '/bv 2026\.9\.4\.0') { + throw "MakeAppx did not receive the expected bundle version: $arguments" + } + + Assert-Fails -MessagePattern 'must be different' -Action { + & $scriptPath ` + -X64Package $x64Package ` + -Arm64Package $x64Package ` + -PackageVersion '2026.9.4.0' ` + -OutputPath (Join-Path $testRoot 'duplicate.msixbundle') ` + -MakeAppxPath $fakeMakeAppx + } + Assert-Fails -MessagePattern 'four numeric components' -Action { + & $scriptPath ` + -X64Package $x64Package ` + -Arm64Package $arm64Package ` + -PackageVersion '2026.9.4' ` + -OutputPath (Join-Path $testRoot 'bad-version.msixbundle') ` + -MakeAppxPath $fakeMakeAppx + } + Assert-Fails -MessagePattern 'already exists' -Action { + & $scriptPath ` + -X64Package $x64Package ` + -Arm64Package $arm64Package ` + -PackageVersion '2026.9.4.0' ` + -OutputPath $bundle ` + -MakeAppxPath $fakeMakeAppx + } + + Write-Host 'MSIX bundle build tests passed.' +} +finally { + Remove-Item Env:OPENCLAW_BUNDLE_TEST_ARGUMENTS -ErrorAction SilentlyContinue + if ([IO.Directory]::Exists($testRoot)) { + [IO.Directory]::Delete($testRoot, $true) + } +} diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index a2a7fcd7..0551438f 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -18,7 +18,11 @@ $requiredFragments = @( 'tenant-id: ${{ vars.AZURE_TENANT_ID }}' 'subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}' 'uses: azure/artifact-signing-action@v2' + 'name: Compose unsigned multi-architecture MSIX bundle' + 'name: Upload unsigned multi-architecture MSIX bundle' 'files-folder-recurse: true' + 'files: ${{ github.workspace }}\artifacts\bundle\OpenClawGateway.msixbundle' + 'name: Upload signed multi-architecture MSIX bundle' 'endpoint: https://eus.codesigning.azure.net/' 'signing-account-name: openclaw' 'certificate-profile-name: openclaw' @@ -26,8 +30,11 @@ $requiredFragments = @( 'contents: write' 'uses: softprops/action-gh-release@v3' 'tag_name: ${{ needs.authorize-signing.outputs.release_tag }}' + 'target_commitish: ${{ github.sha }}' + 'generate_release_notes: true' 'overwrite_files: false' 'fail_on_unmatched_files: true' + 'release-assets/*.msixbundle' ) foreach ($fragment in $requiredFragments) { From 546bc513395343753d94f745a3089ee46d77ecc7 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Fri, 11 Sep 2026 18:50:45 -0700 Subject: [PATCH 3/3] fix(ci): authorize exact MSIX bundle contents --- .github/workflows/gateway-msix.yml | 11 ++- README.md | 5 +- scripts/Test-SigningInputs.Tests.ps1 | 87 ++++++++++++++++++- scripts/Test-SigningInputs.ps1 | 86 ++++++++++++++++++ scripts/Test-WorkflowSigningConfiguration.ps1 | 1 + 5 files changed, 186 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gateway-msix.yml b/.github/workflows/gateway-msix.yml index 93cff8c5..3701ae89 100644 --- a/.github/workflows/gateway-msix.yml +++ b/.github/workflows/gateway-msix.yml @@ -414,7 +414,9 @@ jobs: authorize-signing: name: Authorize official Gateway MSIX signing if: ${{ github.event_name == 'workflow_dispatch' && inputs.signing_mode == 'official' && github.ref == 'refs/heads/main' }} - needs: build-msix + needs: + - build-msix + - build-msix-bundle runs-on: windows-latest outputs: package_version: ${{ steps.release.outputs.package_version }} @@ -441,6 +443,12 @@ jobs: name: openclaw-gateway-msix-unsigned-arm64 path: artifacts\arm64 + - name: Download unsigned multi-architecture bundle + uses: actions/download-artifact@v8 + with: + name: openclaw-gateway-msix-unsigned-bundle + path: artifacts\bundle + - name: Resolve official release metadata id: release shell: pwsh @@ -466,6 +474,7 @@ jobs: .\scripts\Test-SigningInputs.ps1 ` -ArtifactsDirectory artifacts ` -PolicyPath .\release-policy.json ` + -BundlePath artifacts\bundle\OpenClawGateway.msixbundle ` -RequestedRef $env:OPENCLAW_REF ` -PackagingCommit $env:PACKAGING_COMMIT diff --git a/README.md b/README.md index 7616e133..e602ac33 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,9 @@ official signing and its corresponding stable Gateway tag. Updating that policy requires a reviewed repository change. Official signing runs only from `main` and verifies the workflow input, release-derived package version, both architecture metadata files, both MSIX hashes, the embedded manifests, and -every file against the embedded application inventory before requesting Azure -credentials. +every file against the embedded application inventory. It also byte-compares +the bundle's embedded packages with those authorized standalone packages before +requesting Azure credentials. ## Build and test diff --git a/scripts/Test-SigningInputs.Tests.ps1 b/scripts/Test-SigningInputs.Tests.ps1 index b93895bb..03077a36 100644 --- a/scripts/Test-SigningInputs.Tests.ps1 +++ b/scripts/Test-SigningInputs.Tests.ps1 @@ -171,16 +171,86 @@ function Invoke-PolicyValidation { [Parameter(Mandatory)] [string]$Root, - [string]$RequestedRef = $approvedCommit + [string]$RequestedRef = $approvedCommit, + + [switch]$PreserveBundle ) + if (-not $PreserveBundle) { + New-TestBundle -Root $Root + } + & (Join-Path $PSScriptRoot 'Test-SigningInputs.ps1') ` -ArtifactsDirectory $Root ` -PolicyPath $policyPath ` + -BundlePath (Join-Path $Root 'bundle\OpenClawGateway.msixbundle') ` -RequestedRef $RequestedRef ` -PackagingCommit $packagingCommit } +function New-TestBundle { + param( + [Parameter(Mandatory)] + [string]$Root, + + [string]$X64Package = (Join-Path ` + $Root ` + 'x64\OpenClawGateway-x64.msix') + ) + + $bundleDirectory = Join-Path $Root 'bundle' + $bundleStaging = Join-Path $Root '.bundle-package' + Remove-Item ` + -LiteralPath $bundleDirectory, $bundleStaging ` + -Recurse ` + -Force ` + -ErrorAction SilentlyContinue + $bundleMetadata = Join-Path $bundleStaging 'AppxMetadata' + New-Item ` + -Path $bundleDirectory, $bundleMetadata ` + -ItemType Directory ` + -Force | + Out-Null + + Copy-Item ` + -LiteralPath $X64Package ` + -Destination (Join-Path $bundleStaging 'OpenClawGateway-x64.msix') + Copy-Item ` + -LiteralPath (Join-Path ` + $Root ` + 'arm64\OpenClawGateway-arm64.msix') ` + -Destination (Join-Path $bundleStaging 'OpenClawGateway-arm64.msix') + + @" + + + + + + + + +"@ | Set-Content ` + -LiteralPath (Join-Path ` + $bundleMetadata ` + 'AppxBundleManifest.xml') ` + -Encoding utf8 + + [IO.Compression.ZipFile]::CreateFromDirectory( + $bundleStaging, + (Join-Path $bundleDirectory 'OpenClawGateway.msixbundle') + ) + Remove-Item -LiteralPath $bundleStaging -Recurse -Force +} + function Assert-Fails { param( [Parameter(Mandatory)] @@ -475,6 +545,21 @@ try { Invoke-PolicyValidation -Root $testRoot } + Reset-TestArtifacts + $substitutedX64 = Join-Path $testRoot 'substituted-x64.msix' + Copy-Item ` + -LiteralPath (Join-Path ` + $testRoot ` + 'x64\OpenClawGateway-x64.msix') ` + -Destination $substitutedX64 + Add-Content -LiteralPath $substitutedX64 -Value 'substituted' + New-TestBundle -Root $testRoot -X64Package $substitutedX64 + Assert-Fails ` + -MessagePattern 'does not match the authorized standalone package' ` + -Action { + Invoke-PolicyValidation -Root $testRoot -PreserveBundle + } + Write-Host 'Gateway MSIX signing policy tests passed.' } finally { diff --git a/scripts/Test-SigningInputs.ps1 b/scripts/Test-SigningInputs.ps1 index 64366646..96863ef6 100644 --- a/scripts/Test-SigningInputs.ps1 +++ b/scripts/Test-SigningInputs.ps1 @@ -6,6 +6,9 @@ param( [Parameter(Mandatory)] [string]$PolicyPath, + [Parameter(Mandatory)] + [string]$BundlePath, + [Parameter(Mandatory)] [string]$RequestedRef, @@ -145,6 +148,7 @@ if ( $expectedPackagingCommit = $PackagingCommit.ToLowerInvariant() $expectedPackageVersion = $null +$expectedPackages = @{} foreach ($architecture in @('x64', 'arm64')) { $directory = Join-Path $resolvedArtifactsDirectory $architecture $metadataPath = Join-Path $directory 'msix-metadata.json' @@ -198,6 +202,11 @@ foreach ($architecture in @('x64', 'arm64')) { if ($actualMsixHash -ne ([string]$metadata.sha256).ToLowerInvariant()) { throw "The $architecture MSIX hash does not match its metadata." } + $expectedPackages[$architecture] = @{ + Name = $msix.Name + Path = $msix.FullName + Sha256 = $actualMsixHash + } $packageArchive = [IO.Compression.ZipFile]::OpenRead($msix.FullName) try { @@ -331,6 +340,83 @@ foreach ($architecture in @('x64', 'arm64')) { } } +$resolvedBundlePath = (Resolve-Path -LiteralPath $BundlePath).Path +if ([IO.Path]::GetExtension($resolvedBundlePath) -ine '.msixbundle') { + throw 'The official signing bundle must use the .msixbundle extension.' +} + +$bundleArchive = [IO.Compression.ZipFile]::OpenRead($resolvedBundlePath) +try { + $bundleEntries = New-PackageEntryIndex -Archive $bundleArchive + [xml]$bundleManifest = Read-ZipEntryText ` + -EntriesByPath $bundleEntries ` + -Path 'AppxMetadata/AppxBundleManifest.xml' + $bundleIdentity = $bundleManifest.SelectSingleNode( + "/*[local-name()='Bundle']/*[local-name()='Identity']" + ) + if ( + $null -eq $bundleIdentity -or + $bundleIdentity.Name -ne 'OpenClaw.Gateway' -or + $bundleIdentity.Publisher -ne $policy.publisher -or + $bundleIdentity.Version -ne $expectedPackageVersion + ) { + throw 'The MSIX bundle manifest identity is unexpected.' + } + + $bundlePackages = @( + $bundleManifest.SelectNodes( + "/*[local-name()='Bundle']/*[local-name()='Packages']/*[local-name()='Package']" + ) + ) + if ($bundlePackages.Count -ne 2) { + throw 'The MSIX bundle must contain exactly two application packages.' + } + + $seenArchitectures = + [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::OrdinalIgnoreCase + ) + foreach ($bundlePackage in $bundlePackages) { + $architecture = [string]$bundlePackage.Architecture + if ( + -not $expectedPackages.ContainsKey($architecture) -or + -not $seenArchitectures.Add($architecture) -or + $bundlePackage.Type -ne 'application' -or + $bundlePackage.Version -ne $expectedPackageVersion + ) { + throw 'The MSIX bundle package manifest is unexpected.' + } + + $expectedPackage = $expectedPackages[$architecture] + $fileName = [string]$bundlePackage.FileName + if ($fileName -ne $expectedPackage.Name) { + throw "The bundled $architecture MSIX filename is unexpected." + } + + $bundleEntry = Get-PackageEntry ` + -EntriesByPath $bundleEntries ` + -Path $fileName + $bundledHash = Get-PackageEntrySha256 -Entry $bundleEntry + if ($bundledHash -ne $expectedPackage.Sha256) { + throw ( + "The bundled $architecture MSIX does not match the " + + 'authorized standalone package.' + ) + } + } + + $embeddedMsixEntries = @( + $bundleEntries.Keys | + Where-Object { [IO.Path]::GetExtension($_) -ieq '.msix' } + ) + if ($embeddedMsixEntries.Count -ne 2) { + throw 'The MSIX bundle contains an unexpected package file set.' + } +} +finally { + $bundleArchive.Dispose() +} + Write-Host ( "Authorized official signing for OpenClaw commit $approvedCommit " + "and Gateway MSIX version $expectedPackageVersion." diff --git a/scripts/Test-WorkflowSigningConfiguration.ps1 b/scripts/Test-WorkflowSigningConfiguration.ps1 index 0551438f..d1f553d4 100644 --- a/scripts/Test-WorkflowSigningConfiguration.ps1 +++ b/scripts/Test-WorkflowSigningConfiguration.ps1 @@ -20,6 +20,7 @@ $requiredFragments = @( 'uses: azure/artifact-signing-action@v2' 'name: Compose unsigned multi-architecture MSIX bundle' 'name: Upload unsigned multi-architecture MSIX bundle' + '-BundlePath artifacts\bundle\OpenClawGateway.msixbundle' 'files-folder-recurse: true' 'files: ${{ github.workspace }}\artifacts\bundle\OpenClawGateway.msixbundle' 'name: Upload signed multi-architecture MSIX bundle'