mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 20:58:10 +00:00
* ci: gate stable releases on Windows companion assets * fix(release): reject malformed Windows checksum manifests * fix(release): make Windows recovery fail closed * fix(release): tighten Windows asset identity checks * fix(release): validate prepared candidate tarballs --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
286 lines
12 KiB
YAML
286 lines
12 KiB
YAML
name: Windows Node Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Existing OpenClaw release tag to receive Windows Hub installers, for example v2026.6.1
|
|
required: true
|
|
type: string
|
|
windows_node_tag:
|
|
description: Exact openclaw-windows-node release tag to promote, for example v0.6.3
|
|
required: true
|
|
type: string
|
|
expected_installer_digests:
|
|
description: Compact JSON map of installer asset names to pinned source sha256 digests
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
concurrency:
|
|
group: windows-node-release-${{ inputs.tag }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
promote_signed_windows_installers:
|
|
name: Promote signed Windows installers
|
|
runs-on: windows-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Validate inputs
|
|
shell: pwsh
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
|
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
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]*))?$') {
|
|
throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
|
|
}
|
|
$stableRelease = -not (
|
|
$env:RELEASE_TAG.Contains("-alpha.") -or
|
|
$env:RELEASE_TAG.Contains("-beta.")
|
|
)
|
|
if ($env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$') {
|
|
throw "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: $env:WINDOWS_NODE_TAG"
|
|
}
|
|
|
|
try {
|
|
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
|
|
} catch {
|
|
throw "expected_installer_digests must be a JSON object: $_"
|
|
}
|
|
# Add future signed installer names, such as MSIX x64/ARM64, here.
|
|
$requiredInstallerNames = @(
|
|
"OpenClawCompanion-Setup-x64.exe",
|
|
"OpenClawCompanion-Setup-arm64.exe"
|
|
)
|
|
$allowedTargetCompanionAssetNames = @(
|
|
$requiredInstallerNames
|
|
"OpenClawCompanion-SHA256SUMS.txt"
|
|
)
|
|
if ($expectedDigests.Count -ne $requiredInstallerNames.Count) {
|
|
throw "expected_installer_digests must contain exactly the current installer asset contract."
|
|
}
|
|
foreach ($name in $requiredInstallerNames) {
|
|
$digest = [string]$expectedDigests[$name]
|
|
if ($digest -notmatch '^sha256:[A-Fa-f0-9]{64}$') {
|
|
throw "expected_installer_digests is missing a valid pinned digest for $name."
|
|
}
|
|
}
|
|
|
|
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
|
|
if ($targetRelease.tagName -ne $env:RELEASE_TAG) {
|
|
throw "OpenClaw release tag mismatch: expected $env:RELEASE_TAG, got $($targetRelease.tagName)"
|
|
}
|
|
$unexpectedTargetCompanionAssets = @(
|
|
$targetRelease.assets |
|
|
Where-Object {
|
|
$_.name.StartsWith("OpenClawCompanion-") -and
|
|
$_.name -notin $allowedTargetCompanionAssetNames
|
|
} |
|
|
ForEach-Object name |
|
|
Sort-Object
|
|
)
|
|
if ($unexpectedTargetCompanionAssets.Count -ne 0) {
|
|
throw "Target OpenClaw release contains unexpected OpenClawCompanion assets before upload: $($unexpectedTargetCompanionAssets -join ', ')"
|
|
}
|
|
|
|
$sourceRelease = gh release view $env:WINDOWS_NODE_TAG --repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
|
|
if ($sourceRelease.tagName -ne $env:WINDOWS_NODE_TAG) {
|
|
throw "Windows source release tag mismatch: expected $env:WINDOWS_NODE_TAG, got $($sourceRelease.tagName)"
|
|
}
|
|
if ($sourceRelease.isDraft) {
|
|
throw "Windows source release must be published: $($sourceRelease.url)"
|
|
}
|
|
if ($stableRelease -and $sourceRelease.isPrerelease) {
|
|
throw "Stable OpenClaw releases require a non-prerelease Windows source release: $($sourceRelease.url)"
|
|
}
|
|
foreach ($name in $requiredInstallerNames) {
|
|
$sourceAssets = @($sourceRelease.assets | Where-Object name -eq $name)
|
|
if ($sourceAssets.Count -ne 1) {
|
|
throw "Windows source release must contain exactly one required asset $name; found $($sourceAssets.Count)."
|
|
}
|
|
if ([string]$sourceAssets[0].digest -ne [string]$expectedDigests[$name]) {
|
|
throw "Windows source release asset digest does not match the pinned digest: $name"
|
|
}
|
|
}
|
|
|
|
- name: Download Windows Hub release installers
|
|
shell: pwsh
|
|
env:
|
|
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
|
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
|
# Add future signed installer patterns, such as MSIX x64/ARM64, here.
|
|
# Every matched installer is signature-checked, checksummed, and promoted.
|
|
$installerPatterns = @(
|
|
"OpenClawCompanion-Setup-x64.exe",
|
|
"OpenClawCompanion-Setup-arm64.exe"
|
|
)
|
|
$downloadArgs = @(
|
|
$env:WINDOWS_NODE_TAG,
|
|
"--repo", "openclaw/openclaw-windows-node",
|
|
"--dir", "dist"
|
|
)
|
|
foreach ($pattern in $installerPatterns) {
|
|
$downloadArgs += @("--pattern", $pattern)
|
|
}
|
|
gh release download @downloadArgs
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to download Windows release assets from $env:WINDOWS_NODE_TAG."
|
|
}
|
|
|
|
foreach ($pattern in $installerPatterns) {
|
|
$patternMatches = @(Get-ChildItem -LiteralPath dist -File | Where-Object Name -Like $pattern)
|
|
if ($patternMatches.Count -ne 1) {
|
|
throw "Expected exactly one Windows installer matching '$pattern', found $($patternMatches.Count)."
|
|
}
|
|
}
|
|
|
|
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
|
|
foreach ($file in Get-ChildItem -LiteralPath dist -File) {
|
|
$expectedHash = ([string]$expectedDigests[$file.Name]) -replace '^sha256:', ''
|
|
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
|
|
if ($actualHash -ne $expectedHash) {
|
|
throw "Downloaded Windows source asset does not match pinned digest: $($file.Name)"
|
|
}
|
|
}
|
|
|
|
- name: Verify Authenticode signatures
|
|
shell: pwsh
|
|
run: |
|
|
$expectedSignerSubject = "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"
|
|
Get-ChildItem -LiteralPath dist -File | ForEach-Object {
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
|
if ($signature.Status -ne "Valid") {
|
|
throw "$($_.Name) Authenticode signature was $($signature.Status)."
|
|
}
|
|
if (-not $signature.SignerCertificate) {
|
|
throw "$($_.Name) has no signer certificate."
|
|
}
|
|
if ($signature.SignerCertificate.Subject -ne $expectedSignerSubject) {
|
|
throw "$($_.Name) has unexpected signer subject $($signature.SignerCertificate.Subject)."
|
|
}
|
|
[pscustomobject]@{
|
|
File = $_.Name
|
|
Signer = $signature.SignerCertificate.Subject
|
|
Thumbprint = $signature.SignerCertificate.Thumbprint
|
|
} | Format-List
|
|
}
|
|
|
|
- name: Write SHA-256 manifest
|
|
shell: pwsh
|
|
run: |
|
|
Get-ChildItem -LiteralPath dist -File |
|
|
Sort-Object Name |
|
|
ForEach-Object {
|
|
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
|
|
"$($hash.Hash.ToLowerInvariant()) $($_.Name)"
|
|
} | Set-Content -Encoding utf8NoBOM -Path dist/OpenClawCompanion-SHA256SUMS.txt
|
|
|
|
- name: Upload to OpenClaw release
|
|
shell: pwsh
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
$releaseAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name | ForEach-Object FullName)
|
|
gh release upload $env:RELEASE_TAG @releaseAssets --repo $env:GITHUB_REPOSITORY --clobber
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to upload Windows release assets to $env:RELEASE_TAG."
|
|
}
|
|
|
|
- name: Verify promoted release asset contract
|
|
shell: pwsh
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
New-Item -ItemType Directory -Force -Path verified | Out-Null
|
|
$expectedAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name)
|
|
$expectedCompanionAssetNames = @($expectedAssets | ForEach-Object Name | Sort-Object)
|
|
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets | ConvertFrom-Json
|
|
$actualCompanionAssetNames = @(
|
|
$targetRelease.assets |
|
|
Where-Object { $_.name.StartsWith("OpenClawCompanion-") } |
|
|
ForEach-Object name |
|
|
Sort-Object
|
|
)
|
|
$assetContractDiff = @(
|
|
Compare-Object `
|
|
-ReferenceObject $expectedCompanionAssetNames `
|
|
-DifferenceObject $actualCompanionAssetNames
|
|
)
|
|
if (
|
|
$actualCompanionAssetNames.Count -ne $expectedCompanionAssetNames.Count -or
|
|
$assetContractDiff.Count -ne 0
|
|
) {
|
|
throw "Promoted OpenClawCompanion asset names do not exactly match the current contract."
|
|
}
|
|
|
|
foreach ($asset in $expectedAssets) {
|
|
gh release download $env:RELEASE_TAG `
|
|
--repo $env:GITHUB_REPOSITORY `
|
|
--pattern $asset.Name `
|
|
--dir verified
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to download promoted Windows release asset $($asset.Name)."
|
|
}
|
|
}
|
|
|
|
$manifestPath = "verified/OpenClawCompanion-SHA256SUMS.txt"
|
|
$manifestEntries = @(Get-Content -LiteralPath $manifestPath | ForEach-Object {
|
|
if ($_ -notmatch '^([A-Fa-f0-9]{64}) ([^\\/]+)$') {
|
|
throw "Invalid Windows SHA-256 manifest entry: $_"
|
|
}
|
|
[PSCustomObject]@{
|
|
Hash = $Matches[1]
|
|
Name = $Matches[2]
|
|
}
|
|
})
|
|
$expectedInstallerNames = @(
|
|
$expectedAssets |
|
|
Where-Object Name -ne "OpenClawCompanion-SHA256SUMS.txt" |
|
|
ForEach-Object Name
|
|
)
|
|
$manifestInstallerNames = @($manifestEntries | ForEach-Object Name | Sort-Object)
|
|
$contractDiff = @(
|
|
Compare-Object `
|
|
-ReferenceObject $expectedInstallerNames `
|
|
-DifferenceObject $manifestInstallerNames
|
|
)
|
|
if ($contractDiff.Count -ne 0) {
|
|
throw "Promoted Windows SHA-256 manifest does not match the installer asset contract."
|
|
}
|
|
|
|
foreach ($entry in $manifestEntries) {
|
|
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath "verified/$($entry.Name)").Hash
|
|
if ($hash -ne $entry.Hash) {
|
|
throw "Promoted Windows release asset checksum mismatch: $($entry.Name)"
|
|
}
|
|
}
|
|
|
|
- name: Summary
|
|
shell: pwsh
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
|
run: |
|
|
@"
|
|
## Windows Hub installers promoted
|
|
|
|
OpenClaw release: $env:RELEASE_TAG
|
|
Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
|
|
"@ >> $env:GITHUB_STEP_SUMMARY
|
|
Get-ChildItem -LiteralPath dist -File |
|
|
Sort-Object Name |
|
|
ForEach-Object {
|
|
"- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/$($_.Name)"
|
|
} >> $env:GITHUB_STEP_SUMMARY
|