Files
nexu-io__open-design/tools/release/scripts/build-platform.ps1
open-design-crew[bot] 809d45f27c feat(integrations): add the local Open Design Cloud flow for Codex (#6055)
* feat(daemon): expose Vela login through CLI and MCP

* feat(mcp): add interactive local brief card

* feat(packaged): bootstrap local MCP headlessly

* feat(daemon): secure local BYOK credential profiles

* feat(cli): manage secure BYOK profiles

* feat(web): store BYOK keys in secure profiles

* fix(runs): deduplicate cloud generation requests

* feat(daemon): add end-to-end plugin observability

* fix(pack): include native PTY runtime in desktop packages

Lazy-load node-pty in the daemon so missing native binaries only
disable Terminal and interactive Keychain operations instead of
crashing startup.

Ship and validate target prebuilds in macOS and Windows packages.
Repair the macOS spawn helper before signing, and invalidate stale
Windows packaging caches.

Cover the packaged PTY path in macOS and Windows smoke tests. Require
Vela CLI for every beta desktop build target.

* fix(mcp): localize and stabilize plugin briefs

Keep collect and confirm on one MCP Apps card, report intrinsic size changes,
and remove the fragile widget-state fallback.

Localize brief forms, summaries, status, and public errors from the current
request or Host locale while preserving stable protocol ids and selections.

Keep internal runtime identifiers out of user-facing MCP guidance.

* fix(packaged): register live Studio URLs with daemon

Register the packaged web sidecar loopback origin over namespace-scoped daemon
IPC after dynamic binding.

Invalidate install-info by the live web port and remove the second MCP URL cache
so run and project responses use the current Studio address.

Validate loopback origins and cover registration, rebind, and cache refresh.

* fix(mcp): stabilize brief confirmation lifecycle

Keep confirmed briefs locked after Host publication failures and retry only the cached follow-up message instead of confirming twice.

Replace blank initialization with a compact loading state, accept immediate and delayed tool results, and bump the MCP Apps resource to v3 so Codex does not reuse the stale card.

Add an executable JSDOM Host bridge harness covering double-submit, publication retry, delayed results, and repeated draft notifications. Validated with the full daemon suite, workspace typecheck, and guard.

* fix(mcp): prevent brief app resize feedback loops

Replace the self-observed intrinsic-height loop with deduplicated body measurements so Codex MCP app iframes do not repeatedly resize themselves and fail sandbox startup.

Bump the brief resource URI to v4 to avoid cached v3 cards and cover the regression with the focused brief-app spec. Validated with daemon tests/build, workspace guard/typecheck, a verified beta DMG, and a direct packaged MCP resource smoke.

* fix(mcp): use the final Open Design plugin identity

Make open-design the only accepted external plugin ID across MCP tools, run attribution, artifact version origins, and telemetry. Rename the Brief MCP App resource to v5 and reject the unpublished old ID instead of aliasing it.

Validated with 171 daemon tests, 5 web tests, daemon/web typechecks, and guard.

* chore(pack): bump Vela CLI to 0.0.27

* chore(nix): refresh pnpm dependency hashes

* fix(mcp): honor host and plugin request contracts

Respect independently advertised MCP Apps capabilities and prefer the Codex-native follow-up bridge so confirmed briefs render as readable user turns instead of context-only messages. Bump the brief resource to v6 to invalidate stale cards.\n\nMark Vela login and account probes as open-world interactions, and require stable request IDs for attributed generation so lost responses cannot create a second logical run.

* fix(cli): support keyless BYOK profiles

Add an explicit --no-api-key save mode for local and self-hosted providers while keeping credential-bearing profiles stdin-only. Reject missing or conflicting credential modes and cover both paths in the CLI tests.

* fix(pack): replace headless owner when desktop opens

Advertise headless packaged runtimes as having no visible window, then shut them down before a normal desktop launch continues instead of treating SHOW as a successful focus.

Validated with the focused packaged headless and launcher lifecycle tests (20 passed).

* fix(pack): clean up failed headless startup

Acquire the headless identity, sidecars, and IPC server through one failure-atomic lifecycle so later bootstrap errors close every previously acquired owner in reverse order.

Cover MCP-install and web-identity publication failures; validated with 22 focused tests and the packaged typecheck.

* fix(byok): remove raw provider request contract

Expose only daemon-owned BYOK profile references from shared run types and the web transport, while retaining the daemon's fail-closed rejection for legacy or untyped raw credential payloads.

Validated with 76 web provider tests plus web and contracts typechecks.

* fix(daemon): preserve interrupted run idempotency

Hydrate queued or running durable requests as DAEMON_RESTARTED failures before idempotency reuse, preserving the original request and workflow mappings instead of creating a second cloud operation.

Share the restart transition with boot reconciliation and expose synthetic terminal events to reattaching clients. Validated with 48 focused tests and the daemon typecheck.

* fix(byok): roll back failed credential metadata writes

Snapshot secure-store values before mutation and compensate create, update, and delete operations when profile metadata cannot be persisted, preventing orphaned or lost credentials.

Validated with nine focused credential-service tests and the daemon typecheck.

* fix(web): reconcile selected BYOK profile metadata

Treat the daemon's selected secure profile as authoritative for protocol, endpoint, model, and API version so CLI profile edits cannot leave the browser showing stale execution settings.

Validated with 66 config tests and the web typecheck.

* fix(byok): migrate legacy credentials before cleanup

Import each supported legacy browser credential into a stable daemon-owned secure profile before removing plaintext copies. Keep the original browser record on secure-store or unsupported-provider failures and surface a Settings recovery path.

Cover successful migration, duplicate projections, and injected secure-backend failure while preserving existing Bedrock downgrade behavior in memory.

* fix(byok): secure Windows profiles with DPAPI

Dispatch native Windows BYOK profiles to a CurrentUser DPAPI backend rooted under the resolved daemon data directory. Secrets enter PowerShell only through stdin, encrypted blobs stay separate from non-secret profile metadata, and API responses remain secret-free.

Add platform dispatch coverage, a Windows-only real DPAPI round-trip test, and the packaged Windows smoke path for create, resolve, response-redaction, and delete behavior.

* fix(web): preserve config writes during hydration

* fix(byok): persist secure credentials across entry flows

* test(byok): cover secure onboarding persistence

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
2026-07-30 02:00:56 +00:00

378 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,
[switch]$RequireVelaCli
)
$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"
}
if ($RequireVelaCli) {
$buildArgs += "--require-vela-cli"
}
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"
}
if ($RequireVelaCli) {
$updateArgs += "--require-vela-cli"
}
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
}