build(release): stage local macOS v0.1.0 distribution
This commit is contained in:
@@ -17,6 +17,7 @@ const tag = value("--tag") || process.env.GITHUB_REF_NAME;
|
||||
const commit = value("--commit") || process.env.GITHUB_SHA;
|
||||
const macosSigning = value("--macos-signing");
|
||||
const windowsSigning = value("--windows-signing");
|
||||
const releaseStage = value("--stage") || "complete";
|
||||
const outputPath = resolve(
|
||||
frontendDir,
|
||||
value("--output") || "src-tauri/target/desktop-release-feed/DESKTOP-RELEASE-PROVENANCE.json",
|
||||
@@ -31,6 +32,13 @@ assert(tag === `v${packageInfo.version}`, `Release provenance tag must be v${pac
|
||||
assert(/^[0-9a-f]{40}$/i.test(commit || ""), "Release provenance commit must be a full 40-character SHA.");
|
||||
assert(macosSigning === resolution.macosSigning, `macOS signing mode must be ${resolution.macosSigning}.`);
|
||||
assert(windowsSigning === resolution.windowsSigning, `Windows signing mode must be ${resolution.windowsSigning}.`);
|
||||
assert(["complete", "macos-first"].includes(releaseStage), "Release provenance stage must be complete or macos-first.");
|
||||
if (releaseStage === "macos-first") {
|
||||
assert(
|
||||
resolution.macosLocalExactTagFallback === true && resolution.windowsDelivery === "deferred-same-tag",
|
||||
`v${packageInfo.version} does not approve a staged local macOS-first release.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Desktop release provenance creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||
@@ -50,6 +58,13 @@ const provenance = {
|
||||
updaterSigning: "required-and-verified",
|
||||
artifactLabel: resolution.artifactLabel,
|
||||
distribution: resolution.distribution,
|
||||
stage: releaseStage,
|
||||
platformAvailability:
|
||||
releaseStage === "macos-first"
|
||||
? { macos: "available", windows: "pending-same-tag" }
|
||||
: { macos: "available", windows: "available" },
|
||||
updaterFeedActivation:
|
||||
releaseStage === "macos-first" ? "withheld-pending-combined-verification" : "combined-platform-verification-required",
|
||||
warnings: resolution.warningRequired
|
||||
? [
|
||||
"macOS is ad-hoc signed and not Apple-notarized; Gatekeeper warnings are expected.",
|
||||
|
||||
@@ -34,6 +34,14 @@ const validatePolicy = (policy) => {
|
||||
assert(exception?.macos === "ad-hoc", `${prefix} must constrain macOS to ad-hoc signing.`);
|
||||
assert(exception?.windows === "unsigned", `${prefix} must constrain Windows to unsigned.`);
|
||||
assert(exception?.distribution === "controlled", `${prefix} must constrain distribution to controlled.`);
|
||||
if (exception?.version === "0.1.0") {
|
||||
assert(exception?.macosLocalExactTagFallback === true, `${prefix} must explicitly approve the local exact-tag macOS fallback.`);
|
||||
assert(exception?.windowsDelivery === "deferred-same-tag", `${prefix} must defer Windows only from the same tag.`);
|
||||
assert(
|
||||
exception?.updaterFeedActivation === "after-combined-platform-verification",
|
||||
`${prefix} must withhold the production updater feed until both platforms are verified.`,
|
||||
);
|
||||
}
|
||||
assert(/^\d{4}-\d{2}-\d{2}$/.test(exception?.approvedOn || ""), `${prefix} must record an approval date.`);
|
||||
assert(
|
||||
typeof exception?.reason === "string" && exception.reason.trim().length >= 30,
|
||||
@@ -81,6 +89,9 @@ export const resolveDesktopReleasePolicy = (version, policy) => {
|
||||
artifactLabel: "UNSIGNED-PLATFORM",
|
||||
distribution: exception.distribution,
|
||||
warningRequired: true,
|
||||
macosLocalExactTagFallback: exception.macosLocalExactTagFallback === true,
|
||||
windowsDelivery: exception.windowsDelivery,
|
||||
updaterFeedActivation: exception.updaterFeedActivation,
|
||||
exception,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -70,6 +70,25 @@ const validateBaseUrl = () => {
|
||||
);
|
||||
};
|
||||
|
||||
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");
|
||||
@@ -173,6 +192,7 @@ if (requiresWindowsSigning) {
|
||||
}
|
||||
|
||||
validateBaseUrl();
|
||||
validateClientServerUrl();
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`Desktop release readiness check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||
|
||||
@@ -387,6 +387,19 @@ const verifyUpdaterBoundary = async () => {
|
||||
assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials.");
|
||||
};
|
||||
|
||||
const verifyDesktopServerConfigurationBoundary = async () => {
|
||||
const source = await readFile(resolve(sourceDir, "runtime/desktopServerConfig.ts"), "utf8");
|
||||
assert(
|
||||
source.includes("import.meta.env.VITE_DESKTOP_SERVER_URL"),
|
||||
"Desktop server defaults must come from VITE_DESKTOP_SERVER_URL.",
|
||||
);
|
||||
assert(!source.includes("ctms.huapont.cn"), "The production CTMS origin must not be hard-coded in Desktop runtime source.");
|
||||
assert(
|
||||
source.includes("getStoredDesktopServerUrl() || getDefaultDesktopServerUrl()"),
|
||||
"A manually stored Desktop server URL must override the build-time default.",
|
||||
);
|
||||
};
|
||||
|
||||
const verifyWorkflowGates = async () => {
|
||||
const packageInfo = await readJson(resolve(frontendDir, "package.json"));
|
||||
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
|
||||
@@ -406,6 +419,18 @@ const verifyWorkflowGates = async () => {
|
||||
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
|
||||
}
|
||||
|
||||
const provenanceSource = await readFile(resolve(frontendDir, "scripts/create-desktop-release-provenance.mjs"), "utf8");
|
||||
assert(provenanceSource.includes('"macos-first"'), "Release provenance must support the approved v0.1.0 macOS-first stage.");
|
||||
assert(
|
||||
provenanceSource.includes("withheld-pending-combined-verification"),
|
||||
"Staged macOS provenance must record that updater feed activation is withheld.",
|
||||
);
|
||||
const windowsPendingNotice = await readFile(resolve(frontendDir, "desktop-release-windows-pending.txt"), "utf8");
|
||||
assert(
|
||||
windowsPendingNotice.includes("same immutable v0.1.0 tag") && windowsPendingNotice.includes("latest.json is intentionally withheld"),
|
||||
"The staged v0.1.0 release must include an explicit Windows-pending and updater-feed notice.",
|
||||
);
|
||||
|
||||
const releasePolicy = await readJson(resolve(frontendDir, "desktop-release-policy.json"));
|
||||
assert(releasePolicy.schemaVersion === 1, "Desktop release policy schemaVersion must be 1.");
|
||||
assert(
|
||||
@@ -418,6 +443,12 @@ const verifyWorkflowGates = async () => {
|
||||
);
|
||||
if (packageInfo.version === "0.1.0") {
|
||||
assert(Boolean(currentUnsignedException), "Desktop release policy must record the approved v0.1.0 exception.");
|
||||
assert(
|
||||
currentUnsignedException?.macosLocalExactTagFallback === true &&
|
||||
currentUnsignedException?.windowsDelivery === "deferred-same-tag" &&
|
||||
currentUnsignedException?.updaterFeedActivation === "after-combined-platform-verification",
|
||||
"The v0.1.0 exception must constrain local macOS staging, deferred Windows delivery, and updater feed activation.",
|
||||
);
|
||||
}
|
||||
if (currentUnsignedException) {
|
||||
assert(
|
||||
@@ -444,6 +475,7 @@ const verifyWorkflowGates = async () => {
|
||||
}
|
||||
assert(workflow.includes("VITE_BUILD_CHANNEL"), "Client quality gates workflow must inject VITE_BUILD_CHANNEL.");
|
||||
assert(workflow.includes("VITE_BUILD_COMMIT"), "Client quality gates workflow must inject VITE_BUILD_COMMIT.");
|
||||
assert(workflow.includes("VITE_DESKTOP_SERVER_URL"), "Client quality gates workflow must inject VITE_DESKTOP_SERVER_URL.");
|
||||
assert(workflow.match(/node-version: "22\.13"/g)?.length === 2, "Client quality gates must use Node.js 22.13 for Web and Desktop jobs.");
|
||||
|
||||
const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8");
|
||||
@@ -463,6 +495,7 @@ const verifyWorkflowGates = async () => {
|
||||
"DESKTOP-RELEASE-PROVENANCE.json",
|
||||
"--require-provenance",
|
||||
"TAURI_SIGNING_PRIVATE_KEY",
|
||||
"VITE_DESKTOP_SERVER_URL",
|
||||
"APPLE_ID",
|
||||
"APPLE_PASSWORD",
|
||||
"APPLE_TEAM_ID",
|
||||
@@ -496,6 +529,7 @@ const verifyWorkflowGates = async () => {
|
||||
"runs-on: windows-latest",
|
||||
"VITE_BUILD_CHANNEL",
|
||||
"VITE_BUILD_COMMIT",
|
||||
"VITE_DESKTOP_SERVER_URL",
|
||||
"npm run release:env:check",
|
||||
"npm run version:check",
|
||||
"npm run runtime:check",
|
||||
@@ -554,6 +588,7 @@ await verifySourceSafety();
|
||||
await verifyNotificationBoundary();
|
||||
await verifySessionBoundary();
|
||||
await verifyUpdaterBoundary();
|
||||
await verifyDesktopServerConfigurationBoundary();
|
||||
await verifyOnlyOfficeBoundary();
|
||||
await verifyWorkflowGates();
|
||||
|
||||
|
||||
@@ -35,6 +35,25 @@ const isNativeDesktopRelease = Boolean(
|
||||
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);
|
||||
@@ -61,6 +80,7 @@ assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...al
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user