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