















@@ -8,9 +8,12 @@ on:
88required: true
99type: string
1010windows_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
1216required: true
13-default: latest
1417type: string
15181619permissions:
@@ -31,53 +34,139 @@ jobs:
3134env:
3235RELEASE_TAG: ${{ inputs.tag }}
3336WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
37+EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
3438GH_TOKEN: ${{ github.token }}
3539run: |
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
4311144112 - name: Download Windows Hub release installers
45113shell: pwsh
46114env:
47115WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
116+EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
48117GH_TOKEN: ${{ github.token }}
49118run: |
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 }
6915470155 - name: Verify Authenticode signatures
71156shell: pwsh
72157run: |
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
@@ -88,7 +177,7 @@ jobs:
88177 - name: Write SHA-256 manifest
89178shell: pwsh
90179run: |
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:
101190RELEASE_TAG: ${{ inputs.tag }}
102191GH_TOKEN: ${{ github.token }}
103192run: |
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+ }
110268111269 - name: Summary
112270shell: pwsh
@@ -119,8 +277,9 @@ jobs:
119277120278 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
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。