mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-07 23:09:34 +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>
373 lines
13 KiB
Bash
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
|