mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-08 07:19:17 +08:00
* 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>
371 lines
15 KiB
PowerShell
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
|
|
}
|