返回 DeepSeek-Reasonix
reconcile-release-publication.test.mjs
根目录 / scripts / reconcile-release-publication.test.mjs
1 import assert from "node:assert/strict";
2 import test from "node:test";
3 import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, rmSync, realpathSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import path from "node:path";
6 import { spawnSync } from "node:child_process";
7 import { createCoreLedger, createPointerLedger, mergeLedgers, npmPackageNames } from "./release-publication-ledger.mjs";
8
9 const sha = "a".repeat(40);
10 function fixture(t) {
11 const root = realpathSync(mkdtempSync(path.join(tmpdir(), "release-reconcile-")));
12 t.after(() => rmSync(root, { recursive: true, force: true }));
13 const scripts = path.join(root, "scripts"), bin = path.join(root, "bin");
14 mkdirSync(scripts); mkdirSync(bin);
15 for (const file of ["reconcile-release-publication.sh", "fetch-stable-release-manifest.sh", "check-release-public-access.sh", "release-publication-ledger.mjs"]) copyFileSync(`scripts/${file}`, path.join(scripts, file));
16 const release = { isDraft: false, isPrerelease: false, assets: [] };
17 const core = createCoreLedger({ version: "1.2.3", sourceSHA: sha, operation: "recover", cliRelease: release, desktopRelease: release, npmPackages: npmPackageNames.map(name => ({ name, version: "1.2.3", latest: "1.2.3", gitHead: sha, integrity: "sha512-example" })) });
18 writeFileSync(path.join(root, "core.json"), JSON.stringify(core));
19 writeFileSync(path.join(root, "full.json"), JSON.stringify(mergeLedgers(core, createPointerLedger({ version: "1.2.3", sourceSHA: sha, operation: "recover", manifest: { version: "v1.2.3" }, homebrewVersion: "1.2.3" }))));
20 writeFileSync(path.join(scripts, "verify-stable-release-artifacts.sh"), `#!/usr/bin/env bash
21 set -euo pipefail
22 test "$VERIFY_PUBLIC_POINTERS" = true
23 test "\${VERIFY_FAIL:-}" != true
24 if [ "\${NEWER_POINTER:-}" = true ]; then cp "$FIXTURE/core.json" "$RELEASE_LEDGER_OUTPUT"; else cp "$FIXTURE/full.json" "$RELEASE_LEDGER_OUTPUT"; fi
25 `);
26 writeFileSync(path.join(scripts, "release-event.mjs"), `import {writeFileSync} from 'node:fs'; writeFileSync(process.argv[process.argv.indexOf('--output')+1], '{}');`);
27 writeFileSync(path.join(bin, "go"), `#!/usr/bin/env node
28 const args=process.argv.slice(2);
29 if (args[0]!=='run' || !args[1].endsWith('/release-manifest-fetch/main.go') || args[2]!=='1.2.3') process.exit(99);
30 if(process.env.HTTP_FAIL==='true') process.exit(22);
31 const body=process.env.MANIFEST_BODY || JSON.stringify({version:process.env.POINTER || 'v1.2.3'});
32 try { if(!/^v[0-9]+\\.[0-9]+\\.[0-9]+$/.test(JSON.parse(body).version)) process.exit(1); }
33 catch { process.exit(1); }
34 require('node:fs').writeFileSync(args[3],body);
35 `, { mode: 0o755 });
36 writeFileSync(path.join(bin, "gh"), `#!/usr/bin/env node
37 const fs=require('node:fs'), path=require('node:path');
38 const args=process.argv.slice(2), root=process.env.FIXTURE;
39 fs.appendFileSync(path.join(root,'calls'),JSON.stringify(args)+'\\n');
40 if(args[0]==='api') console.log('2026-01-01T00:00:00Z');
41 else if(args[0]==='release' && args[1]==='view') console.log(process.env.MISSING_EVENT==='true'?'0':'1');
42 else if(args[0]==='release' && args[1]==='download') {
43 if(process.env.EVENT_DOWNLOAD_FAIL==='true') process.exit(1);
44 fs.writeFileSync(args[args.indexOf('--output')+1],process.env.EVENT_CONFLICT==='true'?'conflict':'{}');
45 } else if(args[0]==='release' && args[1]==='upload') {
46 if(path.basename(args.at(-1))!=='release-event.json') process.exit(3);
47 } else process.exit(4);
48 `, { mode: 0o755 });
49 const env = { ...process.env, FIXTURE: root, PATH: `${bin}:${process.env.PATH}`, GITHUB_ACTIONS: "true", GITHUB_REPOSITORY: "esengine/DeepSeek-Reasonix", GITHUB_REF: "refs/heads/main-v2", GITHUB_REF_PROTECTED: "true", GITHUB_RUN_ID: "123", GITHUB_RUN_ATTEMPT: "2", RELEASE_REPOSITORY: "esengine/DeepSeek-Reasonix", RELEASE_VERSION: "1.2.3", RELEASE_OPERATION: "recover", RELEASE_EXPECTED_SHA: sha, RELEASE_LEDGER_OUTPUT: path.join(root, "ledger.json") };
50 return { root, scripts, env,
51 run: extra => spawnSync("bash", [path.join(scripts, "reconcile-release-publication.sh")], { env: { ...env, ...extra }, encoding: "utf8" }),
52 ledger: () => JSON.parse(readFileSync(env.RELEASE_LEDGER_OUTPUT, "utf8")),
53 calls: () => { try { return readFileSync(path.join(root, "calls"), "utf8"); } catch { return ""; } },
54 };
55 }
56
57 test("publication completes once every product surface is verified", t => {
58 const f = fixture(t), result = f.run({});
59 assert.equal(result.status, 0, result.stderr);
60 const ledger = f.ledger();
61 assert.equal(ledger.completionState, "complete");
62 assert.deepEqual(Object.keys(ledger.surfaces).sort(), ["cli", "desktop", "homebrew", "npm", "stableManifest", "tags"]);
63 assert.equal(ledger.verificationContext.stage, "complete");
64 assert.doesNotMatch(f.calls(), /"(?:upload|publish|push)"/);
65 });
66
67 test("newer pointer is preserved and still counts as recovered", t => {
68 const f = fixture(t), result = f.run({ NEWER_POINTER: "true" });
69 assert.equal(result.status, 0, result.stderr);
70 assert.equal(f.ledger().completionState, "immutable-complete-newer-pointer-preserved");
71 });
72
73 for (const [extra, stage] of [
74 [{ VERIFY_FAIL: "true" }, "immutable"],
75 [{ EVENT_DOWNLOAD_FAIL: "true" }, "release-event"],
76 [{ EVENT_CONFLICT: "true" }, "release-event"],
77 ]) test(`failure evidence survives ${JSON.stringify(extra)}`, t => {
78 const f = fixture(t), result = f.run(extra);
79 assert.notEqual(result.status, 0);
80 const ledger = f.ledger();
81 assert.equal(ledger.verificationContext.stage, stage);
82 assert.equal(ledger.completionState, stage === "immutable" ? "verification-failed" : "release-event-pending");
83 assert.doesNotMatch(f.calls(), /upload/);
84 });
85
86 test("a missing event is created with the immutable asset filename", t => {
87 const f = fixture(t), result = f.run({ MISSING_EVENT: "true" });
88 assert.equal(result.status, 0, result.stderr);
89 assert.match(f.calls(), /release-event\.json/);
90 assert.match(f.calls(), /upload/);
91 });
92
93 test("public access preflight accepts the previous stable version but rejects challenges and invalid JSON", t => {
94 const f = fixture(t);
95 for (const [extra, success] of [[{ POINTER: "v1.0.0" }, true], [{ HTTP_FAIL: "true" }, false], [{ MANIFEST_BODY: "{}" }, false], [{ MANIFEST_BODY: "html" }, false]]) {
96 const result = spawnSync("bash", [path.join(f.scripts, "check-release-public-access.sh"), "1.2.3"], { env: { ...f.env, ...extra }, encoding: "utf8" });
97 assert.equal(result.status === 0, success, result.stderr);
98 }
99 });
100
101 test("real public verifier rejects tag drift before inspecting any packages", t => {
102 const f = fixture(t);
103 writeFileSync(path.join(f.root, "bin/git"), `#!/usr/bin/env node\nconsole.log('${"b".repeat(40)}\\trefs/tags/v1.2.3');`, { mode: 0o755 });
104 const result = spawnSync("bash", ["scripts/verify-stable-release-artifacts.sh"], { env: { ...f.env, CLI_TAG: "v1.2.3", DESKTOP_TAG: "desktop-v1.2.3" }, encoding: "utf8" });
105 assert.notEqual(result.status, 0);
106 assert.match(result.stderr, /differs from the verified source SHA/);
107 assert.equal(f.calls(), "");
108 });
109
110 test("the 1.x release control plane neither deploys nor verifies the website", () => {
111 const promote = readFileSync(".github/workflows/release-promote.yml", "utf8");
112 assert.match(readFileSync(".github/workflows/release-stable.yml", "utf8"), /group: stable-release-publication/);
113 assert.match(promote, /group: stable-release-publication/);
114 assert.match(promote, /bash scripts\/reconcile-release-publication.sh/);
115 assert.match(promote, /retention-days: 90/);
116 for (const file of [
117 ".github/workflows/release-promote.yml", ".github/workflows/release-stable.yml",
118 ".github/workflows/release-desktop.yml", ".github/workflows/release-verify.yml",
119 "scripts/reconcile-release-publication.sh", "scripts/verify-stable-release-artifacts.sh",
120 ]) {
121 const text = readFileSync(file, "utf8");
122 assert.doesNotMatch(text, /pages\.yml|--ref website|reasonix\.io\/changelog\/v\$|--dump-dom/, file);
123 }
124 });
125
125 lines Plain Text