Files
nexu-io-open-design/tools/release/scripts/build-platform.sh
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

373 lines
13 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
required() {
local name="$1"
if [ -z "${!name:-}" ]; then
echo "$name is required" >&2
exit 1
fi
}
format_duration() {
local ms="$1"
if [ "$ms" -ge 60000 ]; then
node -e 'process.stdout.write(`${Math.round((Number(process.argv[1]) / 60000) * 10) / 10}m`)' "$ms"
else
node -e 'process.stdout.write(`${Math.round((Number(process.argv[1]) / 1000) * 10) / 10}s`)' "$ms"
fi
}
measure_step() {
local name="$1"
shift
echo "##[group]$name"
local started duration
started="$(node -e 'process.stdout.write(String(Date.now()))')"
if "$@"; then
duration="$(( $(node -e 'process.stdout.write(String(Date.now()))') - started ))"
release_timings_json="${release_timings_json:+$release_timings_json,}{\"step\":$(node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "$name"),\"status\":\"success\",\"durationMs\":$duration}"
echo "[$name] success in $(format_duration "$duration")"
else
local status=$?
duration="$(( $(node -e 'process.stdout.write(String(Date.now()))') - started ))"
release_timings_json="${release_timings_json:+$release_timings_json,}{\"step\":$(node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "$name"),\"status\":\"failed\",\"durationMs\":$duration}"
echo "[$name] failed in $(format_duration "$duration")"
echo "##[endgroup]"
return "$status"
fi
echo "##[endgroup]"
}
is_transient_notary_output() {
local output="$1"
case "$output" in
*abortedUpload* | *deadlineExceeded* | *ECONNRESET* | *ETIMEDOUT* | *ENOTFOUND* | *EAI_AGAIN* | *"socket hang up"* | *"network connection was lost"*)
return 0
;;
*)
return 1
;;
esac
}
notary_auth_args() {
if [ -n "${APPLE_NOTARY_KEYCHAIN_PROFILE:-}" ]; then
if [ -n "${APPLE_NOTARY_KEYCHAIN:-}" ]; then
printf '%s\0' --keychain "$APPLE_NOTARY_KEYCHAIN"
fi
printf '%s\0' --keychain-profile "$APPLE_NOTARY_KEYCHAIN_PROFILE"
else
printf '%s\0' --apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID"
fi
}
notarize_mac_dmg_once() {
local dmg_path="$1"
local auth_args=()
local s3_arg="--no-s3-acceleration"
local status
if [ "${OPEN_DESIGN_NOTARIZE_S3_ACCELERATION:-false}" = "true" ]; then
s3_arg="--s3-acceleration"
fi
while IFS= read -r -d '' arg; do
auth_args+=("$arg")
done < <(notary_auth_args)
echo "[release notarize] submitting $(basename "$dmg_path") ($(wc -c < "$dmg_path" | tr -d ' ') bytes) with $s3_arg"
xcrun notarytool submit "$dmg_path" \
"${auth_args[@]}" \
--wait \
--output-format json \
"$s3_arg"
status=$?
if [ "$status" -ne 0 ]; then
return "$status"
fi
xcrun stapler staple "$dmg_path"
}
notarize_mac_dmg() {
local dmg_path="$1"
local attempts="${OPEN_DESIGN_NOTARIZE_ATTEMPTS:-8}"
local retry_delay_ms="${OPEN_DESIGN_NOTARIZE_RETRY_DELAY_MS:-15000}"
local attempt output status
if [ ! -f "$dmg_path" ]; then
echo "expected dmg not found for notarization: $dmg_path" >&2
return 1
fi
for ((attempt = 1; attempt <= attempts; attempt += 1)); do
set +e
output="$(notarize_mac_dmg_once "$dmg_path" 2>&1)"
status=$?
set -e
printf '%s\n' "$output"
if [ "$status" -eq 0 ]; then
return 0
fi
if [ "$attempt" -lt "$attempts" ] && is_transient_notary_output "$output"; then
echo "[release notarize] transient notarytool failure on attempt $attempt/$attempts; retrying in ${retry_delay_ms}ms" >&2
sleep "$(node -e 'process.stdout.write(String(Number(process.argv[1]) / 1000))' "$retry_delay_ms")"
else
return "$status"
fi
done
}
prepare_mac_signing() {
if [ -n "${CSC_KEYCHAIN:-}" ]; then
unset CSC_LINK
unset CSC_KEY_PASSWORD
return 0
fi
required APPLE_SIGNING_CERTIFICATE_BASE64
required APPLE_SIGNING_CERTIFICATE_PASSWORD
local cert_path="$RELEASE_WORK_DIR/open-design-signing.p12"
if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then
printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path"
fi
export CSC_LINK="$cert_path"
export CSC_KEY_PASSWORD="$APPLE_SIGNING_CERTIFICATE_PASSWORD"
}
prepare_mac_notarization() {
required APPLE_ID
required APPLE_TEAM_ID
if [ -z "${APPLE_NOTARY_KEYCHAIN_PROFILE:-}" ]; then
required APPLE_APP_SPECIFIC_PASSWORD
fi
}
select_mac_signing_identity() {
local keychain_path="${CSC_KEYCHAIN:-}"
if [ -z "$keychain_path" ]; then
return 0
fi
local identities identity_name csc_name
identities="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identities"
identity_name="$(printf '%s\n' "$identities" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$identity_name" ]; then
echo "mac signing failed: no Developer ID Application identity found in $keychain_path" >&2
exit 1
fi
csc_name="${identity_name#Developer ID Application: }"
export CSC_NAME="$csc_name"
echo "mac signing: selected CSC_NAME=$CSC_NAME"
}
write_outputs() {
node --input-type=module <<'NODE'
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
const build = JSON.parse(readFileSync(process.env.BUILD_JSON_PATH, "utf8"));
const outputs = {
build_json_path: process.env.BUILD_JSON_PATH,
build_log_path: process.env.BUILD_LOG_PATH ?? "",
release_target: process.env.RELEASE_TARGET,
release_version: process.env.RELEASE_VERSION,
...(build.appPath ? { app_path: build.appPath } : {}),
...(build.dmgPath ? { dmg_path: build.dmgPath } : {}),
...(build.payloadPath ? { payload_path: build.payloadPath } : {}),
...(build.zipPath ? { zip_path: build.zipPath } : {}),
...(build.outputRoot ? { output_root: build.outputRoot } : {}),
};
mkdirSync(dirname(process.env.RELEASE_OUTPUTS_PATH), { recursive: true });
writeFileSync(process.env.RELEASE_OUTPUTS_PATH, `${JSON.stringify(outputs, null, 2)}\n`, "utf8");
NODE
}
required BUILD_JSON_PATH
required BUILD_LOG_PATH
required RELEASE_BUILD_TARGET
required RELEASE_CHANNEL
required RELEASE_NAMESPACE
required RELEASE_OUTPUTS_PATH
required RELEASE_SMOKE_MODE
required RELEASE_TARGET
required RELEASE_VERSION
required RELEASE_WORK_DIR
required TOOLS_PACK_CACHE_DIR
required TOOLS_PACK_DIR
case "$RELEASE_SMOKE_MODE" in
skip | core | full) ;;
*) echo "unsupported RELEASE_SMOKE_MODE: $RELEASE_SMOKE_MODE" >&2; exit 1 ;;
esac
mkdir -p "$RELEASE_WORK_DIR" "$TOOLS_PACK_CACHE_DIR" "$(dirname "$BUILD_JSON_PATH")" "$(dirname "$BUILD_LOG_PATH")"
: > "$BUILD_LOG_PATH"
release_timings_json=""
release_channel="$RELEASE_CHANNEL"
resolve_local_update_version() {
local channel="$1"
local version="$2"
if [ "$channel" = "stable" ]; then
if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
printf '%s.%s.%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$((BASH_REMATCH[3] + 1))"
return 0
fi
echo "full mac stable smoke requires stable version x.y.z; got $version" >&2
return 1
fi
if [[ "$version" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-${channel}\.([0-9]+)$ ]]; then
printf '%s-%s.%s' "${BASH_REMATCH[1]}" "$channel" "$((BASH_REMATCH[2] + 1))"
return 0
fi
echo "full mac payload smoke requires counted version x.y.z-$channel.N; got $version" >&2
return 1
}
case "$RELEASE_TARGET" in
mac_arm64 | mac_x64)
sign_mode="${RELEASE_SIGN_MODE:-no}"
case "$sign_mode" in
no | sign-only | notarize) ;;
*) echo "unsupported RELEASE_SIGN_MODE: $sign_mode" >&2; exit 1 ;;
esac
case "$RELEASE_BUILD_TARGET" in
dmg | all) ;;
*) echo "unsupported mac RELEASE_BUILD_TARGET: $RELEASE_BUILD_TARGET" >&2; exit 1 ;;
esac
if [ "$sign_mode" != "no" ]; then
measure_step "prepare mac signing" prepare_mac_signing
measure_step "select mac signing identity" select_mac_signing_identity
fi
if [ "$sign_mode" = "notarize" ]; then
measure_step "prepare mac notarization" prepare_mac_notarization
fi
rm -rf "$TOOLS_PACK_DIR"
build_args=(
exec tools-pack mac build
--dir "$TOOLS_PACK_DIR"
--cache-dir "$TOOLS_PACK_CACHE_DIR"
--namespace "$RELEASE_NAMESPACE"
--portable
--app-version "$RELEASE_VERSION"
--mac-compression "${MAC_COMPRESSION:-normal}"
--to "$RELEASE_BUILD_TARGET"
--json
)
if [ "${REQUIRE_VELA_CLI:-false}" = "true" ]; then
build_args+=(--require-vela-cli)
fi
if [ "$sign_mode" != "no" ]; then
build_args+=(--signed)
fi
;;
linux_x64)
if [ "$RELEASE_BUILD_TARGET" != "appimage" ]; then
echo "unsupported linux_x64 RELEASE_BUILD_TARGET: $RELEASE_BUILD_TARGET" >&2
exit 1
fi
rm -rf "$TOOLS_PACK_DIR"
build_args=(
exec tools-pack linux build
--dir "$TOOLS_PACK_DIR"
--cache-dir "$TOOLS_PACK_CACHE_DIR"
--namespace "$RELEASE_NAMESPACE"
--portable
--app-version "$RELEASE_VERSION"
--to appimage
--containerized
--json
)
;;
*)
echo "unsupported RELEASE_TARGET for build-platform.sh: $RELEASE_TARGET" >&2
exit 1
;;
esac
if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$BUILD_LOG_PATH" >&2))"; then
printf '%s\n' "$build_output" | tee "$BUILD_JSON_PATH"
if [ "${sign_mode:-no}" = "notarize" ]; then
dmg_path="$(BUILD_JSON_PATH="$BUILD_JSON_PATH" node --input-type=module <<'NODE'
import { readFileSync } from "node:fs";
const build = JSON.parse(readFileSync(process.env.BUILD_JSON_PATH, "utf8"));
process.stdout.write(build.dmgPath ?? "");
NODE
)"
measure_step "notarize mac dmg" notarize_mac_dmg "$dmg_path"
fi
BUILD_JSON_PATH="$BUILD_JSON_PATH" RELEASE_TIMINGS_JSON="[$release_timings_json]" node --input-type=module <<'NODE'
import { readFileSync, writeFileSync } from "node:fs";
const path = process.env.BUILD_JSON_PATH;
const build = JSON.parse(readFileSync(path, "utf8"));
build.releaseScriptTimings = JSON.parse(process.env.RELEASE_TIMINGS_JSON ?? "[]");
writeFileSync(path, `${JSON.stringify(build, null, 2)}\n`, "utf8");
NODE
else
build_status=$?
printf '%s\n' "$build_output"
exit "$build_status"
fi
if [ "$RELEASE_SMOKE_MODE" = "skip" ]; then
echo "Skipping $RELEASE_TARGET packaged runtime smoke: smoke mode skip"
elif [ "$RELEASE_TARGET" = "linux_x64" ]; then
required RELEASE_REPORT_DIR
mkdir -p "$RELEASE_REPORT_DIR/screenshots"
OD_PACKAGED_E2E_LINUX_APPIMAGE=1 \
OD_PACKAGED_E2E_NAMESPACE="$RELEASE_NAMESPACE" \
OD_PACKAGED_E2E_SCREENSHOT_PATH="$RELEASE_REPORT_DIR/screenshots/open-design-linux-smoke.png" \
OD_PACKAGED_E2E_TOOLS_PACK_DIR="$TOOLS_PACK_DIR" \
pnpm --dir e2e test specs/linux.spec.ts 2>&1 | tee "$RELEASE_REPORT_DIR/vitest.log"
else
required RELEASE_REPORT_DIR
update_build_json_path=""
update_version=""
if [ "$RELEASE_SMOKE_MODE" = "full" ] && [ -z "${OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL:-}" ]; then
if [ "$RELEASE_TARGET" != "mac_arm64" ]; then
echo "local tools-serve mac updater fixture is only supported for mac_arm64; provide OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL for $RELEASE_TARGET full smoke" >&2
exit 1
fi
update_version="$(resolve_local_update_version "$release_channel" "$RELEASE_VERSION")"
update_fixture_dir="$RELEASE_WORK_DIR/tools-pack-update-fixture"
update_build_json_path="$RELEASE_WORK_DIR/mac-tools-pack-update-build.json"
update_args=(
exec tools-pack mac build
--dir "$update_fixture_dir"
--cache-dir "$TOOLS_PACK_CACHE_DIR"
--namespace "$RELEASE_NAMESPACE"
--portable
--app-version "$update_version"
--mac-compression "${MAC_COMPRESSION:-normal}"
--to dmg
--json
)
build_mac_update_fixture() {
local update_output
update_output="$(pnpm "${update_args[@]}")"
printf '%s\n' "$update_output" > "$update_build_json_path"
}
measure_step "tools-pack mac build update fixture" build_mac_update_fixture
fi
OD_PACKAGED_E2E_BUILD_JSON_PATH="$BUILD_JSON_PATH" \
OD_PACKAGED_E2E_BUILD_LOG_PATH="$BUILD_LOG_PATH" \
OD_PACKAGED_E2E_MAC=1 \
OD_PACKAGED_E2E_MAC_UPDATE_BUILD_JSON_PATH="$update_build_json_path" \
OD_PACKAGED_E2E_MAC_UPDATE_FIXTURE="${update_build_json_path:+tools-serve}" \
OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL="${OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL:-}" \
OD_PACKAGED_E2E_MAC_UPDATE_VERSION="${OD_PACKAGED_E2E_MAC_UPDATE_VERSION:-$update_version}" \
OD_PACKAGED_E2E_MAC_SMOKE_PROFILE="$RELEASE_SMOKE_MODE" \
OD_PACKAGED_E2E_NAMESPACE="$RELEASE_NAMESPACE" \
OD_PACKAGED_E2E_RELEASE_CHANNEL="$release_channel" \
OD_PACKAGED_E2E_RELEASE_VERSION="$RELEASE_VERSION" \
OD_PACKAGED_E2E_REPORT_DIR="$RELEASE_REPORT_DIR" \
OD_PACKAGED_E2E_TOOLS_PACK_DIR="$TOOLS_PACK_DIR" \
pnpm --dir e2e exec tsx scripts/release-smoke.ts mac specs/mac.spec.ts
fi
write_outputs