完善桌面端界面、发布检查与邮箱域名同步
This commit is contained in:
@@ -48,6 +48,7 @@ const verifyTauriConfig = async () => {
|
||||
const targetList = Array.isArray(targets) ? targets : [targets].filter(Boolean);
|
||||
const csp = tauriConfig.app?.security?.csp || "";
|
||||
const cspTokens = csp.split(/[;\s]+/).filter(Boolean);
|
||||
const mainWindow = tauriConfig.app?.windows?.find((window) => window.label === "main") || tauriConfig.app?.windows?.[0];
|
||||
|
||||
assert(tauriConfig.bundle?.active === true, "Tauri bundle must be active for desktop release builds.");
|
||||
assert(targetList.includes("app"), "Tauri bundle targets must include app.");
|
||||
@@ -68,6 +69,8 @@ const verifyTauriConfig = async () => {
|
||||
"Tauri CSP must not use wildcard sources.",
|
||||
);
|
||||
assert(!/\bconnect-src\b[^;]*\bhttp:\b/.test(csp), "Tauri CSP must not allow broad http: API access.");
|
||||
assert(mainWindow?.minWidth === 1180, "Main desktop window must keep the minimum width at 1180.");
|
||||
assert(mainWindow?.minHeight === 760, "Main desktop window must keep the minimum height at 760.");
|
||||
};
|
||||
|
||||
const verifyCapabilities = async () => {
|
||||
@@ -151,16 +154,61 @@ const verifySourceSafety = async () => {
|
||||
const source = await readFile(path, "utf8");
|
||||
const file = relative(rootDir, path);
|
||||
assert(!/[?&]token=/.test(source), `${file}: token must not be passed through query parameters.`);
|
||||
assert(
|
||||
!/console\.(log|debug|info|warn|error)\s*\([^)]*token/i.test(source),
|
||||
`${file}: token-related values must not be written to console logs.`,
|
||||
);
|
||||
if (source.includes("ctms_token") && file !== "frontend/src/runtime/secureSessionStorage.ts") {
|
||||
fail(`${file}: ctms_token may only be handled by secureSessionStorage.`);
|
||||
}
|
||||
if (source.includes("sendNotification") && file !== "frontend/src/runtime/notifications.ts") {
|
||||
fail(`${file}: system notifications must be routed through frontend/src/runtime/notifications.ts.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const verifyNotificationBoundary = async () => {
|
||||
const source = await readFile(resolve(sourceDir, "runtime/notifications.ts"), "utf8");
|
||||
assert(source.includes('title: "CTMS 文件更新"'), "Desktop notification title must stay generic.");
|
||||
assert(source.includes('body: "有新的文件版本待查看"'), "Desktop notification body must stay generic.");
|
||||
assert(!/showSystemNotification\s*=\s*async\s*\([^)]*[a-zA-Z]/.test(source), "Desktop notification body must not accept dynamic business content.");
|
||||
};
|
||||
|
||||
const verifyUpdaterBoundary = async () => {
|
||||
const source = await readFile(resolve(tauriDir, "src/updates.rs"), "utf8");
|
||||
assert(source.includes('join("desktop-updates/stable/latest.json")'), "Desktop updater must derive the fixed stable latest.json path.");
|
||||
assert(source.includes("desktop updates require HTTPS outside localhost"), "Desktop updater must reject non-local HTTP update feeds.");
|
||||
assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials.");
|
||||
};
|
||||
|
||||
const verifyWorkflowGates = async () => {
|
||||
const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8");
|
||||
const requiredCommands = [
|
||||
"npm run version:check",
|
||||
"npm run runtime:check",
|
||||
"npm run desktop:release:check",
|
||||
"npm run ui:contract",
|
||||
"npm run type-check",
|
||||
"npm run test:unit",
|
||||
"npm run build",
|
||||
"npm run desktop:build:app",
|
||||
"npm run release:env:check",
|
||||
];
|
||||
|
||||
for (const command of requiredCommands) {
|
||||
assert(workflow.includes(command), `Client quality gates workflow must run ${command}.`);
|
||||
}
|
||||
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.");
|
||||
};
|
||||
|
||||
await verifyTauriConfig();
|
||||
await verifyCapabilities();
|
||||
await verifyRustBoundary();
|
||||
await verifySourceSafety();
|
||||
await verifyNotificationBoundary();
|
||||
await verifyUpdaterBoundary();
|
||||
await verifyWorkflowGates();
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`Desktop release gate failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||
const failures = [];
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const optionValue = (name) => {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : undefined;
|
||||
};
|
||||
|
||||
const feedPath = resolve(
|
||||
frontendDir,
|
||||
optionValue("--feed") || process.env.DESKTOP_UPDATE_FEED || "src-tauri/target/release/bundle/latest.json",
|
||||
);
|
||||
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
||||
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
||||
|
||||
const fail = (message) => failures.push(message);
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) fail(message);
|
||||
};
|
||||
|
||||
const assertFileExists = async (path, description) => {
|
||||
try {
|
||||
await access(path);
|
||||
} catch {
|
||||
fail(`${description} does not exist: ${path}`);
|
||||
}
|
||||
};
|
||||
|
||||
let feed;
|
||||
try {
|
||||
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
||||
} catch (error) {
|
||||
fail(`Cannot read desktop update feed ${feedPath}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (feed) {
|
||||
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
||||
const platforms = feed.platforms || {};
|
||||
const darwinArm = platforms["darwin-aarch64"];
|
||||
const darwinIntel = platforms["darwin-x86_64"];
|
||||
|
||||
assert(normalizedFeedVersion === packageInfo.version, `latest.json version must match package version ${packageInfo.version}.`);
|
||||
assert(Boolean(feed.pub_date || feed.pubDate), "latest.json must include a publication date.");
|
||||
assert(Boolean(darwinArm), "latest.json must include darwin-aarch64.");
|
||||
assert(Boolean(darwinIntel), "latest.json must include darwin-x86_64.");
|
||||
|
||||
const entries = [
|
||||
["darwin-aarch64", darwinArm],
|
||||
["darwin-x86_64", darwinIntel],
|
||||
];
|
||||
|
||||
for (const [platform, entry] of entries) {
|
||||
const rawUrl = entry?.url;
|
||||
const signature = entry?.signature;
|
||||
assert(typeof signature === "string" && signature.length > 80, `${platform} must include an updater signature.`);
|
||||
assert(typeof rawUrl === "string" && rawUrl.length > 0, `${platform} must include an artifact URL.`);
|
||||
if (!rawUrl) continue;
|
||||
|
||||
let url;
|
||||
try {
|
||||
url = new URL(rawUrl);
|
||||
} catch {
|
||||
fail(`${platform} artifact URL is invalid: ${rawUrl}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(url.protocol === "https:", `${platform} artifact URL must use HTTPS.`);
|
||||
assert(!/[?&]token=/i.test(url.search), `${platform} artifact URL must not contain token query parameters.`);
|
||||
assert(!url.pathname.endsWith("/latest.json"), `${platform} artifact URL must point to an immutable artifact, not latest.json.`);
|
||||
assert(url.pathname.includes(packageInfo.version), `${platform} artifact URL must include version ${packageInfo.version}.`);
|
||||
if (expectedBaseUrl) {
|
||||
assert(rawUrl.startsWith(expectedBaseUrl), `${platform} artifact URL must start with ${expectedBaseUrl}.`);
|
||||
}
|
||||
|
||||
if (artifactDir) {
|
||||
const artifactPath = resolve(artifactDir, basename(url.pathname));
|
||||
await assertFileExists(artifactPath, `${platform} updater artifact`);
|
||||
await assertFileExists(`${artifactPath}.sig`, `${platform} updater artifact signature`);
|
||||
}
|
||||
}
|
||||
|
||||
if (darwinArm?.url && darwinIntel?.url) {
|
||||
assert(darwinArm.url === darwinIntel.url, "Universal macOS latest.json must point both darwin architectures at the same artifact.");
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`Desktop update feed check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log("Desktop update feed check passed.");
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||
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 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 signed desktop release builds.`);
|
||||
};
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
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 (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.");
|
||||
}
|
||||
|
||||
if (env.REQUIRE_DESKTOP_SIGNING === "true") {
|
||||
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
|
||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
||||
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 (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.");
|
||||
}
|
||||
Reference in New Issue
Block a user