88 required : true
99 type : string
1010 windows_node_tag :
11- description : openclaw-windows-node release tag to promote, or latest
11+ description : Exact openclaw-windows-node release tag to promote, for example v0.6.3
12+ required : true
13+ type : string
14+ expected_installer_digests :
15+ description : Compact JSON map of installer asset names to pinned source sha256 digests
1216 required : true
13- default : latest
1417 type : string
1518
1619permissions :
@@ -31,53 +34,139 @@ jobs:
3134 env :
3235 RELEASE_TAG : ${{ inputs.tag }}
3336 WINDOWS_NODE_TAG : ${{ inputs.windows_node_tag }}
37+ EXPECTED_INSTALLER_DIGESTS : ${{ inputs.expected_installer_digests }}
3438 GH_TOKEN : ${{ github.token }}
3539 run : |
3640 if ($env:RELEASE_TAG -notmatch '^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$') {
3741 throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
3842 }
39- if ($env:WINDOWS_NODE_TAG -ne "latest" -and $env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
40- throw "Invalid openclaw-windows-node release tag: $env:WINDOWS_NODE_TAG"
43+ $stableRelease = -not (
44+ $env:RELEASE_TAG.Contains("-alpha.") -or
45+ $env:RELEASE_TAG.Contains("-beta.")
46+ )
47+ if ($env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$') {
48+ throw "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: $env:WINDOWS_NODE_TAG"
49+ }
50+
51+ try {
52+ $expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
53+ } catch {
54+ throw "expected_installer_digests must be a JSON object: $_"
55+ }
56+ # Add future signed installer names, such as MSIX x64/ARM64, here.
57+ $requiredInstallerNames = @(
58+ "OpenClawCompanion-Setup-x64.exe",
59+ "OpenClawCompanion-Setup-arm64.exe"
60+ )
61+ $allowedTargetCompanionAssetNames = @(
62+ $requiredInstallerNames
63+ "OpenClawCompanion-SHA256SUMS.txt"
64+ )
65+ if ($expectedDigests.Count -ne $requiredInstallerNames.Count) {
66+ throw "expected_installer_digests must contain exactly the current installer asset contract."
67+ }
68+ foreach ($name in $requiredInstallerNames) {
69+ $digest = [string]$expectedDigests[$name]
70+ if ($digest -notmatch '^sha256:[A-Fa-f0-9]{64}$') {
71+ throw "expected_installer_digests is missing a valid pinned digest for $name."
72+ }
73+ }
74+
75+ $targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
76+ if ($targetRelease.tagName -ne $env:RELEASE_TAG) {
77+ throw "OpenClaw release tag mismatch: expected $env:RELEASE_TAG, got $($targetRelease.tagName)"
78+ }
79+ $unexpectedTargetCompanionAssets = @(
80+ $targetRelease.assets |
81+ Where-Object {
82+ $_.name.StartsWith("OpenClawCompanion-") -and
83+ $_.name -notin $allowedTargetCompanionAssetNames
84+ } |
85+ ForEach-Object name |
86+ Sort-Object
87+ )
88+ if ($unexpectedTargetCompanionAssets.Count -ne 0) {
89+ throw "Target OpenClaw release contains unexpected OpenClawCompanion assets before upload: $($unexpectedTargetCompanionAssets -join ', ')"
90+ }
91+
92+ $sourceRelease = gh release view $env:WINDOWS_NODE_TAG --repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
93+ if ($sourceRelease.tagName -ne $env:WINDOWS_NODE_TAG) {
94+ throw "Windows source release tag mismatch: expected $env:WINDOWS_NODE_TAG, got $($sourceRelease.tagName)"
95+ }
96+ if ($sourceRelease.isDraft) {
97+ throw "Windows source release must be published: $($sourceRelease.url)"
98+ }
99+ if ($stableRelease -and $sourceRelease.isPrerelease) {
100+ throw "Stable OpenClaw releases require a non-prerelease Windows source release: $($sourceRelease.url)"
101+ }
102+ foreach ($name in $requiredInstallerNames) {
103+ $sourceAssets = @($sourceRelease.assets | Where-Object name -eq $name)
104+ if ($sourceAssets.Count -ne 1) {
105+ throw "Windows source release must contain exactly one required asset $name; found $($sourceAssets.Count)."
106+ }
107+ if ([string]$sourceAssets[0].digest -ne [string]$expectedDigests[$name]) {
108+ throw "Windows source release asset digest does not match the pinned digest: $name"
109+ }
41110 }
42- gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY | Out-Null
43111
44112 - name : Download Windows Hub release installers
45113 shell : pwsh
46114 env :
47115 WINDOWS_NODE_TAG : ${{ inputs.windows_node_tag }}
116+ EXPECTED_INSTALLER_DIGESTS : ${{ inputs.expected_installer_digests }}
48117 GH_TOKEN : ${{ github.token }}
49118 run : |
50119 New-Item -ItemType Directory -Force -Path dist | Out-Null
51- $tagArgs = @()
52- if ($env:WINDOWS_NODE_TAG -ne "latest") {
53- $tagArgs += $env:WINDOWS_NODE_TAG
54- }
55- gh release download @tagArgs `
56- --repo openclaw/openclaw-windows-node `
57- --pattern "OpenClawCompanion-Setup-*.exe" `
58- --dir dist
59-
60- $expected = @(
61- "dist/OpenClawCompanion-Setup-x64.exe",
62- "dist/OpenClawCompanion-Setup-arm64.exe"
120+ # Add future signed installer patterns, such as MSIX x64/ARM64, here.
121+ # Every matched installer is signature-checked, checksummed, and promoted.
122+ $installerPatterns = @(
123+ "OpenClawCompanion-Setup-x64.exe",
124+ "OpenClawCompanion-Setup-arm64.exe"
63125 )
64- foreach ($file in $expected) {
65- if (-not (Test-Path -LiteralPath $file)) {
66- throw "Missing expected Windows installer: $file"
126+ $downloadArgs = @(
127+ $env:WINDOWS_NODE_TAG,
128+ "--repo", "openclaw/openclaw-windows-node",
129+ "--dir", "dist"
130+ )
131+ foreach ($pattern in $installerPatterns) {
132+ $downloadArgs += @("--pattern", $pattern)
133+ }
134+ gh release download @downloadArgs
135+ if ($LASTEXITCODE -ne 0) {
136+ throw "Failed to download Windows release assets from $env:WINDOWS_NODE_TAG."
137+ }
138+
139+ foreach ($pattern in $installerPatterns) {
140+ $patternMatches = @(Get-ChildItem -LiteralPath dist -File | Where-Object Name -Like $pattern)
141+ if ($patternMatches.Count -ne 1) {
142+ throw "Expected exactly one Windows installer matching '$pattern', found $($patternMatches.Count)."
143+ }
144+ }
145+
146+ $expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
147+ foreach ($file in Get-ChildItem -LiteralPath dist -File) {
148+ $expectedHash = ([string]$expectedDigests[$file.Name]) -replace '^sha256:', ''
149+ $actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
150+ if ($actualHash -ne $expectedHash) {
151+ throw "Downloaded Windows source asset does not match pinned digest: $($file.Name)"
67152 }
68153 }
69154
70155 - name : Verify Authenticode signatures
71156 shell : pwsh
72157 run : |
73- Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" | ForEach-Object {
158+ $expectedSignerSubject = "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"
159+ Get-ChildItem -LiteralPath dist -File | ForEach-Object {
74160 $signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
75161 if ($signature.Status -ne "Valid") {
76162 throw "$($_.Name) Authenticode signature was $($signature.Status)."
77163 }
78164 if (-not $signature.SignerCertificate) {
79165 throw "$($_.Name) has no signer certificate."
80166 }
167+ if ($signature.SignerCertificate.Subject -ne $expectedSignerSubject) {
168+ throw "$($_.Name) has unexpected signer subject $($signature.SignerCertificate.Subject)."
169+ }
81170 [pscustomobject]@{
82171 File = $_.Name
83172 Signer = $signature.SignerCertificate.Subject
88177 - name : Write SHA-256 manifest
89178 shell : pwsh
90179 run : |
91- Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" |
180+ Get-ChildItem -LiteralPath dist -File |
92181 Sort-Object Name |
93182 ForEach-Object {
94183 $hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
@@ -101,12 +190,81 @@ jobs:
101190 RELEASE_TAG : ${{ inputs.tag }}
102191 GH_TOKEN : ${{ github.token }}
103192 run : |
104- gh release upload $env:RELEASE_TAG `
105- dist/OpenClawCompanion-Setup-x64.exe `
106- dist/OpenClawCompanion-Setup-arm64.exe `
107- dist/OpenClawCompanion-SHA256SUMS.txt `
108- --repo $env:GITHUB_REPOSITORY `
109- --clobber
193+ $releaseAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name | ForEach-Object FullName)
194+ gh release upload $env:RELEASE_TAG @releaseAssets --repo $env:GITHUB_REPOSITORY --clobber
195+ if ($LASTEXITCODE -ne 0) {
196+ throw "Failed to upload Windows release assets to $env:RELEASE_TAG."
197+ }
198+
199+ - name : Verify promoted release asset contract
200+ shell : pwsh
201+ env :
202+ RELEASE_TAG : ${{ inputs.tag }}
203+ GH_TOKEN : ${{ github.token }}
204+ run : |
205+ New-Item -ItemType Directory -Force -Path verified | Out-Null
206+ $expectedAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name)
207+ $expectedCompanionAssetNames = @($expectedAssets | ForEach-Object Name | Sort-Object)
208+ $targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets | ConvertFrom-Json
209+ $actualCompanionAssetNames = @(
210+ $targetRelease.assets |
211+ Where-Object { $_.name.StartsWith("OpenClawCompanion-") } |
212+ ForEach-Object name |
213+ Sort-Object
214+ )
215+ $assetContractDiff = @(
216+ Compare-Object `
217+ -ReferenceObject $expectedCompanionAssetNames `
218+ -DifferenceObject $actualCompanionAssetNames
219+ )
220+ if (
221+ $actualCompanionAssetNames.Count -ne $expectedCompanionAssetNames.Count -or
222+ $assetContractDiff.Count -ne 0
223+ ) {
224+ throw "Promoted OpenClawCompanion asset names do not exactly match the current contract."
225+ }
226+
227+ foreach ($asset in $expectedAssets) {
228+ gh release download $env:RELEASE_TAG `
229+ --repo $env:GITHUB_REPOSITORY `
230+ --pattern $asset.Name `
231+ --dir verified
232+ if ($LASTEXITCODE -ne 0) {
233+ throw "Failed to download promoted Windows release asset $($asset.Name)."
234+ }
235+ }
236+
237+ $manifestPath = "verified/OpenClawCompanion-SHA256SUMS.txt"
238+ $manifestEntries = @(Get-Content -LiteralPath $manifestPath | ForEach-Object {
239+ if ($_ -notmatch '^([A-Fa-f0-9]{64}) ([^\\/]+)$') {
240+ throw "Invalid Windows SHA-256 manifest entry: $_"
241+ }
242+ [PSCustomObject]@{
243+ Hash = $Matches[1]
244+ Name = $Matches[2]
245+ }
246+ })
247+ $expectedInstallerNames = @(
248+ $expectedAssets |
249+ Where-Object Name -ne "OpenClawCompanion-SHA256SUMS.txt" |
250+ ForEach-Object Name
251+ )
252+ $manifestInstallerNames = @($manifestEntries | ForEach-Object Name | Sort-Object)
253+ $contractDiff = @(
254+ Compare-Object `
255+ -ReferenceObject $expectedInstallerNames `
256+ -DifferenceObject $manifestInstallerNames
257+ )
258+ if ($contractDiff.Count -ne 0) {
259+ throw "Promoted Windows SHA-256 manifest does not match the installer asset contract."
260+ }
261+
262+ foreach ($entry in $manifestEntries) {
263+ $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath "verified/$($entry.Name)").Hash
264+ if ($hash -ne $entry.Hash) {
265+ throw "Promoted Windows release asset checksum mismatch: $($entry.Name)"
266+ }
267+ }
110268
111269 - name : Summary
112270 shell : pwsh
@@ -119,8 +277,9 @@ jobs:
119277
120278 OpenClaw release: $env:RELEASE_TAG
121279 Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
122-
123- - https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-x64.exe
124- - https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-arm64.exe
125- - https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-SHA256SUMS.txt
126280 "@ >> $env:GITHUB_STEP_SUMMARY
281+ Get-ChildItem -LiteralPath dist -File |
282+ Sort-Object Name |
283+ ForEach-Object {
284+ "- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/$($_.Name)"
285+ } >> $env:GITHUB_STEP_SUMMARY
0 commit comments