Files
kandev/scripts/release/nightly-release.test.mjs
2026-08-04 00:45:50 +01:00

547 lines
17 KiB
JavaScript

import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import {
chmod,
copyFile,
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
const sourceRoot = path.resolve(import.meta.dirname, "../..");
const packages = [
"kandev",
"@kdlbs/runtime-linux-x64",
"@kdlbs/runtime-linux-arm64",
"@kdlbs/runtime-darwin-x64",
"@kdlbs/runtime-darwin-arm64",
"@kdlbs/runtime-win32-x64",
];
async function writeExecutable(file, content) {
await writeFile(file, content);
await chmod(file, 0o755);
}
function git(root, ...args) {
return execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
}
async function createFixture() {
const root = await mkdtemp(path.join(tmpdir(), "kandev-nightly-release-"));
const releaseDir = path.join(root, "scripts/release");
const binDir = path.join(root, "bin");
const assetsDir = path.join(root, "assets");
const stateFile = path.join(root, "npm-state.tsv");
const outputFile = path.join(root, "github-output");
const publishLog = path.join(root, "publish.log");
await Promise.all([
mkdir(releaseDir, { recursive: true }),
mkdir(binDir, { recursive: true }),
mkdir(assetsDir, { recursive: true }),
writeFile(stateFile, ""),
writeFile(outputFile, ""),
writeFile(publishLog, ""),
]);
await Promise.all(
[
"nightly-release.sh",
"nightly-version.mjs",
"npm-packages.sh",
"npm-view-version.sh",
].map((name) =>
copyFile(
path.join(sourceRoot, "scripts/release", name),
path.join(releaseDir, name),
),
),
);
await writeExecutable(
path.join(releaseDir, "publish-npm.sh"),
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$@" >> "$MOCK_PUBLISH_LOG"
`,
);
await writeExecutable(
path.join(binDir, "npm"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" != "view" || "$3" != "version" ]]; then
printf 'unexpected npm command: %s\n' "$*" >&2
exit 2
fi
spec="$2"
while IFS=$'\t' read -r key value; do
if [[ "$key" != "$spec" ]]; then
continue
fi
if [[ "$value" == "!ERROR!" ]]; then
printf '%s\n' 'npm error code EAI_AGAIN' >&2
exit 1
fi
printf '%s\n' "$value"
exit 0
done < "$MOCK_NPM_STATE"
printf '%s\n' 'npm error code E404' >&2
exit 1
`,
);
await writeExecutable(
path.join(binDir, "sleep"),
"#!/usr/bin/env bash\nexit 0\n",
);
git(root, "init", "-q");
git(root, "config", "user.name", "Nightly Test");
git(root, "config", "user.email", "nightly@example.test");
await writeFile(path.join(root, "history.txt"), "stable\n");
git(root, "add", "history.txt");
git(root, "commit", "-q", "-m", "stable");
const stableSha = git(root, "rev-parse", "HEAD");
git(root, "tag", "v1.2.3");
await writeFile(path.join(root, "history.txt"), "stable\nmain\n");
git(root, "add", "history.txt");
git(root, "commit", "-q", "-m", "main");
const mainSha = git(root, "rev-parse", "HEAD");
await writeFile(path.join(root, "history.txt"), "stable\nmain\nnewer\n");
git(root, "add", "history.txt");
git(root, "commit", "-q", "-m", "newer");
const newerSha = git(root, "rev-parse", "HEAD");
git(root, "checkout", "-q", "--detach", stableSha);
await writeFile(path.join(root, "history.txt"), "stable\ndivergent\n");
git(root, "add", "history.txt");
git(root, "commit", "-q", "-m", "divergent");
const divergentSha = git(root, "rev-parse", "HEAD");
git(root, "checkout", "-q", "--detach", mainSha);
return {
root,
releaseDir,
binDir,
assetsDir,
stateFile,
outputFile,
publishLog,
stableSha,
mainSha,
newerSha,
divergentSha,
version: `1.2.4-nightly.sha${mainSha.slice(0, 12)}`,
};
}
async function setRegistry(fixture, entries) {
const lines = [...entries].map(([spec, value]) => `${spec}\t${value}`);
await writeFile(
fixture.stateFile,
lines.length === 0 ? "" : `${lines.join("\n")}\n`,
);
}
function runScript(fixture, ...args) {
return spawnSync(
"bash",
[path.join(fixture.releaseDir, "nightly-release.sh"), ...args],
{
cwd: fixture.root,
encoding: "utf8",
env: {
...process.env,
MOCK_NPM_STATE: fixture.stateFile,
MOCK_PUBLISH_LOG: fixture.publishLog,
PATH: `${fixture.binDir}${path.delimiter}${process.env.PATH ?? ""}`,
},
},
);
}
async function runPrepare(fixture, scheduledSha = fixture.mainSha) {
await writeFile(fixture.outputFile, "");
const result = runScript(
fixture,
"prepare",
"--scheduled-sha",
scheduledSha,
"--output",
fixture.outputFile,
);
const output = Object.fromEntries(
(await readFile(fixture.outputFile, "utf8"))
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
return { ...result, output };
}
function registryFor(version, nightly = "") {
const entries = new Map([["kandev@latest", "1.2.3"]]);
if (nightly) entries.set("kandev@nightly", nightly);
for (const packageName of packages) {
entries.set(`${packageName}@${version}`, version);
entries.set(`${packageName}@nightly`, version);
}
return entries;
}
function tagsSnapshot(version) {
return packages.map((packageName) => `${packageName}=${version};`).join("");
}
test("prepare publishes a changed main commit and snapshots empty tags", async () => {
const fixture = await createFixture();
try {
await setRegistry(fixture, new Map([["kandev@latest", "1.2.3"]]));
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "true");
assert.equal(result.output.stable_version, "1.2.3");
assert.equal(result.output.version, fixture.version);
assert.equal(result.output.tag, `v${fixture.version}`);
assert.equal(result.output.ref, fixture.mainSha);
assert.equal(result.output.nightly_version_at_start, "");
assert.equal(result.output.nightly_tags_at_start, tagsSnapshot(""));
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare normalizes a legacy two-part stable tag", async () => {
const fixture = await createFixture();
try {
git(fixture.root, "tag", "-d", "v1.2.3");
git(fixture.root, "tag", "v1.2", fixture.stableSha);
await setRegistry(fixture, new Map([["kandev@latest", "1.2.0"]]));
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "true");
assert.equal(result.output.stable_version, "1.2.0");
assert.equal(
result.output.version,
`1.2.1-nightly.sha${fixture.mainSha.slice(0, 12)}`,
);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare skips when main has no commit after stable", async () => {
const fixture = await createFixture();
try {
git(fixture.root, "checkout", "-q", "--detach", fixture.stableSha);
await setRegistry(fixture, new Map([["kandev@latest", "1.2.3"]]));
const result = await runPrepare(fixture, fixture.stableSha);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "false");
assert.match(result.stdout, /No commits on main since stable/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare skips while a stable Git tag is ahead of npm latest", async () => {
const fixture = await createFixture();
try {
git(fixture.root, "tag", "v1.3.0", fixture.mainSha);
await setRegistry(fixture, new Map([["kandev@latest", "1.2.3"]]));
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "false");
assert.match(result.stdout, /stable Git tag v1\.3\.0.*npm @latest 1\.2\.3/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare skips a scheduled commit superseded by the current stable", async () => {
const fixture = await createFixture();
try {
git(fixture.root, "tag", "v1.3.0", fixture.newerSha);
await setRegistry(fixture, new Map([["kandev@latest", "1.3.0"]]));
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "false");
assert.match(result.stdout, /superseded by stable v1\.3\.0/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare skips a complete publication and repairs a partial one", async () => {
const fixture = await createFixture();
try {
const complete = registryFor(fixture.version, fixture.version);
await setRegistry(fixture, complete);
const completeResult = await runPrepare(fixture);
assert.equal(completeResult.status, 0, completeResult.stderr);
assert.equal(completeResult.output.should_publish, "false");
complete.delete(`${packages.at(-1)}@${fixture.version}`);
await setRegistry(fixture, complete);
const partialResult = await runPrepare(fixture);
assert.equal(partialResult.status, 0, partialResult.stderr);
assert.equal(partialResult.output.should_publish, "true");
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare skips a scheduled commit superseded by a newer main Nightly", async () => {
const fixture = await createFixture();
try {
const newerVersion = `1.2.4-nightly.sha${fixture.newerSha.slice(0, 12)}`;
await setRegistry(
fixture,
new Map([
["kandev@latest", "1.2.3"],
["kandev@nightly", newerVersion],
]),
);
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "false");
assert.match(result.stdout, /newer main commit is already published/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare rejects a Nightly version that cannot identify a commit", async () => {
const fixture = await createFixture();
try {
await setRegistry(
fixture,
new Map([
["kandev@latest", "1.2.3"],
["kandev@nightly", "1.2.4-nightly.bad"],
]),
);
const result = await runPrepare(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /unsupported version/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare rejects registry commits outside scheduled main history", async () => {
const cases = [
{
name: "stable baseline",
configure(fixture, registry) {
git(fixture.root, "tag", "-f", "v1.2.3", fixture.divergentSha);
return registry;
},
error: /Latest stable .* is not an ancestor/,
},
{
name: "top-level Nightly",
configure(fixture, registry) {
registry.set(
"kandev@nightly",
`1.2.4-nightly.sha${fixture.divergentSha.slice(0, 12)}`,
);
return registry;
},
error: /Published Nightly commit .* is not an ancestor/,
},
{
name: "partial runtime tag",
configure(fixture, registry) {
const packageName = packages.at(-1);
registry.set("kandev@nightly", fixture.version);
registry.delete(`${packageName}@${fixture.version}`);
registry.set(
`${packageName}@nightly`,
`1.2.4-nightly.sha${fixture.divergentSha.slice(0, 12)}`,
);
return registry;
},
error: /@nightly commit .* is not an ancestor/,
},
];
for (const scenario of cases) {
const fixture = await createFixture();
try {
const registry = scenario.configure(
fixture,
registryFor(fixture.version, fixture.version),
);
await setRegistry(fixture, registry);
const result = await runPrepare(fixture);
assert.notEqual(result.status, 0, scenario.name);
assert.match(result.stderr, scenario.error, scenario.name);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
}
});
test("prepare rejects an exact package whose Nightly tag points elsewhere", async () => {
const fixture = await createFixture();
try {
const registry = registryFor(fixture.version, fixture.version);
registry.set(
`${packages.at(-1)}@nightly`,
`1.2.4-nightly.sha${fixture.stableSha.slice(0, 12)}`,
);
await setRegistry(fixture, registry);
const result = await runPrepare(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /exists but @nightly resolves/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare safely skips registry failures", async () => {
const fixture = await createFixture();
try {
await setRegistry(fixture, new Map([["kandev@latest", "!ERROR!"]]));
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "false");
assert.match(result.stdout, /Could not resolve npm @latest/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("prepare safely skips a package registry failure", async () => {
const fixture = await createFixture();
try {
const registry = new Map([
["kandev@latest", "1.2.3"],
[`${packages[1]}@${fixture.version}`, "!ERROR!"],
]);
await setRegistry(fixture, registry);
const result = await runPrepare(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.output.should_publish, "false");
assert.match(result.stdout, /Could not verify/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("publish revalidates the registry snapshot before delegating", async () => {
const fixture = await createFixture();
try {
await setRegistry(fixture, registryFor(fixture.version, fixture.version));
const result = runScript(
fixture,
"publish",
"--stable-at-start",
"1.2.3",
"--nightly-at-start",
fixture.version,
"--tags-at-start",
tagsSnapshot(fixture.version),
"--version",
fixture.version,
"--assets-dir",
fixture.assetsDir,
);
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(
(await readFile(fixture.publishLog, "utf8")).trim().split("\n"),
[
"--version",
fixture.version,
"--dist-tag",
"nightly",
"--assets-dir",
fixture.assetsDir,
],
);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("publish skips when any locked registry value moves", async () => {
const cases = [
["stable baseline", "kandev@latest", "1.2.4"],
["top-level Nightly", "kandev@nightly", "1.2.4-nightly.sha000000000000"],
["runtime tag", `${packages[1]}@nightly`, "1.2.4-nightly.sha000000000000"],
];
for (const [name, spec, movedValue] of cases) {
const fixture = await createFixture();
try {
const registry = registryFor(fixture.version, fixture.version);
registry.set(spec, movedValue);
await setRegistry(fixture, registry);
const result = runScript(
fixture,
"publish",
"--stable-at-start",
"1.2.3",
"--nightly-at-start",
fixture.version,
"--tags-at-start",
tagsSnapshot(fixture.version),
"--version",
fixture.version,
"--assets-dir",
fixture.assetsDir,
);
assert.equal(result.status, 0, `${name}: ${result.stderr}`);
assert.equal(await readFile(fixture.publishLog, "utf8"), "", name);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
}
});
test("publish skips while a stable Git tag is ahead of npm latest", async () => {
const fixture = await createFixture();
try {
git(fixture.root, "tag", "v1.3.0", fixture.mainSha);
await setRegistry(fixture, registryFor(fixture.version, fixture.version));
const result = runScript(
fixture,
"publish",
"--stable-at-start",
"1.2.3",
"--nightly-at-start",
fixture.version,
"--tags-at-start",
tagsSnapshot(fixture.version),
"--version",
fixture.version,
"--assets-dir",
fixture.assetsDir,
);
assert.equal(result.status, 0, result.stderr);
assert.equal(await readFile(fixture.publishLog, "utf8"), "");
assert.match(result.stdout, /stable Git tag v1\.3\.0.*npm @latest 1\.2\.3/);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});