Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

371 lines
15 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[ValidateSet("win_x64")]
[string]$ReleaseTarget,
[Parameter(Mandatory = $true)]
[string]$ReleaseNamespace,
[Parameter(Mandatory = $true)]
[string]$ReleaseVersion,
[Parameter(Mandatory = $true)]
[ValidateSet("skip", "core", "full")]
[string]$SmokeMode,
[Parameter(Mandatory = $true)]
[ValidateSet("all", "dir", "nsis", "zip")]
[string]$BuildTarget,
[Parameter(Mandatory = $true)]
[ValidateSet("off", "on")]
[string]$SignMode,
[Parameter(Mandatory = $true)]
[string]$WorkRoot,
[Parameter(Mandatory = $true)]
[string]$ToolsPackDir,
[Parameter(Mandatory = $true)]
[string]$CacheDir,
[Parameter(Mandatory = $true)]
[string]$BuildJsonPath,
[Parameter(Mandatory = $true)]
[string]$IndexPath,
[Parameter(Mandatory = $true)]
[string]$ReportRoot,
[Parameter(Mandatory = $true)]
[string]$OutputsPath
)
$ErrorActionPreference = "Stop"
$startedAt = Get-Date
$timings = @()
$failureMessage = $null
$ReleaseChannel = [string]$env:RELEASE_CHANNEL
if ([string]::IsNullOrWhiteSpace($ReleaseChannel)) {
throw "RELEASE_CHANNEL is required"
}
function Format-Duration([int64]$Milliseconds) {
if ($Milliseconds -ge 60000) {
return "$([Math]::Round($Milliseconds / 60000, 1))m"
}
return "$([Math]::Round($Milliseconds / 1000, 1))s"
}
function Measure-Step([string]$Name, [scriptblock]$Script) {
Write-Host "##[group]$Name"
$started = Get-Date
try {
$result = & $Script
$durationMs = [int64]((Get-Date) - $started).TotalMilliseconds
$script:timings += [ordered]@{ step = $Name; status = "success"; durationMs = $durationMs }
Write-Host "[$Name] success in $(Format-Duration $durationMs)"
return $result
} catch {
$durationMs = [int64]((Get-Date) - $started).TotalMilliseconds
$script:timings += [ordered]@{ step = $Name; status = "failed"; durationMs = $durationMs; error = $_.Exception.Message }
$script:failureMessage = $_.Exception.Message
Write-Host "[$Name] failed in $(Format-Duration $durationMs)"
throw
} finally {
Write-Host "##[endgroup]"
}
}
function Write-JsonFile([string]$Path, [object]$Value, [int]$Depth = 10) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Path) | Out-Null
$Value | ConvertTo-Json -Depth $Depth | Set-Content -LiteralPath $Path -Encoding utf8
}
function Read-BuildJson {
if (-not (Test-Path -LiteralPath $BuildJsonPath)) {
return $null
}
return Get-Content -LiteralPath $BuildJsonPath -Raw -Encoding utf8 | ConvertFrom-Json
}
function Get-SmokeSummary {
$summaryJsonPath = Join-Path $ReportRoot "summary.json"
if (-not (Test-Path -LiteralPath $summaryJsonPath)) {
return $null
}
return Get-Content -LiteralPath $summaryJsonPath -Raw -Encoding utf8 | ConvertFrom-Json
}
function Write-Index([string]$Status) {
$durationMs = [int64]((Get-Date) - $startedAt).TotalMilliseconds
$build = Read-BuildJson
$artifacts = $null
if ($build -ne $null) {
$artifacts = [ordered]@{
installerPath = $build.installerPath
latestYmlPath = $build.latestYmlPath
outputRoot = $build.outputRoot
payloadPath = $build.payloadPath
portableZipPath = $build.portableZipPath
}
}
$index = [ordered]@{
artifacts = $artifacts
branch = $env:RELEASE_BRANCH
buildJsonPath = $BuildJsonPath
cacheDir = $CacheDir
channel = $ReleaseChannel
commit = $env:RELEASE_COMMIT
durationMs = $durationMs
failure = $script:failureMessage
generatedAt = (Get-Date).ToUniversalTime().ToString("o")
namespace = $ReleaseNamespace
platform = "win"
releaseTarget = $ReleaseTarget
releaseVersion = $ReleaseVersion
reportDir = $ReportRoot
signed = $SignMode -eq "on"
smoke = Get-SmokeSummary
smokeMode = $SmokeMode
status = $Status
target = $BuildTarget
timings = $script:timings
toolsPackDir = $ToolsPackDir
}
Write-JsonFile -Path $IndexPath -Value $index -Depth 10
Write-JsonFile -Path $OutputsPath -Value ([ordered]@{
build_json_path = $BuildJsonPath
index_path = $IndexPath
release_target = $ReleaseTarget
release_version = $ReleaseVersion
}) -Depth 5
}
function Invoke-CommandChecked([string[]]$Arguments, [string]$WorkingDirectory = (Get-Location).Path) {
Push-Location -LiteralPath $WorkingDirectory
try {
& $Arguments[0] @($Arguments | Select-Object -Skip 1)
if ($LASTEXITCODE -ne 0) {
throw "command failed with exit code ${LASTEXITCODE}: $($Arguments -join ' ')"
}
} finally {
Pop-Location
}
}
function Convert-ArchiveRelativePath([string]$Value) {
if ([string]::IsNullOrWhiteSpace($Value)) {
throw "archive relative path must not be empty"
}
if ([System.IO.Path]::IsPathRooted($Value) -or $Value.Contains("..")) {
throw "unsafe archive relative path: $Value"
}
return $Value.Replace("/", [System.IO.Path]::DirectorySeparatorChar).Replace("\", [System.IO.Path]::DirectorySeparatorChar)
}
function Test-JsonString([object]$Value, [string]$Name, [string]$Expected) {
$actual = [string]$Value
if ($actual -ne $Expected) {
throw "launcher payload manifest $Name expected '$Expected' but got '$actual'"
}
}
function Validate-WinLauncherPayloadArchive([string]$PayloadPath, [string]$ExpectedVersion, [string]$Label) {
if ([string]::IsNullOrWhiteSpace($PayloadPath) -or -not (Test-Path -LiteralPath $PayloadPath)) {
throw "expected launcher payload path for $Label not found at $PayloadPath"
}
$sevenZipExe = Join-Path (Get-Location).Path "tools\pack\resources\win\7zip\7z.exe"
if (-not (Test-Path -LiteralPath $sevenZipExe)) {
throw "bundled 7z.exe not found at $sevenZipExe"
}
$extractRoot = Join-Path $WorkRoot "validate-launcher-payload-$Label"
Remove-Item -LiteralPath $extractRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
try {
& $sevenZipExe x $PayloadPath "-o$extractRoot" -y | Write-Host
if ($LASTEXITCODE -ne 0) {
throw "launcher payload extraction failed with exit code $LASTEXITCODE for $PayloadPath"
}
$manifestPath = Join-Path $extractRoot "manifest.json"
if (-not (Test-Path -LiteralPath $manifestPath)) {
throw "launcher payload manifest not found after extraction: $manifestPath"
}
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding utf8 | ConvertFrom-Json
Test-JsonString $manifest.schemaVersion "schemaVersion" "1"
Test-JsonString $manifest.channel "channel" $ReleaseChannel
Test-JsonString $manifest.namespace "namespace" $ReleaseNamespace
Test-JsonString $manifest.version "version" $ExpectedVersion
Test-JsonString $manifest.platform "platform" "win32"
Test-JsonString $manifest.payloadRoot "payloadRoot" "payload"
Test-JsonString $manifest.entry.cwd "entry.cwd" "payload"
Test-JsonString $manifest.entry.executable "entry.executable" "payload/Open Design.exe"
$entryPath = Join-Path $extractRoot (Convert-ArchiveRelativePath "payload/Open Design.exe")
if (-not (Test-Path -LiteralPath $entryPath)) {
throw "launcher payload entry executable not found after extraction: $entryPath"
}
$configPath = Join-Path $extractRoot (Convert-ArchiveRelativePath "payload/resources/open-design-config.json")
if (-not (Test-Path -LiteralPath $configPath)) {
throw "launcher payload packaged config not found after extraction: $configPath"
}
} finally {
Remove-Item -LiteralPath $extractRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
function Resolve-LocalUpdateVersion([string]$Channel, [string]$Version) {
if ($Channel -eq "stable") {
$stableMatch = [System.Text.RegularExpressions.Regex]::Match($Version, "^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$")
if (-not $stableMatch.Success) {
throw "full Windows stable smoke requires stable version x.y.z; got $Version"
}
return "{0}.{1}.{2}" -f $stableMatch.Groups['major'].Value, $stableMatch.Groups['minor'].Value, ([int]$stableMatch.Groups['patch'].Value + 1)
}
$releaseChannelPattern = [System.Text.RegularExpressions.Regex]::Escape($Channel)
$match = [System.Text.RegularExpressions.Regex]::Match($Version, "^(?<base>\d+\.\d+\.\d+)-$releaseChannelPattern\.(?<number>\d+)$")
if (-not $match.Success) {
throw "full Windows smoke requires a counted version like x.y.z-$Channel.N; got $Version"
}
return "{0}-{1}.{2}" -f $match.Groups['base'].Value, $Channel, ([int]$match.Groups['number'].Value + 1)
}
New-Item -ItemType Directory -Force -Path $WorkRoot, $ToolsPackDir, $CacheDir, $ReportRoot, (Split-Path -Parent $BuildJsonPath), (Split-Path -Parent $IndexPath), (Split-Path -Parent $OutputsPath) | Out-Null
Remove-Item -LiteralPath $BuildJsonPath -Force -ErrorAction SilentlyContinue
try {
Measure-Step "clean tools-pack win namespace" {
Invoke-CommandChecked -Arguments @(
"pnpm.cmd", "exec", "tools-pack", "win", "cleanup",
"--dir", $ToolsPackDir,
"--namespace", $ReleaseNamespace,
"--json"
)
}
$buildArgs = @(
"pnpm.cmd", "exec", "tools-pack", "win", "build",
"--dir", $ToolsPackDir,
"--cache-dir", $CacheDir,
"--namespace", $ReleaseNamespace,
"--portable",
"--app-version", $ReleaseVersion,
"--to", $BuildTarget,
"--json"
)
if ($SignMode -eq "on") {
$buildArgs += "--signed"
}
Measure-Step "tools-pack win build" {
$buildOutput = & $buildArgs[0] @($buildArgs | Select-Object -Skip 1)
if ($LASTEXITCODE -ne 0) {
throw "tools-pack win build failed with exit code $LASTEXITCODE"
}
$buildOutput | Set-Content -LiteralPath $BuildJsonPath -Encoding utf8
}
Measure-Step "validate launcher payload artifact" {
$build = Read-BuildJson
if ($build -eq $null) {
throw "build json missing before launcher payload validation: $BuildJsonPath"
}
Validate-WinLauncherPayloadArchive -PayloadPath ([string]$build.payloadPath) -ExpectedVersion $ReleaseVersion -Label "primary"
}
$localUpdateArtifactPath = $null
$localUpdateVersion = $null
$externalUpdateMetadataUrl = [string]$env:OD_PACKAGED_E2E_WIN_UPDATE_METADATA_URL
$externalUpdateArtifactPath = [string]$env:OD_PACKAGED_E2E_WIN_UPDATE_ARTIFACT_PATH
$externalUpdateVersion = [string]$env:OD_PACKAGED_E2E_WIN_UPDATE_VERSION
$hasExternalUpdateMetadata = -not [string]::IsNullOrWhiteSpace($externalUpdateMetadataUrl)
$hasExternalUpdateArtifactPair = -not [string]::IsNullOrWhiteSpace($externalUpdateArtifactPath) -and -not [string]::IsNullOrWhiteSpace($externalUpdateVersion)
if ($SmokeMode -eq "full" -and -not $hasExternalUpdateMetadata -and -not $hasExternalUpdateArtifactPair) {
$localUpdateVersion = Resolve-LocalUpdateVersion -Channel $ReleaseChannel -Version $ReleaseVersion
$fixtureDir = Join-Path $WorkRoot "tools-pack-update-fixture"
$fixtureJsonPath = Join-Path $WorkRoot "windows-tools-pack-update-build.json"
$updateArgs = @(
"pnpm.cmd", "exec", "tools-pack", "win", "build",
"--dir", $fixtureDir,
"--cache-dir", $CacheDir,
"--namespace", $ReleaseNamespace,
"--app-version", $localUpdateVersion,
"--to", "nsis",
"--json"
)
if ($SignMode -eq "on") {
$updateArgs += "--signed"
}
Measure-Step "tools-pack win build update fixture" {
$updateOutput = & $updateArgs[0] @($updateArgs | Select-Object -Skip 1)
if ($LASTEXITCODE -ne 0) {
throw "tools-pack win update fixture build failed with exit code $LASTEXITCODE"
}
$updateOutput | Set-Content -LiteralPath $fixtureJsonPath -Encoding utf8
$updateBuild = Get-Content -LiteralPath $fixtureJsonPath -Raw | ConvertFrom-Json
$localUpdateArtifactPath = [string]$updateBuild.installerPath
if ([string]::IsNullOrWhiteSpace($localUpdateArtifactPath)) {
throw "tools-pack win build update fixture did not report installerPath"
}
}
Measure-Step "validate launcher payload update fixture" {
$updateBuild = Get-Content -LiteralPath $fixtureJsonPath -Raw | ConvertFrom-Json
Validate-WinLauncherPayloadArchive -PayloadPath ([string]$updateBuild.payloadPath) -ExpectedVersion $localUpdateVersion -Label "update-fixture"
}
}
if ($SmokeMode -eq "skip") {
Write-Host "Skipping Windows packaged runtime smoke: smoke mode skip"
} else {
$previous = @{
OD_PACKAGED_E2E_BUILD_JSON_PATH = $env:OD_PACKAGED_E2E_BUILD_JSON_PATH
OD_PACKAGED_E2E_WIN = $env:OD_PACKAGED_E2E_WIN
OD_PACKAGED_E2E_WIN_SMOKE_PROFILE = $env:OD_PACKAGED_E2E_WIN_SMOKE_PROFILE
OD_PACKAGED_E2E_NAMESPACE = $env:OD_PACKAGED_E2E_NAMESPACE
OD_PACKAGED_E2E_RELEASE_CHANNEL = $env:OD_PACKAGED_E2E_RELEASE_CHANNEL
OD_PACKAGED_E2E_RELEASE_VERSION = $env:OD_PACKAGED_E2E_RELEASE_VERSION
OD_PACKAGED_E2E_REPORT_DIR = $env:OD_PACKAGED_E2E_REPORT_DIR
OD_PACKAGED_E2E_TOOLS_PACK_DIR = $env:OD_PACKAGED_E2E_TOOLS_PACK_DIR
OD_PACKAGED_E2E_WIN_UPDATE_FIXTURE = $env:OD_PACKAGED_E2E_WIN_UPDATE_FIXTURE
OD_PACKAGED_E2E_WIN_UPDATE_ARTIFACT_PATH = $env:OD_PACKAGED_E2E_WIN_UPDATE_ARTIFACT_PATH
OD_PACKAGED_E2E_WIN_UPDATE_VERSION = $env:OD_PACKAGED_E2E_WIN_UPDATE_VERSION
OD_PACKAGED_E2E_WIN_UPDATE_BUILD_JSON_PATH = $env:OD_PACKAGED_E2E_WIN_UPDATE_BUILD_JSON_PATH
}
try {
$env:OD_PACKAGED_E2E_BUILD_JSON_PATH = $BuildJsonPath
$env:OD_PACKAGED_E2E_WIN = "1"
$env:OD_PACKAGED_E2E_WIN_SMOKE_PROFILE = $SmokeMode
$env:OD_PACKAGED_E2E_NAMESPACE = $ReleaseNamespace
$env:OD_PACKAGED_E2E_RELEASE_CHANNEL = $ReleaseChannel
$env:OD_PACKAGED_E2E_RELEASE_VERSION = $ReleaseVersion
$env:OD_PACKAGED_E2E_REPORT_DIR = $ReportRoot
$env:OD_PACKAGED_E2E_TOOLS_PACK_DIR = $ToolsPackDir
if (-not [string]::IsNullOrWhiteSpace($localUpdateArtifactPath)) {
$env:OD_PACKAGED_E2E_WIN_UPDATE_FIXTURE = "tools-serve"
$env:OD_PACKAGED_E2E_WIN_UPDATE_ARTIFACT_PATH = $localUpdateArtifactPath
$env:OD_PACKAGED_E2E_WIN_UPDATE_VERSION = $localUpdateVersion
$env:OD_PACKAGED_E2E_WIN_UPDATE_BUILD_JSON_PATH = Join-Path $WorkRoot "windows-tools-pack-update-build.json"
}
Measure-Step "release smoke win" {
Remove-Item -LiteralPath $ReportRoot -Recurse -Force -ErrorAction SilentlyContinue
Invoke-CommandChecked -Arguments @("pnpm.cmd", "exec", "tsx", "scripts/release-smoke.ts", "win", "specs/win.spec.ts") -WorkingDirectory (Join-Path (Get-Location).Path "e2e")
}
} finally {
foreach ($key in $previous.Keys) {
if ([string]::IsNullOrWhiteSpace([string]$previous[$key])) {
Remove-Item "Env:$key" -ErrorAction SilentlyContinue
} else {
[Environment]::SetEnvironmentVariable($key, [string]$previous[$key], "Process")
}
}
}
}
Write-Index "success"
Write-Host "$ReleaseChannel build index: $IndexPath"
} catch {
if ($script:failureMessage -eq $null) {
$script:failureMessage = $_.Exception.Message
}
try {
Write-Index "failed"
} catch {
Write-Warning "failed to write $ReleaseChannel build index: $($_.Exception.Message)"
}
throw
}