357 lines
10 KiB
JavaScript
357 lines
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import {
|
|
chmod,
|
|
copyFile,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import test from "node:test";
|
|
|
|
const sourceRoot = path.resolve(import.meta.dirname, "../..");
|
|
|
|
async function loadRuntimeInventory() {
|
|
const inventoryPath = path.join(
|
|
sourceRoot,
|
|
"scripts/release/npm-packages.sh",
|
|
);
|
|
const source = await readFile(inventoryPath, "utf8");
|
|
const platformBlock = source.match(/RUNTIME_PLATFORMS=\(([\s\S]*?)\n\)/);
|
|
assert.ok(platformBlock, "runtime platform inventory is missing");
|
|
const platforms = [...platformBlock[1].matchAll(/"([^"]+)"/g)].map(
|
|
(match) => match[1],
|
|
);
|
|
const packageByPlatform = new Map(
|
|
[...source.matchAll(/^\s*\["([^"]+)"\]="([^"]+)"$/gm)].map((match) => [
|
|
match[1],
|
|
match[2],
|
|
]),
|
|
);
|
|
return platforms.map((platform) => {
|
|
const packageName = packageByPlatform.get(platform);
|
|
assert.ok(packageName, `runtime package is missing for ${platform}`);
|
|
return { platform, packageName };
|
|
});
|
|
}
|
|
|
|
const runtimeInventory = await loadRuntimeInventory();
|
|
const runtimePackages = runtimeInventory.map(({ packageName }) => packageName);
|
|
const assets = runtimeInventory.map(
|
|
({ platform }) => `kandev-${platform}.tar.gz`,
|
|
);
|
|
const releases = [
|
|
{ name: "Stable", version: "1.2.3", distTag: "latest" },
|
|
{
|
|
name: "Nightly",
|
|
version: "1.2.4-nightly.shaabcdef123456",
|
|
distTag: "nightly",
|
|
},
|
|
];
|
|
|
|
async function createFixture() {
|
|
const root = await mkdtemp(path.join(tmpdir(), "kandev-publish-npm-"));
|
|
const releaseDir = path.join(root, "scripts/release");
|
|
const cliDir = path.join(root, "apps/cli");
|
|
const assetsDir = path.join(root, "assets");
|
|
const binDir = path.join(root, "bin");
|
|
const publishLog = path.join(root, "publish.log");
|
|
const publishedMetadata = path.join(root, "published-package.json");
|
|
await Promise.all([
|
|
mkdir(releaseDir, { recursive: true }),
|
|
mkdir(cliDir, { recursive: true }),
|
|
mkdir(assetsDir, { recursive: true }),
|
|
mkdir(binDir, { recursive: true }),
|
|
]);
|
|
await Promise.all(
|
|
["publish-npm.sh", "npm-packages.sh", "npm-view-version.sh"].map((name) =>
|
|
copyFile(
|
|
path.join(sourceRoot, "scripts/release", name),
|
|
path.join(releaseDir, name),
|
|
),
|
|
),
|
|
);
|
|
await Promise.all(
|
|
assets.map((name) => writeFile(path.join(assetsDir, name), "fixture")),
|
|
);
|
|
await Promise.all([
|
|
writeFile(publishLog, ""),
|
|
writeFile(publishedMetadata, ""),
|
|
]);
|
|
|
|
const originalPackageJSON = `${JSON.stringify(
|
|
{
|
|
name: "kandev",
|
|
version: "0.0.0-bootstrap",
|
|
optionalDependencies: Object.fromEntries(
|
|
runtimePackages.map((name) => [name, "0.0.0-bootstrap"]),
|
|
),
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`;
|
|
await writeFile(path.join(cliDir, "package.json"), originalPackageJSON);
|
|
|
|
const runtimePackageLines = runtimePackages
|
|
.map((name) => `${name} ${name.slice("@kdlbs/".length)}`)
|
|
.join("\n");
|
|
await writeExecutable(
|
|
path.join(releaseDir, "package-npm-runtime.sh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
version="$1"
|
|
out="$3"
|
|
while read -r package name; do
|
|
dir="$out/@kdlbs/$name"
|
|
mkdir -p "$dir"
|
|
printf '{"name":"%s","version":"%s"}\n' "$package" "$version" > "$dir/package.json"
|
|
done <<'PACKAGES'
|
|
${runtimePackageLines}
|
|
PACKAGES
|
|
`,
|
|
);
|
|
await writeExecutable(
|
|
path.join(binDir, "npm"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
case "$1" in
|
|
view)
|
|
spec="$2"
|
|
if [[ "$MOCK_NPM_MODE" == "registry-failure" ]]; then
|
|
printf '%s\n' 'npm error code EAI_AGAIN' >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$MOCK_NPM_MODE" == "fresh" || "$MOCK_NPM_MODE" == "runtime-publish-failure" ]]; then
|
|
printf '%s\n' 'npm error code E404' >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$MOCK_NPM_MODE" == main-conflict* && "$spec" == "kandev@$MOCK_NPM_VERSION" ]]; then
|
|
printf '%s\n' 'npm error code E404' >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$MOCK_NPM_MODE" == "main-conflict" && "$spec" == "kandev@nightly" ]]; then
|
|
printf '%s\n' '1.2.3-nightly.sha000000000000'
|
|
exit 0
|
|
fi
|
|
printf '%s\n' "$MOCK_NPM_VERSION"
|
|
;;
|
|
publish)
|
|
package="$(node -p "require('./package.json').name")"
|
|
printf '%s\n' "$package" >> "$MOCK_NPM_PUBLISH_LOG"
|
|
if [[ "$package" == "kandev" ]]; then
|
|
cp package.json "$MOCK_NPM_PUBLISHED_METADATA"
|
|
fi
|
|
if [[ "$MOCK_NPM_MODE" == "runtime-publish-failure" && "$package" == "$MOCK_RUNTIME_FAILURE_PACKAGE" ]]; then
|
|
printf '%s\n' 'npm error code E500' >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$MOCK_NPM_MODE" == main-conflict* && "$package" == "kandev" ]]; then
|
|
printf '%s\n' 'npm error code EPUBLISHCONFLICT' >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
printf 'unexpected npm command: %s\n' "$*" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
`,
|
|
);
|
|
await writeExecutable(
|
|
path.join(binDir, "sleep"),
|
|
"#!/usr/bin/env bash\nexit 0\n",
|
|
);
|
|
await writeExecutable(
|
|
path.join(binDir, "gh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
pattern=""
|
|
directory=""
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case "$1" in
|
|
--pattern) pattern="$2"; shift 2 ;;
|
|
--dir) directory="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
mkdir -p "$directory"
|
|
: > "$directory/$pattern"
|
|
`,
|
|
);
|
|
|
|
return {
|
|
root,
|
|
releaseDir,
|
|
cliDir,
|
|
assetsDir,
|
|
binDir,
|
|
publishLog,
|
|
publishedMetadata,
|
|
originalPackageJSON,
|
|
};
|
|
}
|
|
|
|
async function writeExecutable(file, content) {
|
|
await writeFile(file, content);
|
|
await chmod(file, 0o755);
|
|
}
|
|
|
|
async function runPublish(fixture, { version, distTag, mode }) {
|
|
const args = [
|
|
path.join(fixture.releaseDir, "publish-npm.sh"),
|
|
"--version",
|
|
version,
|
|
"--dist-tag",
|
|
distTag,
|
|
];
|
|
if (distTag === "latest") {
|
|
args.push("--release-tag", `v${version}`);
|
|
} else {
|
|
args.push("--assets-dir", fixture.assetsDir);
|
|
}
|
|
const result = spawnSync("bash", args, {
|
|
encoding: "utf8",
|
|
cwd: fixture.root,
|
|
env: {
|
|
...process.env,
|
|
MOCK_NPM_MODE: mode,
|
|
MOCK_NPM_PUBLISH_LOG: fixture.publishLog,
|
|
MOCK_NPM_PUBLISHED_METADATA: fixture.publishedMetadata,
|
|
MOCK_RUNTIME_FAILURE_PACKAGE: runtimePackages[1],
|
|
MOCK_NPM_VERSION: version,
|
|
PATH: `${fixture.binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
},
|
|
});
|
|
return {
|
|
...result,
|
|
packageJSON: await readFile(
|
|
path.join(fixture.cliDir, "package.json"),
|
|
"utf8",
|
|
),
|
|
publishedMetadata: await readFile(fixture.publishedMetadata, "utf8"),
|
|
published: (await readFile(fixture.publishLog, "utf8"))
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean),
|
|
};
|
|
}
|
|
|
|
for (const release of releases) {
|
|
test(`${release.name} publication is idempotent when every exact package already exists`, async () => {
|
|
const fixture = await createFixture();
|
|
try {
|
|
const result = await runPublish(fixture, {
|
|
...release,
|
|
mode: "all-published",
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(result.published, []);
|
|
assert.equal(result.packageJSON, fixture.originalPackageJSON);
|
|
} finally {
|
|
await rm(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
for (const release of releases) {
|
|
test(`a fresh ${release.name} publishes every package with pinned launcher metadata`, async () => {
|
|
const fixture = await createFixture();
|
|
try {
|
|
const result = await runPublish(fixture, { ...release, mode: "fresh" });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(result.published, [...runtimePackages, "kandev"]);
|
|
const metadata = JSON.parse(result.publishedMetadata);
|
|
assert.equal(metadata.version, release.version);
|
|
assert.deepEqual(
|
|
metadata.optionalDependencies,
|
|
Object.fromEntries(
|
|
runtimePackages.map((name) => [name, release.version]),
|
|
),
|
|
);
|
|
assert.equal(result.packageJSON, fixture.originalPackageJSON);
|
|
} finally {
|
|
await rm(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
test("a conflicting Nightly launcher publish fails closed and restores package metadata", async () => {
|
|
const fixture = await createFixture();
|
|
try {
|
|
const result = await runPublish(fixture, {
|
|
version: "1.2.4-nightly.shaabcdef123456",
|
|
distTag: "nightly",
|
|
mode: "main-conflict",
|
|
});
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(
|
|
result.stderr,
|
|
/kandev@nightly resolves to '1\.2\.3-nightly\.sha000000000000'/,
|
|
);
|
|
assert.deepEqual(result.published, ["kandev"]);
|
|
assert.equal(result.packageJSON, fixture.originalPackageJSON);
|
|
} finally {
|
|
await rm(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("a matching Nightly conflict converges as an idempotent success", async () => {
|
|
const fixture = await createFixture();
|
|
try {
|
|
const result = await runPublish(fixture, {
|
|
version: "1.2.4-nightly.shaabcdef123456",
|
|
distTag: "nightly",
|
|
mode: "main-conflict-matching",
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(result.published, ["kandev"]);
|
|
assert.equal(result.packageJSON, fixture.originalPackageJSON);
|
|
} finally {
|
|
await rm(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("a transient registry failure aborts before publishing and preserves metadata", async () => {
|
|
const fixture = await createFixture();
|
|
try {
|
|
const result = await runPublish(fixture, {
|
|
version: "1.2.4-nightly.shaabcdef123456",
|
|
distTag: "nightly",
|
|
mode: "registry-failure",
|
|
});
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(
|
|
result.stderr,
|
|
/could not verify whether @kdlbs\/runtime-linux-x64/,
|
|
);
|
|
assert.deepEqual(result.published, []);
|
|
assert.equal(result.packageJSON, fixture.originalPackageJSON);
|
|
} finally {
|
|
await rm(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("a runtime publish failure withholds the launcher", async () => {
|
|
const fixture = await createFixture();
|
|
try {
|
|
const result = await runPublish(fixture, {
|
|
version: "1.2.4-nightly.shaabcdef123456",
|
|
distTag: "nightly",
|
|
mode: "runtime-publish-failure",
|
|
});
|
|
assert.notEqual(result.status, 0);
|
|
assert.deepEqual(result.published, runtimePackages);
|
|
assert.ok(
|
|
result.stderr.includes(`Failed to publish ${runtimePackages[1]}`),
|
|
);
|
|
assert.match(result.stderr, /Refusing to publish main kandev/);
|
|
assert.equal(result.packageJSON, fixture.originalPackageJSON);
|
|
} finally {
|
|
await rm(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|