返回 DeepSeek-Reasonix
desktop-release-artifacts.mjs
根目录 / scripts / desktop-release-artifacts.mjs
1 import { createHash } from "node:crypto";
2 import { copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3 import path from "node:path";
4 import { pathToFileURL } from "node:url";
5
6 export const platforms = ["darwin-arm64", "darwin-amd64", "darwin-universal", "windows-amd64", "windows-arm64", "linux-amd64"];
7 const fields = {
8 sourceSHA: "RELEASE_SOURCE_SHA", controlSHA: "RELEASE_CONTROL_SHA",
9 tag: "RELEASE_TAG", version: "RELEASE_VERSION", channel: "RELEASE_CHANNEL",
10 signingFingerprint: "RELEASE_SIGNING_FINGERPRINT", prefix: "RELEASE_ARTIFACT_PREFIX",
11 };
12
13 export function releaseIdentity(env) {
14 const identity = Object.fromEntries(Object.entries(fields).map(([key, variable]) => {
15 if (!env[variable]) throw new Error(`missing ${variable}`);
16 return [key, env[variable]];
17 }));
18 const match = /^desktop-([1-9][0-9]*)-([1-9][0-9]*)-(preflight|release)$/.exec(identity.prefix);
19 const producerRun = env.RELEASE_PRODUCER_RUN_ID ?? env.GITHUB_RUN_ID;
20 const producerAttempt = env.RELEASE_PRODUCER_RUN_ATTEMPT ?? env.GITHUB_RUN_ATTEMPT;
21 if (!match || match[1] !== producerRun || !/^[1-9][0-9]*$/.test(producerAttempt ?? "")
22 || BigInt(match[2]) > BigInt(producerAttempt)) throw new Error("artifact set is not from the verified producer run or a completed attempt");
23 if (![identity.sourceSHA, identity.controlSHA].every(sha => /^[a-f0-9]{40}$/.test(sha))) throw new Error("invalid release SHA");
24 return identity;
25 }
26
27 function entries(directory) {
28 return readdirSync(directory).sort().map(name => {
29 if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) throw new Error(`unsafe artifact name: ${name}`);
30 const file = path.join(directory, name);
31 const stat = lstatSync(file);
32 if (!stat.isFile() || stat.size === 0) throw new Error(`invalid artifact: ${name}`);
33 return { name, size: stat.size, sha256: createHash("sha256").update(readFileSync(file)).digest("hex") };
34 });
35 }
36
37 function requireSignatures(files) {
38 const names = new Set(files.map(file => file.name));
39 if (!names.size || !files.some(file => !file.name.endsWith(".minisig"))) throw new Error("empty artifact set");
40 for (const name of names) {
41 const companion = name.endsWith(".minisig") ? name.slice(0, -8) : `${name}.minisig`;
42 if (!names.has(companion)) throw new Error(`missing payload or signature for ${name}`);
43 }
44 }
45
46 function validAttempt(attempt, identity, currentAttempt) {
47 return /^[1-9][0-9]*$/.test(attempt ?? "") && /^[1-9][0-9]*$/.test(currentAttempt ?? "")
48 && BigInt(attempt) >= BigInt(identity.prefix.split("-")[2]) && BigInt(attempt) <= BigInt(currentAttempt);
49 }
50
51 export function verifyBundle(bundle, platform, identity, currentAttempt) {
52 if (!platforms.includes(platform)) throw new Error("invalid release platform");
53 if (!lstatSync(bundle).isDirectory() || lstatSync(bundle).isSymbolicLink()
54 || JSON.stringify(readdirSync(bundle).sort()) !== JSON.stringify(["files", "identity.json"])) {
55 throw new Error("invalid bundle layout");
56 }
57 if (!lstatSync(path.join(bundle, "identity.json")).isFile()
58 || lstatSync(path.join(bundle, "identity.json")).isSymbolicLink()
59 || !lstatSync(path.join(bundle, "files")).isDirectory()
60 || lstatSync(path.join(bundle, "files")).isSymbolicLink()) {
61 throw new Error("invalid bundle entries");
62 }
63 const manifest = JSON.parse(readFileSync(path.join(bundle, "identity.json"), "utf8"));
64 if (manifest.schema !== 1 || manifest.platform !== platform) throw new Error("invalid bundle identity");
65 if (!validAttempt(manifest.buildAttempt, identity, currentAttempt)) throw new Error("invalid producer attempt");
66 for (const [key, value] of Object.entries(identity)) {
67 if (manifest[key] !== value) throw new Error(`artifact identity mismatch: ${key}`);
68 }
69 const files = entries(path.join(bundle, "files"));
70 if (JSON.stringify(files) !== JSON.stringify(manifest.files)) throw new Error(`artifact digest mismatch: ${platform}`);
71 requireSignatures(files);
72 return { manifest, files };
73 }
74
75 export function pack(source, target, platform, identity, buildAttempt = identity.prefix.split("-")[2]) {
76 if (!platforms.includes(platform)) throw new Error("invalid release platform");
77 if (!validAttempt(buildAttempt, identity, buildAttempt)) throw new Error("invalid producer attempt");
78 const files = entries(source);
79 requireSignatures(files);
80 mkdirSync(target); // Never append to a stale bundle.
81 mkdirSync(path.join(target, "files"));
82 for (const { name } of files) copyFileSync(path.join(source, name), path.join(target, "files", name));
83 writeFileSync(path.join(target, "identity.json"), JSON.stringify({ schema: 1, ...identity, buildAttempt, platform, files }, null, 2));
84 }
85
86 export function collect(source, target, identity, currentAttempt = identity.prefix.split("-")[2]) {
87 const expected = platforms.map(platform => `${identity.prefix}-${platform}`).sort();
88 if (JSON.stringify(readdirSync(source).sort()) !== JSON.stringify(expected)) throw new Error("missing or unexpected platform bundle");
89 const copies = new Map();
90 for (const platform of platforms) {
91 const bundle = path.join(source, `${identity.prefix}-${platform}`);
92 const { files } = verifyBundle(bundle, platform, identity, currentAttempt);
93 for (const { name } of files) {
94 if (copies.has(name)) throw new Error(`duplicate artifact across platforms: ${name}`);
95 copies.set(name, path.join(bundle, "files", name));
96 }
97 }
98 // Complete identity and digest verification precedes any publication input.
99 mkdirSync(target);
100 for (const [name, file] of copies) copyFileSync(file, path.join(target, name));
101 }
102
103 if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
104 const [command, source, target, platform] = process.argv.slice(2);
105 const identity = releaseIdentity(process.env);
106 if (command === "pack") pack(source, target, platform, identity, process.env.GITHUB_RUN_ATTEMPT);
107 else if (command === "collect") collect(source, target, identity, process.env.RELEASE_PRODUCER_RUN_ATTEMPT ?? process.env.GITHUB_RUN_ATTEMPT);
108 else if (command === "verify") verifyBundle(source, target, identity, process.env.GITHUB_RUN_ATTEMPT);
109 else throw new Error("usage: desktop-release-artifacts.mjs pack|collect|verify SOURCE TARGET [PLATFORM]");
110 }
111
111 lines Plain Text