Files
ctms/frontend/scripts/verify-release-build-env.mjs
T
Cheng Zhou e3489f3b06
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
build(release): stage local macOS v0.1.0 distribution
2026-07-17 11:31:11 +08:00

189 lines
7.3 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
const rootDir = resolve(frontendDir, "..");
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
const desktopReleasePolicy = await loadDesktopReleasePolicy();
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
const failures = [];
const allowedChannels = new Set(["dev", "main", "release", "local"]);
const fullShaPattern = /^[0-9a-f]{40}$/i;
const semverTag = `v${packageInfo.version}`;
const env = process.env;
const channel = env.VITE_BUILD_CHANNEL || "local";
const commit = env.VITE_BUILD_COMMIT || "local";
const isCi = env.CI === "true" || env.GITHUB_ACTIONS === "true";
const isTagBuild = env.GITHUB_REF_TYPE === "tag";
const isReleaseBuild = env.RELEASE_BUILD === "true" || isTagBuild || channel === "release";
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
const requiresUpdaterSigning = env.REQUIRE_UPDATER_SIGNING === "true";
const allowsUnsignedPlatformRelease = env.ALLOW_UNSIGNED_PLATFORM_RELEASE === "true";
const desktopPlatformSigningMode = env.DESKTOP_PLATFORM_SIGNING_MODE;
const desktopReleasePlatform = env.DESKTOP_RELEASE_PLATFORM;
const isNativeDesktopRelease = Boolean(
desktopReleasePlatform ||
desktopPlatformSigningMode ||
requiresMacosSigning ||
requiresWindowsSigning ||
requiresUpdaterSigning,
);
const validateDesktopServerUrl = () => {
const raw = env.VITE_DESKTOP_SERVER_URL;
requireEnv("VITE_DESKTOP_SERVER_URL");
if (!raw) return;
let url;
try {
url = new URL(raw);
} catch (error) {
fail(`VITE_DESKTOP_SERVER_URL is invalid: ${error.message}`);
return;
}
assert(url.protocol === "https:", "VITE_DESKTOP_SERVER_URL must use HTTPS.");
assert(url.username === "" && url.password === "", "VITE_DESKTOP_SERVER_URL must not include credentials.");
assert(url.pathname === "/", "VITE_DESKTOP_SERVER_URL must be an origin without a path.");
assert(url.search === "" && url.hash === "", "VITE_DESKTOP_SERVER_URL must not include query parameters or fragments.");
};
const fail = (message) => failures.push(message);
const assert = (condition, message) => {
if (!condition) fail(message);
};
const requireEnv = (name) => {
assert(Boolean(env[name]), `${name} must be configured for this desktop release build.`);
};
const gitHead = () => {
try {
return execFileSync("git", ["rev-parse", "HEAD"], {
cwd: rootDir,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
} catch {
return undefined;
}
};
assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...allowedChannels].join(", ")}.`);
if (isCi || isReleaseBuild) {
assert(channel !== "local", "CI and release builds must inject VITE_BUILD_CHANNEL.");
assert(fullShaPattern.test(commit), "CI and release builds must inject a full 40-character VITE_BUILD_COMMIT.");
validateDesktopServerUrl();
}
if (env.GITHUB_SHA) {
assert(
commit === env.GITHUB_SHA || commit === "local",
"VITE_BUILD_COMMIT must match GITHUB_SHA when GitHub Actions provides a source commit.",
);
}
if (isReleaseBuild && fullShaPattern.test(commit)) {
const headSha = gitHead();
assert(Boolean(headSha), "Release builds must be able to resolve the current Git commit.");
if (headSha) {
assert(commit === headSha, "VITE_BUILD_COMMIT must match the current Git HEAD for release builds.");
}
}
if (isTagBuild) {
assert(env.GITHUB_REF_NAME === semverTag, `Release tag must be ${semverTag}; found ${env.GITHUB_REF_NAME || "<missing>"}.`);
assert(channel === "release", "Release tag builds must set VITE_BUILD_CHANNEL=release.");
}
assert(
!(requiresMacosSigning && requiresWindowsSigning),
"macOS and Windows signing requirements must be checked in their native jobs.",
);
if (isNativeDesktopRelease) {
assert(isReleaseBuild, "Native desktop release settings may only be used for release builds.");
assert(
desktopReleasePolicy.updaterSigningRequired === true && requiresUpdaterSigning,
"Formal desktop releases must set REQUIRE_UPDATER_SIGNING=true; updater signing cannot be disabled.",
);
assert(
desktopPlatformSigningMode === desktopReleaseResolution.platformSigningMode,
`DESKTOP_PLATFORM_SIGNING_MODE must be ${desktopReleaseResolution.platformSigningMode} for v${packageInfo.version}.`,
);
assert(
desktopReleasePlatform === "macos" || desktopReleasePlatform === "windows",
"DESKTOP_RELEASE_PLATFORM must be macos or windows for native desktop release builds.",
);
if (isCi) {
assert(isTagBuild, "Native desktop release candidate builds in CI must run from a release tag.");
}
}
if (requiresUpdaterSigning) {
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
}
if (desktopPlatformSigningMode === "signed") {
assert(!allowsUnsignedPlatformRelease, "Signed releases must not enable ALLOW_UNSIGNED_PLATFORM_RELEASE.");
assert(
(desktopReleasePlatform === "macos" && requiresMacosSigning && !requiresWindowsSigning) ||
(desktopReleasePlatform === "windows" && requiresWindowsSigning && !requiresMacosSigning),
"Signed native release jobs must enable only their matching platform signing requirement.",
);
}
if (desktopPlatformSigningMode === "unsigned-exception") {
assert(
desktopReleaseResolution.platformSigningMode === "unsigned-exception",
`v${packageInfo.version} has no approved unsigned platform release exception.`,
);
assert(allowsUnsignedPlatformRelease, "The approved exception requires ALLOW_UNSIGNED_PLATFORM_RELEASE=true.");
assert(
!requiresMacosSigning && !requiresWindowsSigning,
"Unsigned platform exceptions must not claim Apple or Windows platform signing.",
);
if (desktopReleasePlatform === "macos") {
assert(process.platform === "darwin", "The macOS ad-hoc release exception must run on macOS.");
}
if (desktopReleasePlatform === "windows") {
assert(process.platform === "win32", "The unsigned Windows release exception must run on Windows.");
}
}
if (requiresMacosSigning) {
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
requireEnv("APPLE_ID");
requireEnv("APPLE_PASSWORD");
requireEnv("APPLE_TEAM_ID");
assert(
Boolean(env.APPLE_CERTIFICATE || env.APPLE_SIGNING_IDENTITY),
"APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY must be configured for macOS signing.",
);
if (env.APPLE_CERTIFICATE) {
requireEnv("APPLE_CERTIFICATE_PASSWORD");
}
}
if (requiresWindowsSigning) {
assert(process.platform === "win32", "Signed Windows desktop release builds must run on Windows.");
requireEnv("WINDOWS_CERTIFICATE");
requireEnv("WINDOWS_CERTIFICATE_PASSWORD");
requireEnv("WINDOWS_TIMESTAMP_URL");
}
if (failures.length > 0) {
console.error(`Release build environment check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
process.exitCode = 1;
} else {
console.log("Release build environment check passed.");
}