| 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 |