Files
nexu-io-open-design/tools/release/scripts/build-platform.ps1
PerishFire 0bf1b6d6b8 [codex] converge release workflows and stable dry-runs (#4390)
* fix(tools-pack): use junctions for Windows standalone peer deps

* fix(desktop): expose IPC during startup

* fix(tools-pack): preserve Windows inspect diagnostics

* fix(tools-pack): report Windows inspect status errors

* fix(packaged): use Electron net fetch for app protocol

* fix(packaged): load Windows renderer from web sidecar

* fix(desktop): show Windows packaged window during startup

* fix(packaged): disable Windows GPU startup

* fix(tools-pack): keep Windows core smoke observable

* fix(packaged): remove Windows startup probes

* fix(tools-pack): trace Windows desktop IPC status

* fix(tools-pack): add Windows IPC diagnose loop

* fix(release): default beta-s Windows updater feed

* chore: clean merged test eof

* refactor(release): unify prerelease channel model

* chore(release): close prerelease doc escape hatches

* refactor(release): converge release channel workflows

* fix(release): install toolchain in metadata jobs

* fix(release): build release package before contracts

* chore(release): bump development version to 0.10.1

* fix(e2e): seed windows packaged smoke runtime config

* fix(release): install toolchain for metadata publish

* fix(release): materialize betas metadata checkout

* chore(release): bump development version to 0.10.2

* fix(release): allow betas metadata cold start from s3

* fix(e2e): support betas packaged update scenarios

* fix(release): pass betas channel into packaged smoke

* fix(release): set betas channel during self-hosted builds

* fix(release): verify counted channel reservations

* fix(release): use pnpm cmd for betas windows publish

* fix(release): add betas manifest artifact fallback

* fix(release): skip beta-s public metadata fetch

* fix(release): read beta-s manifests from storage

* fix(release): cache beta windows tools-pack builds

* fix(release): inline beta mac tools-pack builds

* fix(pack): deep sign unsigned mac bundles

* docs(pack): document payload-first beta updater validation

* fix(release): align preview tools-pack cache flow

* fix(release): align prerelease tools-pack cache flow

* fix(release): pass github token to prerelease metadata

* fix(release): setup pnpm before feishu notify

* fix(release): add stable dry-run prepublish flow

* fix(release): accept completed prerelease metadata gate

* fix(release): require stable release branches

* fix(release): converge r2 access checks

* fix(updater): use release channel parser for defaults

* fix(updater): harden windows payload relaunch

* fix(release): converge updater smoke fixture contract

* test(e2e): require silent updater fixture output

* fix(release): align stable windows smoke build path

* fix(ci): include release workspace in validation

* fix(ci): repair release validation lanes

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(ci): restore zero-install Feishu notification

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-06-23 06:13:21 +00: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
}