返回 CodeWhale
release-assets.test.js
根目录 / npm / codewhale / test / release-assets.test.js
1 const assert = require("node:assert/strict");
2 const { execFileSync } = require("node:child_process");
3 const fs = require("node:fs");
4 const os = require("node:os");
5 const path = require("node:path");
6 const test = require("node:test");
7
8 const pkg = require("../package.json");
9 const {
10 allReleaseAssetNames,
11 BUNDLE_ASSET_NAMES,
12 BUNDLE_CHECKSUM_MANIFEST,
13 CHECKSUM_MANIFEST,
14 checksummedReleaseAssetNames,
15 detectBinaryNames,
16 LEGACY_TUI_BRIDGE_ASSET_NAMES,
17 } = require("../scripts/artifacts");
18 const {
19 assertChecksumManifestIncludes,
20 assertPackageVersionMatchesBinaryVersion,
21 assertReleaseAssetsFresh,
22 findReleaseWorkflowRun,
23 parseChecksumManifest,
24 } = require("../scripts/verify-release-assets");
25
26 test("parseChecksumManifest accepts GNU and BSD filename forms", () => {
27 const manifest = parseChecksumManifest(
28 [
29 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa codewhale-linux-x64",
30 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb *codewhale-windows-x64.exe",
31 ].join("\n"),
32 );
33
34 assert.equal(manifest.get("codewhale-linux-x64"), "a".repeat(64));
35 assert.equal(manifest.get("codewhale-windows-x64.exe"), "b".repeat(64));
36 });
37
38 test("parseChecksumManifest rejects malformed checksum rows", () => {
39 assert.throws(
40 () => parseChecksumManifest("not-a-sha codewhale-linux-x64"),
41 /Invalid checksum manifest line/,
42 );
43 });
44
45 test("assertReleaseAssetsFresh rejects missing release assets", () => {
46 assert.throws(
47 () =>
48 assertReleaseAssetsFresh(
49 { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-06-26T00:10:00Z" }] },
50 ["codewhale-linux-x64", "codewhale-artifacts-sha256.txt"],
51 { database_id: 123, created_at: "2026-06-26T00:00:00Z" },
52 ),
53 /missing required release asset/,
54 );
55 });
56
57 test("assertChecksumManifestIncludes rejects missing bundle manifest and archive rows", () => {
58 const manifest = parseChecksumManifest(
59 `${"a".repeat(64)} codewhale-linux-x64.tar.gz`,
60 );
61
62 assert.throws(
63 () =>
64 assertChecksumManifestIncludes(
65 manifest,
66 ["codewhale-linux-x64.tar.gz", "codewhale-bundles-sha256.txt"],
67 "Canonical checksum manifest",
68 ),
69 /Canonical checksum manifest is missing codewhale-bundles-sha256\.txt/,
70 );
71 });
72
73 test("bundle checksum rows use public archive basenames", () => {
74 const manifest = parseChecksumManifest(
75 `${"a".repeat(64)} bundles/codewhale-linux-x64.tar.gz`,
76 );
77
78 assert.throws(
79 () =>
80 assertChecksumManifestIncludes(
81 manifest,
82 ["codewhale-linux-x64.tar.gz"],
83 "Bundle checksum manifest",
84 ),
85 /Bundle checksum manifest is missing codewhale-linux-x64\.tar\.gz/,
86 );
87 });
88
89 test("assertReleaseAssetsFresh rejects assets older than the release workflow run", () => {
90 assert.throws(
91 () =>
92 assertReleaseAssetsFresh(
93 { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-06-25T23:59:59Z" }] },
94 ["codewhale-linux-x64"],
95 { database_id: 123, created_at: "2026-06-26T00:00:00Z" },
96 ),
97 /asset set is stale/,
98 );
99 });
100
101 test("assertReleaseAssetsFresh rejects non-uploaded assets", () => {
102 assert.throws(
103 () =>
104 assertReleaseAssetsFresh(
105 { assets: [{ name: "codewhale-linux-x64", state: "new", updated_at: "2026-06-26T00:10:00Z" }] },
106 ["codewhale-linux-x64"],
107 { database_id: 123, created_at: "2026-06-26T00:00:00Z" },
108 ),
109 /asset set is stale/,
110 );
111 });
112
113 test("assertReleaseAssetsFresh accepts assets updated by the release workflow run", () => {
114 assert.doesNotThrow(() =>
115 assertReleaseAssetsFresh(
116 { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-06-26T00:10:00Z" }] },
117 ["codewhale-linux-x64"],
118 { database_id: 123, created_at: "2026-06-26T00:00:00Z" },
119 ),
120 );
121 });
122
123 test("assertReleaseAssetsFresh accepts assets uploaded before a job-level rerun bumped run_started_at (#5429)", () => {
124 assert.doesNotThrow(() =>
125 assertReleaseAssetsFresh(
126 { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-08-15T02:00:00Z" }] },
127 ["codewhale-linux-x64"],
128 {
129 database_id: 123,
130 // `gh run rerun --failed` moves the run-level start forward…
131 run_started_at: "2026-08-15T10:00:00Z",
132 // …but the release job that actually uploaded the assets is unchanged.
133 release_job_started_at: "2026-08-15T01:00:00Z",
134 },
135 ),
136 );
137 });
138
139 test("assertReleaseAssetsFresh still rejects assets older than the successful release job", () => {
140 assert.throws(
141 () =>
142 assertReleaseAssetsFresh(
143 { assets: [{ name: "codewhale-linux-x64", state: "uploaded", updated_at: "2026-08-15T00:30:00Z" }] },
144 ["codewhale-linux-x64"],
145 {
146 database_id: 123,
147 run_started_at: "2026-08-15T10:00:00Z",
148 release_job_started_at: "2026-08-15T01:00:00Z",
149 },
150 ),
151 /asset set is stale/,
152 );
153 });
154
155 test("findReleaseWorkflowRun accepts a successful release job when a downstream job failed", async () => {
156 const run = {
157 id: 123,
158 head_sha: "abc123",
159 head_branch: "v0.9.6",
160 event: "push",
161 conclusion: "failure",
162 updated_at: "2026-08-12T08:48:00Z",
163 };
164 const api = async (_repo, endpoint) => {
165 if (endpoint.includes("/workflows/release.yml/runs")) {
166 return { workflow_runs: [run] };
167 }
168 assert.equal(endpoint, "/actions/runs/123/jobs?per_page=100");
169 return {
170 jobs: [
171 { name: "release", conclusion: "success", started_at: "2026-08-12T07:30:00Z" },
172 { name: "npm", conclusion: "failure" },
173 ],
174 };
175 };
176
177 assert.deepEqual(await findReleaseWorkflowRun("owner/repo", "v0.9.6", "abc123", api), {
178 ...run,
179 release_job_started_at: "2026-08-12T07:30:00Z",
180 });
181 });
182
183 test("findReleaseWorkflowRun rejects runs without a successful release job", async () => {
184 const api = async (_repo, endpoint) => {
185 if (endpoint.includes("/workflows/release.yml/runs")) {
186 return {
187 workflow_runs: [
188 {
189 id: 123,
190 head_sha: "abc123",
191 head_branch: "v0.9.6",
192 event: "push",
193 conclusion: "failure",
194 updated_at: "2026-08-12T08:48:00Z",
195 },
196 ],
197 };
198 }
199 return { jobs: [{ name: "release", conclusion: "failure" }] };
200 };
201
202 await assert.rejects(
203 findReleaseWorkflowRun("owner/repo", "v0.9.6", "abc123", api),
204 /No successful asset-publishing job found/,
205 );
206 });
207
208 test("assertPackageVersionMatchesBinaryVersion allows packaging-only releases only with an explicit override", () => {
209 assert.doesNotThrow(() => assertPackageVersionMatchesBinaryVersion(pkg.version));
210 assert.throws(
211 () => assertPackageVersionMatchesBinaryVersion("0.0.0-packaging-test"),
212 /does not match codewhaleBinaryVersion/,
213 );
214
215 const previous = process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH;
216 process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH = "1";
217 try {
218 assert.doesNotThrow(() => assertPackageVersionMatchesBinaryVersion("0.0.0-packaging-test"));
219 } finally {
220 if (previous === undefined) {
221 delete process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH;
222 } else {
223 process.env.CODEWHALE_ALLOW_NPM_BINARY_MISMATCH = previous;
224 }
225 }
226 });
227
228 test("npm publication requires the checkout guard and canonical release-asset gate", () => {
229 assert.equal(
230 pkg.scripts.prepublishOnly,
231 "bash ../../scripts/release/require-release-tag-checkout.sh && " +
232 "bash ../../scripts/release/verify-release-assets.sh",
233 );
234 });
235
236 test("full local release fixture satisfies the public asset inventory", () => {
237 const repoRoot = path.resolve(__dirname, "..", "..", "..");
238 const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-assets-"));
239 const buildDir = path.join(fixtureRoot, "build");
240 const outputDir = path.join(fixtureRoot, "assets");
241 const executableSuffix = process.platform === "win32" ? ".exe" : "";
242
243 try {
244 fs.mkdirSync(buildDir, { recursive: true });
245 fs.writeFileSync(
246 path.join(buildDir, `codewhale${executableSuffix}`),
247 "fixture:codewhale\n",
248 );
249
250 execFileSync(
251 process.execPath,
252 [
253 path.join(repoRoot, "scripts", "release", "prepare-local-release-assets.js"),
254 outputDir,
255 buildDir,
256 ],
257 {
258 env: { ...process.env, DEEPSEEK_TUI_PREPARE_ALL_ASSETS: "1" },
259 stdio: "pipe",
260 },
261 );
262
263 for (const assetName of allReleaseAssetNames()) {
264 assert.equal(
265 fs.existsSync(path.join(outputDir, assetName)),
266 true,
267 `missing fixture asset ${assetName}`,
268 );
269 }
270
271 const { codewhale, codew } = detectBinaryNames();
272 assert.deepEqual(
273 fs.readFileSync(path.join(outputDir, codew)),
274 fs.readFileSync(path.join(outputDir, codewhale)),
275 "codew must contain the exact same runtime bytes as codewhale",
276 );
277 for (const legacyAsset of LEGACY_TUI_BRIDGE_ASSET_NAMES) {
278 const primaryAsset = legacyAsset.replace("codewhale-tui-", "codewhale-");
279 assert.deepEqual(
280 fs.readFileSync(path.join(outputDir, legacyAsset)),
281 fs.readFileSync(path.join(outputDir, primaryAsset)),
282 `${legacyAsset} must be a byte-identical compatibility copy`,
283 );
284 }
285
286 const canonicalChecksums = parseChecksumManifest(
287 fs.readFileSync(path.join(outputDir, CHECKSUM_MANIFEST), "utf8"),
288 );
289 assert.doesNotThrow(() =>
290 assertChecksumManifestIncludes(
291 canonicalChecksums,
292 checksummedReleaseAssetNames(),
293 "Canonical checksum manifest",
294 ),
295 );
296
297 const bundleChecksums = parseChecksumManifest(
298 fs.readFileSync(path.join(outputDir, BUNDLE_CHECKSUM_MANIFEST), "utf8"),
299 );
300 assert.doesNotThrow(() =>
301 assertChecksumManifestIncludes(
302 bundleChecksums,
303 BUNDLE_ASSET_NAMES,
304 "Bundle checksum manifest",
305 ),
306 );
307 } finally {
308 fs.rmSync(fixtureRoot, { recursive: true, force: true });
309 }
310 });
311
311 lines JAVASCRIPT