返回 CodeWhale
assemble-release-assets.test.js
根目录 / scripts / release / assemble-release-assets.test.js
1 #!/usr/bin/env node
2
3 const assert = require("node:assert/strict");
4 const { execFileSync } = require("node:child_process");
5 const crypto = require("node:crypto");
6 const fs = require("node:fs");
7 const os = require("node:os");
8 const path = require("node:path");
9 const test = require("node:test");
10
11 const {
12 allReleaseAssetNames,
13 BUNDLE_ASSET_NAMES,
14 BUNDLE_CHECKSUM_MANIFEST,
15 CHECKSUM_MANIFEST,
16 checksummedReleaseAssetNames,
17 } = require("../../npm/codewhale/scripts/artifacts");
18 const {
19 assemble,
20 parseChecksumManifest,
21 verifyAssetDirectory,
22 windowsLauncherContents,
23 } = require("./assemble-release-assets");
24
25 const repoRoot = path.resolve(__dirname, "..", "..");
26
27 function sha256(filePath) {
28 return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
29 }
30
31 function makeIntermediateArtifacts(root) {
32 const generated = new Set(["codewhale.bat", CHECKSUM_MANIFEST]);
33 const copied = allReleaseAssetNames().filter((name) => !generated.has(name));
34 for (const name of copied) {
35 if (name === BUNDLE_CHECKSUM_MANIFEST) {
36 continue;
37 }
38 const artifactDirectory = BUNDLE_ASSET_NAMES.includes(name)
39 ? path.join(root, "codewhale-bundles")
40 : path.join(root, name);
41 fs.mkdirSync(artifactDirectory, { recursive: true });
42 fs.writeFileSync(path.join(artifactDirectory, name), `fixture:${name}\n`);
43 }
44
45 const bundleManifestDirectory = path.join(root, "codewhale-bundles");
46 fs.mkdirSync(bundleManifestDirectory, { recursive: true });
47 const rows = BUNDLE_ASSET_NAMES.map((name) => {
48 const matches = fs
49 .readdirSync(root, { recursive: true })
50 .map((entry) => path.join(root, entry))
51 .filter((entry) => path.basename(entry) === name && fs.statSync(entry).isFile());
52 assert.equal(matches.length, 1, `fixture should contain one ${name}`);
53 return `${sha256(matches[0])} ${name}`;
54 }).sort();
55 fs.writeFileSync(
56 path.join(bundleManifestDirectory, BUNDLE_CHECKSUM_MANIFEST),
57 `${rows.join("\n")}\n`,
58 );
59 }
60
61 test("authoritative release inventory contains seven targets and 34 assets", () => {
62 const assets = allReleaseAssetNames();
63 assert.equal(assets.length, 34);
64 assert.equal(checksummedReleaseAssetNames().length, 33);
65 for (const required of [
66 "codewhale-android-arm64",
67 "codew-android-arm64",
68 "codewhale-windows-arm64.exe",
69 "codew-windows-arm64.exe",
70 "codewhale-windows-arm64.zip",
71 "CodeWhaleSetup.exe",
72 ]) {
73 assert.ok(assets.includes(required), `missing ${required}`);
74 }
75 });
76
77 test("assembly creates and verifies the exact release asset directory", async () => {
78 const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-asset-assembly-"));
79 const input = path.join(tempRoot, "input");
80 const output = path.join(tempRoot, "output");
81 try {
82 fs.mkdirSync(input, { recursive: true });
83 makeIntermediateArtifacts(input);
84 await assemble(input, output);
85 await assert.doesNotReject(() => verifyAssetDirectory(output));
86
87 assert.deepEqual(
88 fs.readdirSync(output).sort(),
89 [...allReleaseAssetNames()].sort(),
90 );
91 assert.equal(
92 fs.readFileSync(path.join(output, "codewhale.bat"), "utf8"),
93 windowsLauncherContents(),
94 );
95
96 const checksums = parseChecksumManifest(
97 fs.readFileSync(path.join(output, CHECKSUM_MANIFEST), "utf8"),
98 CHECKSUM_MANIFEST,
99 );
100 assert.deepEqual([...checksums.keys()].sort(), [...checksummedReleaseAssetNames()].sort());
101 } finally {
102 fs.rmSync(tempRoot, { force: true, recursive: true });
103 }
104 });
105
106 test("bundle helper creates the exact nine archives and checksum manifest", () => {
107 const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-bundle-assembly-"));
108 const input = path.join(tempRoot, "input");
109 const output = path.join(tempRoot, "output");
110 const repeatedOutput = path.join(tempRoot, "output-repeated");
111 try {
112 fs.mkdirSync(input, { recursive: true });
113 for (const name of allReleaseAssetNames().filter((asset) =>
114 /^(codewhale|codew|codewhale-tui)-(linux|android|macos|windows)-/.test(asset) &&
115 !asset.endsWith(".tar.gz") &&
116 !asset.endsWith(".zip"),
117 )) {
118 const artifactDirectory = path.join(input, name);
119 fs.mkdirSync(artifactDirectory, { recursive: true });
120 // GitHub's artifact transport normalizes regular files to 0644. The
121 // bundler must restore executable modes for non-Windows archives.
122 fs.writeFileSync(path.join(artifactDirectory, name), `fixture:${name}\n`, { mode: 0o644 });
123 }
124
125 execFileSync(
126 "bash",
127 [path.join(repoRoot, "scripts/release/create-release-bundles.sh"), input, output],
128 { cwd: repoRoot, stdio: "pipe" },
129 );
130 execFileSync(
131 "bash",
132 [path.join(repoRoot, "scripts/release/create-release-bundles.sh"), input, repeatedOutput],
133 { cwd: repoRoot, stdio: "pipe" },
134 );
135 assert.deepEqual(
136 fs.readdirSync(output).sort(),
137 [...BUNDLE_ASSET_NAMES, BUNDLE_CHECKSUM_MANIFEST].sort(),
138 );
139 const checksums = parseChecksumManifest(
140 fs.readFileSync(path.join(output, BUNDLE_CHECKSUM_MANIFEST), "utf8"),
141 BUNDLE_CHECKSUM_MANIFEST,
142 );
143 for (const name of BUNDLE_ASSET_NAMES) {
144 assert.equal(checksums.get(name), sha256(path.join(output, name)));
145 assert.deepEqual(
146 fs.readFileSync(path.join(output, name)),
147 fs.readFileSync(path.join(repeatedOutput, name)),
148 `${name} should be byte-reproducible for identical inputs`,
149 );
150 }
151 assert.deepEqual(
152 fs.readFileSync(path.join(output, BUNDLE_CHECKSUM_MANIFEST)),
153 fs.readFileSync(path.join(repeatedOutput, BUNDLE_CHECKSUM_MANIFEST)),
154 "bundle checksum manifest should be reproducible",
155 );
156
157 const linuxEntries = execFileSync(
158 "tar",
159 ["-tzf", path.join(output, "codewhale-linux-x64.tar.gz")],
160 { encoding: "utf8" },
161 );
162 for (const entry of ["codewhale", "codew", "codewhale-tui", "install.sh"]) {
163 assert.match(linuxEntries, new RegExp(`codewhale-linux-x64/${entry}\\n`));
164 }
165 const extracted = path.join(tempRoot, "extracted");
166 fs.mkdirSync(extracted);
167 execFileSync(
168 "tar",
169 ["-xzf", path.join(output, "codewhale-linux-x64.tar.gz"), "-C", extracted],
170 { stdio: "pipe" },
171 );
172 for (const entry of ["codewhale", "codew", "codewhale-tui", "install.sh"]) {
173 const mode = fs.statSync(path.join(extracted, "codewhale-linux-x64", entry)).mode & 0o777;
174 assert.equal(mode, 0o755, `${entry} should remain executable after artifact transport`);
175 }
176 const portableEntries = execFileSync(
177 "unzip",
178 ["-Z1", path.join(output, "codewhale-windows-arm64-portable.zip")],
179 { encoding: "utf8" },
180 );
181 assert.match(portableEntries, /codewhale-windows-arm64-portable\/codew\.exe\n/);
182 assert.doesNotMatch(portableEntries, /install\.bat/);
183 } finally {
184 fs.rmSync(tempRoot, { force: true, recursive: true });
185 }
186 });
187
188 test("verification rejects modified and unexpected assets", async () => {
189 const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-asset-tamper-"));
190 const input = path.join(tempRoot, "input");
191 const output = path.join(tempRoot, "output");
192 try {
193 fs.mkdirSync(input, { recursive: true });
194 makeIntermediateArtifacts(input);
195 await assemble(input, output);
196 fs.appendFileSync(path.join(output, "codewhale-linux-x64"), "tampered\n");
197 await assert.rejects(
198 () => verifyAssetDirectory(output),
199 /checksum mismatch for codewhale-linux-x64/,
200 );
201
202 fs.writeFileSync(path.join(output, "unexpected.txt"), "unexpected\n");
203 await assert.rejects(
204 () => verifyAssetDirectory(output),
205 /unexpected: unexpected\.txt/,
206 );
207 } finally {
208 fs.rmSync(tempRoot, { force: true, recursive: true });
209 }
210 });
211
211 lines JAVASCRIPT