build(desktop): allow controlled unsigned v0.1.0 release
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, 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 packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||
const args = process.argv.slice(2);
|
||||
const value = (name) => {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : undefined;
|
||||
};
|
||||
|
||||
const policy = await loadDesktopReleasePolicy();
|
||||
const resolution = resolveDesktopReleasePolicy(packageInfo.version, policy);
|
||||
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 outputPath = resolve(
|
||||
frontendDir,
|
||||
value("--output") || "src-tauri/target/desktop-release-feed/DESKTOP-RELEASE-PROVENANCE.json",
|
||||
);
|
||||
|
||||
const failures = [];
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) failures.push(message);
|
||||
};
|
||||
|
||||
assert(tag === `v${packageInfo.version}`, `Release provenance tag must be v${packageInfo.version}.`);
|
||||
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}.`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Desktop release provenance creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||
}
|
||||
|
||||
const provenance = {
|
||||
schemaVersion: 1,
|
||||
product: "CTMS Desktop",
|
||||
version: packageInfo.version,
|
||||
tag,
|
||||
commit,
|
||||
platformSigningMode: resolution.platformSigningMode,
|
||||
platformSigning: {
|
||||
macos: macosSigning,
|
||||
windows: windowsSigning,
|
||||
},
|
||||
updaterSigning: "required-and-verified",
|
||||
artifactLabel: resolution.artifactLabel,
|
||||
distribution: resolution.distribution,
|
||||
warnings: resolution.warningRequired
|
||||
? [
|
||||
"macOS is ad-hoc signed and not Apple-notarized; Gatekeeper warnings are expected.",
|
||||
"Windows is not Authenticode-signed or RFC 3161 timestamped; SmartScreen warnings are expected.",
|
||||
"Verify the release tag, commit, updater signatures, and SHA256SUMS.txt through a trusted channel before installation.",
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(provenance, null, 2)}\n`);
|
||||
console.log(`Desktop release provenance created at ${outputPath}`);
|
||||
@@ -0,0 +1,86 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||
export const desktopReleasePolicyPath = resolve(frontendDir, "desktop-release-policy.json");
|
||||
|
||||
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
||||
|
||||
const validatePolicy = (policy) => {
|
||||
const failures = [];
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) failures.push(message);
|
||||
};
|
||||
|
||||
assert(policy?.schemaVersion === 1, "desktop release policy schemaVersion must be 1.");
|
||||
assert(
|
||||
policy?.defaultPlatformSigningMode === "signed",
|
||||
"desktop release policy must default platform signing to signed.",
|
||||
);
|
||||
assert(policy?.updaterSigningRequired === true, "desktop release policy must always require updater signing.");
|
||||
assert(
|
||||
Array.isArray(policy?.unsignedPlatformExceptions),
|
||||
"desktop release policy unsignedPlatformExceptions must be an array.",
|
||||
);
|
||||
|
||||
const versions = new Set();
|
||||
for (const exception of policy?.unsignedPlatformExceptions || []) {
|
||||
const prefix = `unsigned platform exception ${exception?.version || "<missing>"}`;
|
||||
assert(semverPattern.test(exception?.version || ""), `${prefix} must use an exact X.Y.Z version.`);
|
||||
assert(!versions.has(exception?.version), `${prefix} is duplicated.`);
|
||||
versions.add(exception?.version);
|
||||
assert(exception?.status === "approved", `${prefix} must have status approved.`);
|
||||
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.`);
|
||||
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,
|
||||
`${prefix} must record a substantive reason.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new Error(failures.join("\n"));
|
||||
}
|
||||
};
|
||||
|
||||
export const loadDesktopReleasePolicy = async () => {
|
||||
const policy = JSON.parse(await readFile(desktopReleasePolicyPath, "utf8"));
|
||||
validatePolicy(policy);
|
||||
return policy;
|
||||
};
|
||||
|
||||
export const resolveDesktopReleasePolicy = (version, policy) => {
|
||||
if (!semverPattern.test(version || "")) {
|
||||
throw new Error(`Desktop release version must use exact X.Y.Z semver; found ${version || "<missing>"}.`);
|
||||
}
|
||||
|
||||
const exception = policy.unsignedPlatformExceptions.find(
|
||||
(candidate) => candidate.version === version && candidate.status === "approved",
|
||||
);
|
||||
|
||||
if (!exception) {
|
||||
return {
|
||||
version,
|
||||
platformSigningMode: "signed",
|
||||
macosSigning: "apple-developer",
|
||||
windowsSigning: "authenticode",
|
||||
artifactLabel: "SIGNED-PLATFORM",
|
||||
distribution: "formal",
|
||||
warningRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
platformSigningMode: "unsigned-exception",
|
||||
macosSigning: exception.macos,
|
||||
windowsSigning: exception.windows,
|
||||
artifactLabel: "UNSIGNED-PLATFORM",
|
||||
distribution: exception.distribution,
|
||||
warningRequired: true,
|
||||
exception,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { appendFile, 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 packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||
const args = process.argv.slice(2);
|
||||
const value = (name) => {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : undefined;
|
||||
};
|
||||
|
||||
const policy = await loadDesktopReleasePolicy();
|
||||
const resolution = resolveDesktopReleasePolicy(packageInfo.version, policy);
|
||||
const expectedTag = `v${packageInfo.version}`;
|
||||
|
||||
if (args.includes("--require-tag")) {
|
||||
if (process.env.GITHUB_REF_TYPE !== "tag" || process.env.GITHUB_REF_NAME !== expectedTag) {
|
||||
throw new Error(
|
||||
`Desktop release policy must be resolved from tag ${expectedTag}; found ${process.env.GITHUB_REF_TYPE || "<missing>"} ${process.env.GITHUB_REF_NAME || "<missing>"}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const outputs = {
|
||||
version: resolution.version,
|
||||
platform_signing_mode: resolution.platformSigningMode,
|
||||
macos_signing: resolution.macosSigning,
|
||||
windows_signing: resolution.windowsSigning,
|
||||
artifact_label: resolution.artifactLabel,
|
||||
distribution: resolution.distribution,
|
||||
warning_required: String(resolution.warningRequired),
|
||||
updater_signing_required: String(policy.updaterSigningRequired),
|
||||
};
|
||||
|
||||
const githubOutput = value("--github-output");
|
||||
if (githubOutput) {
|
||||
await appendFile(githubOutput, `${Object.entries(outputs).map(([key, output]) => `${key}=${output}`).join("\n")}\n`);
|
||||
}
|
||||
|
||||
console.log("Desktop release policy check passed.");
|
||||
console.log(` version: ${resolution.version}`);
|
||||
console.log(` platform signing mode: ${resolution.platformSigningMode}`);
|
||||
console.log(` macOS: ${resolution.macosSigning}`);
|
||||
console.log(` Windows: ${resolution.windowsSigning}`);
|
||||
console.log(" updater signing: required");
|
||||
@@ -2,10 +2,13 @@ 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;
|
||||
@@ -13,6 +16,10 @@ 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"];
|
||||
@@ -38,7 +45,7 @@ const gitMaybe = (args) => {
|
||||
};
|
||||
|
||||
const requireEnv = (name) => {
|
||||
assert(Boolean(env[name]), `${name} must be configured for signed desktop release readiness.`);
|
||||
assert(Boolean(env[name]), `${name} must be configured for desktop release readiness.`);
|
||||
};
|
||||
|
||||
const validateBaseUrl = () => {
|
||||
@@ -95,19 +102,54 @@ if (headSha && env.VITE_BUILD_COMMIT) {
|
||||
assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit.");
|
||||
}
|
||||
|
||||
assert(
|
||||
requiresMacosSigning || requiresWindowsSigning,
|
||||
"Release readiness requires REQUIRE_DESKTOP_SIGNING=true or REQUIRE_WINDOWS_SIGNING=true.",
|
||||
);
|
||||
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) {
|
||||
|
||||
@@ -62,7 +62,7 @@ const verifyTauriConfig = async () => {
|
||||
assert(targetList.includes("dmg"), "Tauri bundle targets must include dmg for macOS distribution.");
|
||||
assert(
|
||||
tauriConfig.bundle?.createUpdaterArtifacts === true,
|
||||
"Tauri must create updater artifacts for signed desktop release builds.",
|
||||
"Tauri must create signed updater artifacts for formal desktop release builds.",
|
||||
);
|
||||
assert(
|
||||
typeof tauriConfig.plugins?.updater?.pubkey === "string" && tauriConfig.plugins.updater.pubkey.length > 80,
|
||||
@@ -392,7 +392,10 @@ const verifyWorkflowGates = async () => {
|
||||
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
|
||||
const requiredScripts = [
|
||||
"desktop:build:macos-release",
|
||||
"desktop:build:macos-unsigned-release",
|
||||
"desktop:build:windows-release",
|
||||
"desktop:release-policy:check",
|
||||
"desktop:release-provenance:create",
|
||||
"desktop:update-feed:create",
|
||||
"desktop:update-feed:check",
|
||||
"desktop:release-readiness:check",
|
||||
@@ -403,6 +406,26 @@ const verifyWorkflowGates = async () => {
|
||||
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
|
||||
}
|
||||
|
||||
const releasePolicy = await readJson(resolve(frontendDir, "desktop-release-policy.json"));
|
||||
assert(releasePolicy.schemaVersion === 1, "Desktop release policy schemaVersion must be 1.");
|
||||
assert(
|
||||
releasePolicy.defaultPlatformSigningMode === "signed",
|
||||
"Desktop release policy must default future versions to signed platform releases.",
|
||||
);
|
||||
assert(releasePolicy.updaterSigningRequired === true, "Desktop release policy must require updater signing.");
|
||||
const currentUnsignedException = releasePolicy.unsignedPlatformExceptions?.find(
|
||||
(exception) => exception.version === packageInfo.version,
|
||||
);
|
||||
if (packageInfo.version === "0.1.0") {
|
||||
assert(Boolean(currentUnsignedException), "Desktop release policy must record the approved v0.1.0 exception.");
|
||||
}
|
||||
if (currentUnsignedException) {
|
||||
assert(
|
||||
currentUnsignedException.macos === "ad-hoc" && currentUnsignedException.windows === "unsigned",
|
||||
`The v${packageInfo.version} exception must use macOS ad-hoc and unsigned Windows modes.`,
|
||||
);
|
||||
}
|
||||
|
||||
const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8");
|
||||
const requiredCommands = [
|
||||
"npm run version:check",
|
||||
@@ -429,6 +452,16 @@ const verifyWorkflowGates = async () => {
|
||||
"runs-on: windows-latest",
|
||||
"REQUIRE_DESKTOP_SIGNING",
|
||||
"REQUIRE_WINDOWS_SIGNING",
|
||||
"REQUIRE_UPDATER_SIGNING",
|
||||
"ALLOW_UNSIGNED_PLATFORM_RELEASE",
|
||||
"DESKTOP_PLATFORM_SIGNING_MODE",
|
||||
"desktop:release-policy:check",
|
||||
"desktop:build:macos-unsigned-release",
|
||||
"Signature=adhoc",
|
||||
'"NotSigned"',
|
||||
"UNSIGNED-PLATFORM",
|
||||
"DESKTOP-RELEASE-PROVENANCE.json",
|
||||
"--require-provenance",
|
||||
"TAURI_SIGNING_PRIVATE_KEY",
|
||||
"APPLE_ID",
|
||||
"APPLE_PASSWORD",
|
||||
|
||||
@@ -2,9 +2,12 @@ import { access, readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename, 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 packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
||||
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
||||
const failures = [];
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -21,6 +24,11 @@ const feedPath = resolve(
|
||||
);
|
||||
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
||||
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
||||
const requiresProvenance = args.includes("--require-provenance");
|
||||
const provenancePath =
|
||||
optionValue("--provenance") ||
|
||||
process.env.DESKTOP_RELEASE_PROVENANCE ||
|
||||
(artifactDir ? resolve(artifactDir, "DESKTOP-RELEASE-PROVENANCE.json") : undefined);
|
||||
const requiredPlatforms = new Set([
|
||||
"darwin-aarch64",
|
||||
"darwin-x86_64",
|
||||
@@ -94,6 +102,56 @@ const assertManifestEntries = async (checksums) => {
|
||||
}
|
||||
};
|
||||
|
||||
const verifyProvenance = async (checksums) => {
|
||||
if (!requiresProvenance) return;
|
||||
if (!provenancePath) {
|
||||
fail("Desktop release provenance path is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
let provenance;
|
||||
try {
|
||||
provenance = JSON.parse(await readFile(provenancePath, "utf8"));
|
||||
} catch (error) {
|
||||
fail(`Cannot read desktop release provenance ${provenancePath}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(provenance.schemaVersion === 1, "Desktop release provenance schemaVersion must be 1.");
|
||||
assert(provenance.version === packageInfo.version, "Desktop release provenance version must match the package version.");
|
||||
assert(provenance.tag === `v${packageInfo.version}`, "Desktop release provenance tag must match the package version.");
|
||||
assert(/^[0-9a-f]{40}$/i.test(provenance.commit || ""), "Desktop release provenance must contain a full commit SHA.");
|
||||
assert(
|
||||
provenance.platformSigningMode === desktopReleaseResolution.platformSigningMode,
|
||||
`Desktop release provenance platformSigningMode must be ${desktopReleaseResolution.platformSigningMode}.`,
|
||||
);
|
||||
assert(
|
||||
provenance.platformSigning?.macos === desktopReleaseResolution.macosSigning,
|
||||
`Desktop release provenance macOS signing must be ${desktopReleaseResolution.macosSigning}.`,
|
||||
);
|
||||
assert(
|
||||
provenance.platformSigning?.windows === desktopReleaseResolution.windowsSigning,
|
||||
`Desktop release provenance Windows signing must be ${desktopReleaseResolution.windowsSigning}.`,
|
||||
);
|
||||
assert(
|
||||
provenance.updaterSigning === "required-and-verified",
|
||||
"Desktop release provenance must record required-and-verified updater signing.",
|
||||
);
|
||||
assert(
|
||||
provenance.artifactLabel === desktopReleaseResolution.artifactLabel,
|
||||
`Desktop release provenance artifactLabel must be ${desktopReleaseResolution.artifactLabel}.`,
|
||||
);
|
||||
if (desktopReleaseResolution.warningRequired) {
|
||||
assert(
|
||||
Array.isArray(provenance.warnings) && provenance.warnings.length >= 2,
|
||||
"Unsigned platform release provenance must include installation trust warnings.",
|
||||
);
|
||||
}
|
||||
if (artifactDir) {
|
||||
await assertChecksum(checksums, provenancePath, "desktop release provenance");
|
||||
}
|
||||
};
|
||||
|
||||
let feed;
|
||||
try {
|
||||
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
||||
@@ -104,6 +162,7 @@ try {
|
||||
if (feed) {
|
||||
const checksums = await readChecksumManifest();
|
||||
await assertManifestEntries(checksums);
|
||||
await verifyProvenance(checksums);
|
||||
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
||||
const platforms = feed.platforms || {};
|
||||
const darwinArm = platforms["darwin-aarch64"];
|
||||
|
||||
@@ -2,10 +2,13 @@ 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"]);
|
||||
@@ -20,6 +23,17 @@ 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 fail = (message) => failures.push(message);
|
||||
const assert = (condition, message) => {
|
||||
@@ -27,7 +41,7 @@ const assert = (condition, message) => {
|
||||
};
|
||||
|
||||
const requireEnv = (name) => {
|
||||
assert(Boolean(env[name]), `${name} must be configured for signed desktop release builds.`);
|
||||
assert(Boolean(env[name]), `${name} must be configured for this desktop release build.`);
|
||||
};
|
||||
|
||||
const gitHead = () => {
|
||||
@@ -74,13 +88,59 @@ assert(
|
||||
"macOS and Windows signing requirements must be checked in their native jobs.",
|
||||
);
|
||||
|
||||
if (requiresMacosSigning) {
|
||||
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
|
||||
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, "Signed desktop release candidate builds in CI must run from a release tag.");
|
||||
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");
|
||||
@@ -95,11 +155,6 @@ if (requiresMacosSigning) {
|
||||
|
||||
if (requiresWindowsSigning) {
|
||||
assert(process.platform === "win32", "Signed Windows desktop release builds must run on Windows.");
|
||||
if (isCi) {
|
||||
assert(isTagBuild, "Signed Windows desktop release candidate builds in CI must run from a release tag.");
|
||||
}
|
||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
||||
requireEnv("WINDOWS_CERTIFICATE");
|
||||
requireEnv("WINDOWS_CERTIFICATE_PASSWORD");
|
||||
requireEnv("WINDOWS_TIMESTAMP_URL");
|
||||
|
||||
Reference in New Issue
Block a user