返回 DeepSeek-Reasonix
release-publication-ledger.mjs
根目录 / scripts / release-publication-ledger.mjs
1 import { readFileSync, writeFileSync } from "node:fs";
2 import path from "node:path";
3 import { pathToFileURL } from "node:url";
4
5 const VERSION_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
6 const SHA_RE = /^[0-9a-f]{40}$/;
7
8 export const npmPackageNames = [
9 "reasonix",
10 "@reasonix/cli-darwin-arm64",
11 "@reasonix/cli-darwin-x64",
12 "@reasonix/cli-linux-arm64",
13 "@reasonix/cli-linux-x64",
14 "@reasonix/cli-win32-arm64",
15 "@reasonix/cli-win32-x64",
16 ];
17
18 function compareStable(a, b) {
19 if (!VERSION_RE.test(a) || !VERSION_RE.test(b)) throw new Error("invalid stable version in publication observation");
20 const aa = a.split(".").map(Number);
21 const bb = b.split(".").map(Number);
22 for (let index = 0; index < aa.length; index += 1) {
23 if (aa[index] !== bb[index]) return aa[index] > bb[index] ? 1 : -1;
24 }
25 return 0;
26 }
27
28 export function ownsPublicSite(version, operation, manifest) {
29 if (!["publish", "recover"].includes(operation)) throw new Error("invalid publication operation");
30 const current = manifest?.version;
31 if (typeof current !== "string" || !current.startsWith("v")) throw new Error("missing or invalid Stable manifest version");
32 const comparison = compareStable(current.slice(1), version);
33 if (comparison === 0) return true;
34 if (comparison > 0 && operation === "recover") return false;
35 throw new Error(`Stable manifest serves ${current}, want v${version}`);
36 }
37
38 function requireIdentity(version, sourceSHA, operation) {
39 if (!VERSION_RE.test(version)) throw new Error("invalid publication ledger version");
40 if (!SHA_RE.test(sourceSHA)) throw new Error("invalid publication ledger source SHA");
41 if (!["publish", "recover"].includes(operation)) throw new Error("invalid publication operation");
42 }
43
44 function releaseAssets(release, surface) {
45 if (release?.isDraft !== false || release?.isPrerelease !== false || !Array.isArray(release.assets)) {
46 throw new Error(`${surface} release is not a public final release`);
47 }
48 return release.assets.map(asset => ({
49 name: asset.name,
50 size: asset.size,
51 digest: asset.digest || null,
52 state: "identity-verified",
53 })).sort((a, b) => a.name.localeCompare(b.name));
54 }
55
56 export function createCoreLedger({ version, sourceSHA, operation, cliRelease, desktopRelease, npmPackages, observedAt = new Date().toISOString() }) {
57 requireIdentity(version, sourceSHA, operation);
58 if (!Array.isArray(npmPackages) || npmPackages.length !== npmPackageNames.length) {
59 throw new Error("publication ledger requires all npm packages");
60 }
61 const packages = npmPackages.map(item => {
62 if (!npmPackageNames.includes(item.name) || item.version !== version || !item.integrity) {
63 throw new Error(`invalid npm publication observation: ${item.name ?? "<unknown>"}`);
64 }
65 if ((item.reasonixCandidateSha && item.reasonixCandidateSha !== sourceSHA)
66 || (item.gitHead && item.gitHead !== sourceSHA)
67 || (!item.reasonixCandidateSha && !item.gitHead)) {
68 throw new Error(`npm package does not match the candidate: ${item.name}`);
69 }
70 const pointerComparison = compareStable(item.latest, version);
71 if (pointerComparison < 0 || (operation === "publish" && pointerComparison !== 0)) {
72 throw new Error(`npm latest is inconsistent for ${item.name}: ${item.latest}`);
73 }
74 return {
75 name: item.name,
76 version: item.version,
77 integrity: item.integrity,
78 latest: item.latest,
79 state: "identity-verified",
80 pointerState: pointerComparison === 0 ? "public-entry-updated" : "newer-entry-preserved",
81 };
82 }).sort((a, b) => a.name.localeCompare(b.name));
83 if (new Set(packages.map(item => item.name)).size !== npmPackageNames.length) {
84 throw new Error("publication ledger contains duplicate npm packages");
85 }
86 for (const name of npmPackageNames) {
87 if (!packages.some(item => item.name === name)) throw new Error(`publication ledger is missing npm package: ${name}`);
88 }
89 return {
90 schema: 1,
91 version,
92 sourceSHA,
93 operation,
94 observedAt,
95 surfaces: {
96 tags: {
97 state: "identity-verified",
98 items: [`v${version}`, `npm-v${version}`, `desktop-v${version}`].map(name => ({ name, sha: sourceSHA })),
99 },
100 cli: { state: "identity-verified", assets: releaseAssets(cliRelease, "CLI") },
101 npm: { state: "identity-verified", packages },
102 desktop: { state: "identity-verified", assets: releaseAssets(desktopRelease, "Desktop") },
103 },
104 };
105 }
106
107 export function createSiteLedger({ version, sourceSHA, operation, manifest, observedAt = new Date().toISOString() }) {
108 requireIdentity(version, sourceSHA, operation);
109 if (manifest?.version !== `v${version}`) throw new Error("Stable manifest does not match the publication ledger");
110 return {
111 schema: 1,
112 version,
113 sourceSHA,
114 operation,
115 observedAt,
116 surfaces: {
117 stableManifest: { state: "public-entry-updated", version: manifest.version },
118 homepage: { state: "public-entry-updated", version: `v${version}` },
119 changelog: { state: "public-entry-updated", version: `v${version}` },
120 homebrew: { state: "public-entry-updated", version },
121 },
122 };
123 }
124
125 export function mergeLedgers(core, site, observedAt = new Date().toISOString()) {
126 if (core.schema !== 1 || site.schema !== 1 || core.version !== site.version
127 || core.sourceSHA !== site.sourceSHA || core.operation !== site.operation) {
128 throw new Error("publication ledger fragments do not describe one release");
129 }
130 return { ...core, observedAt, surfaces: { ...core.surfaces, ...site.surfaces } };
131 }
132
133 function read(file) {
134 return JSON.parse(readFileSync(file, "utf8"));
135 }
136
137 if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
138 const [command, ...args] = process.argv.slice(2);
139 if (command === "site-owner" && args.length === 3) {
140 const [version, operation, manifestPath] = args;
141 console.log(ownsPublicSite(version, operation, read(manifestPath)));
142 } else if (command === "core" && args.length === 7) {
143 const [version, sourceSHA, operation, cliPath, desktopPath, npmPath, output] = args;
144 writeFileSync(output, `${JSON.stringify(createCoreLedger({ version, sourceSHA, operation, cliRelease: read(cliPath), desktopRelease: read(desktopPath), npmPackages: read(npmPath) }), null, 2)}\n`);
145 } else if (command === "site" && args.length === 5) {
146 const [version, sourceSHA, operation, manifestPath, output] = args;
147 writeFileSync(output, `${JSON.stringify(createSiteLedger({ version, sourceSHA, operation, manifest: read(manifestPath) }), null, 2)}\n`);
148 } else if (command === "merge" && args.length === 3) {
149 const [corePath, sitePath, output] = args;
150 writeFileSync(output, `${JSON.stringify(mergeLedgers(read(corePath), read(sitePath)), null, 2)}\n`);
151 } else {
152 throw new Error("usage: release-publication-ledger.mjs core VERSION SHA OPERATION CLI DESKTOP NPM OUTPUT | site VERSION SHA OPERATION MANIFEST OUTPUT | merge CORE SITE OUTPUT");
153 }
154 }
155
155 lines Plain Text