203 lines
7.7 KiB
JavaScript
203 lines
7.7 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 env = process.env;
|
|
const fullShaPattern = /^[0-9a-f]{40}$/i;
|
|
const expectedTag = `v${packageInfo.version}`;
|
|
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 requiredUpdaterEnv = ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"];
|
|
const requiredMacosEnv = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID"];
|
|
const requiredWindowsEnv = ["WINDOWS_CERTIFICATE", "WINDOWS_CERTIFICATE_PASSWORD"];
|
|
|
|
const fail = (message) => failures.push(message);
|
|
const assert = (condition, message) => {
|
|
if (!condition) fail(message);
|
|
};
|
|
|
|
const git = (args) =>
|
|
execFileSync("git", args, {
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
}).trim();
|
|
|
|
const gitMaybe = (args) => {
|
|
try {
|
|
return git(args);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const requireEnv = (name) => {
|
|
assert(Boolean(env[name]), `${name} must be configured for desktop release readiness.`);
|
|
};
|
|
|
|
const validateBaseUrl = () => {
|
|
const raw = env.DESKTOP_UPDATE_BASE_URL;
|
|
requireEnv("DESKTOP_UPDATE_BASE_URL");
|
|
if (!raw) return;
|
|
|
|
let url;
|
|
try {
|
|
url = new URL(raw.endsWith("/") ? raw : `${raw}/`);
|
|
} catch (error) {
|
|
fail(`DESKTOP_UPDATE_BASE_URL is invalid: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
assert(url.protocol === "https:", "DESKTOP_UPDATE_BASE_URL must use HTTPS.");
|
|
assert(url.username === "" && url.password === "", "DESKTOP_UPDATE_BASE_URL must not include credentials.");
|
|
assert(!/[?&]token=/i.test(url.search), "DESKTOP_UPDATE_BASE_URL must not include token query parameters.");
|
|
assert(
|
|
url.pathname.includes(packageInfo.version),
|
|
`DESKTOP_UPDATE_BASE_URL must include the immutable version segment ${packageInfo.version}.`,
|
|
);
|
|
};
|
|
|
|
const validateClientServerUrl = () => {
|
|
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 validateWindowsTimestampUrl = () => {
|
|
const raw = env.WINDOWS_TIMESTAMP_URL;
|
|
requireEnv("WINDOWS_TIMESTAMP_URL");
|
|
if (!raw) return;
|
|
|
|
let url;
|
|
try {
|
|
url = new URL(raw);
|
|
} catch (error) {
|
|
fail(`WINDOWS_TIMESTAMP_URL is invalid: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
assert(["http:", "https:"].includes(url.protocol), "WINDOWS_TIMESTAMP_URL must use HTTP or HTTPS.");
|
|
assert(url.username === "" && url.password === "", "WINDOWS_TIMESTAMP_URL must not include credentials.");
|
|
assert(!/[?&]token=/i.test(url.search), "WINDOWS_TIMESTAMP_URL must not include token query parameters.");
|
|
};
|
|
|
|
const headSha = gitMaybe(["rev-parse", "HEAD"]);
|
|
const exactTag = gitMaybe(["describe", "--tags", "--exact-match", "HEAD"]);
|
|
const status = gitMaybe(["status", "--porcelain"]);
|
|
|
|
assert(Boolean(headSha), "Current Git commit cannot be resolved.");
|
|
assert(exactTag === expectedTag, `Current commit must be exactly tagged ${expectedTag}; found ${exactTag || "<none>"}.`);
|
|
assert(status === "", "Release readiness requires a clean working tree.");
|
|
|
|
assert(env.VITE_BUILD_CHANNEL === "release", "VITE_BUILD_CHANNEL must be release.");
|
|
assert(fullShaPattern.test(env.VITE_BUILD_COMMIT || ""), "VITE_BUILD_COMMIT must be the full release commit SHA.");
|
|
if (headSha && env.VITE_BUILD_COMMIT) {
|
|
assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit.");
|
|
}
|
|
|
|
assert(
|
|
!(requiresMacosSigning && requiresWindowsSigning),
|
|
"macOS and Windows signing readiness must be checked in their native jobs.",
|
|
);
|
|
assert(
|
|
desktopReleasePlatform === "macos" || desktopReleasePlatform === "windows",
|
|
"DESKTOP_RELEASE_PLATFORM must be macos or windows.",
|
|
);
|
|
assert(
|
|
desktopPlatformSigningMode === desktopReleaseResolution.platformSigningMode,
|
|
`DESKTOP_PLATFORM_SIGNING_MODE must be ${desktopReleaseResolution.platformSigningMode} for v${packageInfo.version}.`,
|
|
);
|
|
assert(
|
|
desktopReleasePolicy.updaterSigningRequired === true && requiresUpdaterSigning,
|
|
"Release readiness requires REQUIRE_UPDATER_SIGNING=true; updater signing cannot be disabled.",
|
|
);
|
|
|
|
for (const name of requiredUpdaterEnv) {
|
|
requireEnv(name);
|
|
}
|
|
|
|
if (desktopPlatformSigningMode === "signed") {
|
|
assert(!allowsUnsignedPlatformRelease, "Signed releases must not enable ALLOW_UNSIGNED_PLATFORM_RELEASE.");
|
|
assert(
|
|
(desktopReleasePlatform === "macos" && requiresMacosSigning && !requiresWindowsSigning) ||
|
|
(desktopReleasePlatform === "windows" && requiresWindowsSigning && !requiresMacosSigning),
|
|
"Signed readiness must enable only the matching native 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 readiness must not claim Apple or Windows platform signing.",
|
|
);
|
|
if (desktopReleasePlatform === "macos") {
|
|
assert(process.platform === "darwin", "The macOS ad-hoc readiness check must run on macOS.");
|
|
}
|
|
if (desktopReleasePlatform === "windows") {
|
|
assert(process.platform === "win32", "The unsigned Windows readiness check must run on Windows.");
|
|
}
|
|
}
|
|
|
|
if (requiresMacosSigning) {
|
|
assert(process.platform === "darwin", "Signed macOS desktop release readiness must run on macOS.");
|
|
for (const name of requiredMacosEnv) {
|
|
requireEnv(name);
|
|
}
|
|
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 readiness must run on Windows.");
|
|
for (const name of requiredWindowsEnv) {
|
|
requireEnv(name);
|
|
}
|
|
validateWindowsTimestampUrl();
|
|
}
|
|
|
|
validateBaseUrl();
|
|
validateClientServerUrl();
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`Desktop release readiness check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log("Desktop release readiness check passed.");
|
|
}
|