Files
openclaw/scripts/npm-placeholder-publication.mjs
T
Peter Steinberger c70aee247e refactor(scripts): migrate JavaScript tools to TypeScript (#121005)
* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
2026-08-09 07:21:35 -07:00

826 lines
28 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
lstatSync,
mkdtempSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { gzipSync } from "node:zlib";
import { readPublicationArtifactArchive, sha256Digest } from "./lib/actions-artifact-archive.mjs";
import { fetchNpmRegistryPackumentWithRetry } from "./lib/npm-publish-plan.mjs";
const MANIFEST_FILENAME = "npm-placeholder-manifest.json";
const MANIFEST_SCHEMA = "openclaw.npm-placeholder-publication/v1";
const PACKAGE_VERSION = "0.0.0";
const PUBLISH_TAG = "placeholder";
const WORKFLOW_PATH = ".github/workflows/npm-placeholder-bootstrap.yml";
const PACKAGE_NAME_RE = /^@openclaw\/[a-z0-9][a-z0-9._-]*$/u;
const SHA_RE = /^[0-9a-f]{40}$/u;
const SHA256_RE = /^[0-9a-f]{64}$/u;
const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
const MAX_FILE_BYTES = 2 * 1024 * 1024;
/** @typedef {typeof fetch} PlaceholderFetch */
/**
* @typedef {object} CreatePlaceholderPublicationParams
* @property {PlaceholderFetch} [fetchImpl]
* @property {string} outputDir
* @property {string} packages
* @property {string} repoRoot
* @property {string} targetSha
* @property {string} workflowSha
*/
/**
* @typedef {object} PublishPlaceholdersParams
* @property {string} artifactDir
* @property {PlaceholderFetch} [fetchImpl]
* @property {string} npmToken
* @property {(args: string[], options: { cwd: string; env: NodeJS.ProcessEnv }) => void} [npmRunner]
* @property {number} [registryAttempts]
* @property {(delayMs: number) => Promise<void>} [sleep]
* @property {string} targetSha
* @property {string} [tempRoot]
* @property {string} workflowSha
*/
function compareCodeUnits(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function assertTrimmedString(value, label) {
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
throw new Error(`${label} must be a non-empty trimmed string.`);
}
return value;
}
function assertCommitSha(value, label) {
const sha = assertTrimmedString(value, label);
if (!SHA_RE.test(sha)) {
throw new Error(`${label} must be a full lowercase commit SHA.`);
}
return sha;
}
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function npmIntegrity(bytes) {
return `sha512-${createHash("sha512").update(bytes).digest("base64")}`;
}
function npmShasum(bytes) {
return createHash("sha1").update(bytes).digest("hex");
}
function canonicalJson(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}
function placeholderPackageJson(packageName) {
return {
name: packageName,
version: PACKAGE_VERSION,
description: "Reserved package name for an official OpenClaw plugin.",
license: "MIT",
repository: {
type: "git",
url: "git+https://github.com/openclaw/openclaw.git",
},
publishConfig: {
access: "public",
tag: PUBLISH_TAG,
},
};
}
function placeholderReadme(packageName) {
return `# ${packageName}\n\nReserved placeholder for the official OpenClaw plugin package. Use a published release version instead.\n`;
}
function writeTarString(header, offset, length, value) {
const bytes = Buffer.from(value, "utf8");
if (bytes.length > length) {
throw new Error(`Tar header value exceeds ${length} bytes.`);
}
bytes.copy(header, offset);
}
function writeTarOctal(header, offset, length, value) {
const encoded = value.toString(8).padStart(length - 1, "0");
writeTarString(header, offset, length, `${encoded}\0`);
}
function tarEntry(path, content) {
const header = Buffer.alloc(512);
writeTarString(header, 0, 100, path);
writeTarOctal(header, 100, 8, 0o644);
writeTarOctal(header, 108, 8, 0);
writeTarOctal(header, 116, 8, 0);
writeTarOctal(header, 124, 12, content.length);
writeTarOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
header[156] = "0".charCodeAt(0);
writeTarString(header, 257, 6, "ustar\0");
writeTarString(header, 263, 2, "00");
const checksum = header.reduce((sum, byte) => sum + byte, 0);
writeTarString(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
const padding = Buffer.alloc((512 - (content.length % 512)) % 512);
return Buffer.concat([header, content, padding]);
}
export function createPlaceholderTarball(packageName) {
if (!PACKAGE_NAME_RE.test(packageName)) {
throw new Error(`Invalid OpenClaw package name: ${packageName}`);
}
const packageJson = Buffer.from(canonicalJson(placeholderPackageJson(packageName)), "utf8");
const readme = Buffer.from(placeholderReadme(packageName), "utf8");
const tar = Buffer.concat([
tarEntry("package/package.json", packageJson),
tarEntry("package/README.md", readme),
Buffer.alloc(1024),
]);
return gzipSync(tar, { level: 9, mtime: 0 });
}
export function parseSelectedPackages(input) {
const raw = assertTrimmedString(input, "package selection");
const packages = raw.split(",").map((value) => value.trim());
if (packages.some((value) => value.length === 0)) {
throw new Error("Package selection must not contain empty entries.");
}
for (const packageName of packages) {
if (!PACKAGE_NAME_RE.test(packageName)) {
throw new Error(`Invalid OpenClaw package name: ${packageName}`);
}
}
if (new Set(packages).size !== packages.length) {
throw new Error("Package selection must not contain duplicates.");
}
return packages;
}
function readJsonRegularFile(path, label) {
const info = lstatSync(path);
if (!info.isFile() || info.isSymbolicLink() || info.size === 0 || info.size > MAX_FILE_BYTES) {
throw new Error(`${label} must be a bounded regular file.`);
}
let value;
try {
value = JSON.parse(readFileSync(path, "utf8"));
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${label} is invalid JSON: ${detail}`, { cause: error });
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${label} must contain an object.`);
}
return value;
}
export function resolveSelectedPackageSources(repoRoot, packageNames) {
const extensionsDir = resolve(repoRoot, "extensions");
const officialPackageNames = new Set();
for (const catalogName of ["plugin", "provider", "channel"]) {
const catalogPath = resolve(
repoRoot,
"scripts",
"lib",
`official-external-${catalogName}-catalog.json`,
);
const catalog = readJsonRegularFile(catalogPath, `Official external ${catalogName} catalog`);
if (!Array.isArray(catalog.entries)) {
throw new Error(`Official external ${catalogName} catalog entries must be an array.`);
}
for (const entry of catalog.entries) {
if (
entry?.source === "official" &&
typeof entry.name === "string" &&
entry.openclaw?.install?.npmSpec === entry.name
) {
officialPackageNames.add(entry.name);
}
}
}
const candidates = [];
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.isSymbolicLink()) {
continue;
}
const packageDir = `extensions/${entry.name}`;
const packageJsonPath = resolve(repoRoot, packageDir, "package.json");
let info;
try {
info = lstatSync(packageJsonPath);
} catch (error) {
if (error && typeof error === "object" && error.code === "ENOENT") {
continue;
}
throw error;
}
if (!info.isFile() || info.isSymbolicLink()) {
throw new Error(`${packageDir}/package.json must be a regular file.`);
}
const packageJsonBytes = readFileSync(packageJsonPath);
const packageJson = readJsonRegularFile(packageJsonPath, `${packageDir}/package.json`);
candidates.push({ packageDir, packageJson, packageJsonBytes });
}
return packageNames.map((packageName) => {
const matches = candidates.filter((candidate) => candidate.packageJson.name === packageName);
if (matches.length !== 1) {
throw new Error(`${packageName} must map uniquely to extensions/*/package.json.`);
}
const [match] = matches;
if (
match.packageJson.private === true ||
["private", "restricted"].includes(match.packageJson.publishConfig?.access) ||
match.packageJson.openclaw?.install?.npmSpec !== packageName ||
match.packageJson.openclaw?.build?.bundledDist !== false ||
match.packageJson.openclaw?.release?.publishToNpm !== true
) {
throw new Error(`${packageName} is not a public release-enabled npm plugin.`);
}
if (!officialPackageNames.has(packageName)) {
throw new Error(`${packageName} is not present in an official external package catalog.`);
}
return {
packageDir: match.packageDir,
packageName,
sourcePackageJsonSha256: sha256(match.packageJsonBytes),
};
});
}
function normalizeDistTags(value, packageName) {
if (value === undefined) {
return {};
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${packageName}: npm dist-tags must be an object.`);
}
const entries = [];
for (const [tag, version] of Object.entries(value)) {
if (
typeof tag !== "string" ||
tag.length === 0 ||
typeof version !== "string" ||
version.length === 0
) {
throw new Error(`${packageName}: npm dist-tags contain an invalid entry.`);
}
entries.push([tag, version]);
}
return Object.fromEntries(entries.toSorted(([left], [right]) => compareCodeUnits(left, right)));
}
export function classifyRegistryState(params) {
const { expectedIntegrity, expectedShasum, packageName, registry } = params;
if (registry.status === 404) {
return { action: "publish", newPackage: true, nonPlaceholderTags: {} };
}
if (registry.status !== 200 || !registry.packument || typeof registry.packument !== "object") {
throw new Error(`${packageName}: npm registry returned HTTP ${registry.status}.`);
}
const distTags = normalizeDistTags(registry.packument["dist-tags"], packageName);
if (distTags[PUBLISH_TAG] !== undefined && distTags[PUBLISH_TAG] !== PACKAGE_VERSION) {
throw new Error(
`${packageName}: placeholder dist-tag points to ${distTags[PUBLISH_TAG]}, expected ${PACKAGE_VERSION}.`,
);
}
const nonPlaceholderTags = Object.fromEntries(
Object.entries(distTags).filter(([tag]) => tag !== PUBLISH_TAG),
);
const publishedDist = registry.packument.versions?.[PACKAGE_VERSION]?.dist;
if (publishedDist !== undefined) {
if (
publishedDist?.integrity !== expectedIntegrity ||
publishedDist?.shasum !== expectedShasum
) {
throw new Error(`${packageName}@${PACKAGE_VERSION}: npm registry tarball bytes differ.`);
}
return {
action: distTags[PUBLISH_TAG] === PACKAGE_VERSION ? "skip" : "tag",
newPackage: false,
nonPlaceholderTags,
};
}
return { action: "publish", newPackage: false, nonPlaceholderTags };
}
async function readRegistry(packageName, fetchImpl = fetch) {
const result = await fetchNpmRegistryPackumentWithRetry({
packageName,
packageUrl: `https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
fetchImpl,
});
return { packument: result.packument, status: result.status };
}
async function readStableRegistry(packageName, fetchImpl = fetch) {
const observations = [];
for (let attempt = 0; attempt < 3; attempt += 1) {
observations.push(await readRegistry(packageName, fetchImpl));
}
const serialized = observations.map((observation) => JSON.stringify(observation));
if (new Set(serialized).size !== 1) {
throw new Error(`${packageName}: npm registry state changed during placeholder planning.`);
}
return observations[0];
}
function assertFreshDirectory(path) {
try {
lstatSync(path);
throw new Error(`Output directory already exists: ${path}`);
} catch (error) {
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
throw error;
}
}
mkdirSync(path, { recursive: true, mode: 0o700 });
}
/** @param {CreatePlaceholderPublicationParams} params */
export async function createPlaceholderPublication(params) {
const repoRoot = resolve(params.repoRoot);
const outputDir = resolve(params.outputDir);
const targetSha = assertCommitSha(params.targetSha, "target SHA");
const workflowSha = assertCommitSha(params.workflowSha, "workflow SHA");
const packageNames = parseSelectedPackages(params.packages);
const sources = resolveSelectedPackageSources(repoRoot, packageNames);
assertFreshDirectory(outputDir);
const packages = [];
for (const source of sources) {
const tarball = createPlaceholderTarball(source.packageName);
const tarballName = `${source.packageName.slice(1).replaceAll("/", "-")}-${PACKAGE_VERSION}.tgz`;
const identity = {
integrity: npmIntegrity(tarball),
sha256: sha256(tarball),
shasum: npmShasum(tarball),
sizeBytes: tarball.length,
};
const registry = await readStableRegistry(source.packageName, params.fetchImpl);
const state = classifyRegistryState({
expectedIntegrity: identity.integrity,
expectedShasum: identity.shasum,
packageName: source.packageName,
registry,
});
writeFileSync(join(outputDir, tarballName), tarball, { flag: "wx", mode: 0o600 });
packages.push({
...source,
action: state.action,
newPackage: state.newPackage,
preExistingDistTags: state.nonPlaceholderTags,
tarball: { name: tarballName, ...identity },
});
}
const manifest = {
schema: MANIFEST_SCHEMA,
targetSha,
workflowPath: WORKFLOW_PATH,
workflowSha,
version: PACKAGE_VERSION,
publishTag: PUBLISH_TAG,
packages,
};
writeFileSync(join(outputDir, MANIFEST_FILENAME), canonicalJson(manifest), {
flag: "wx",
mode: 0o600,
});
return manifest;
}
function validateManifest(value, params) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("Placeholder manifest must be an object.");
}
const topLevelKeys = Object.keys(value).toSorted();
if (
JSON.stringify(topLevelKeys) !==
JSON.stringify([
"packages",
"publishTag",
"schema",
"targetSha",
"version",
"workflowPath",
"workflowSha",
]) ||
value.schema !== MANIFEST_SCHEMA ||
value.targetSha !== params.targetSha ||
value.workflowSha !== params.workflowSha ||
value.workflowPath !== WORKFLOW_PATH ||
value.version !== PACKAGE_VERSION ||
value.publishTag !== PUBLISH_TAG ||
!Array.isArray(value.packages) ||
value.packages.length === 0
) {
throw new Error("Placeholder manifest identity does not match the approved publication.");
}
const names = value.packages.map((entry) => entry?.packageName);
if (new Set(names).size !== names.length || names.some((name) => !PACKAGE_NAME_RE.test(name))) {
throw new Error("Placeholder manifest package inventory is invalid.");
}
for (const entry of value.packages) {
const entryKeys = Object.keys(entry).toSorted();
if (
JSON.stringify(entryKeys) !==
JSON.stringify([
"action",
"newPackage",
"packageDir",
"packageName",
"preExistingDistTags",
"sourcePackageJsonSha256",
"tarball",
])
) {
throw new Error(`${entry.packageName}: placeholder manifest entry shape is invalid.`);
}
const expectedTarballName = `${entry.packageName.slice(1).replaceAll("/", "-")}-${PACKAGE_VERSION}.tgz`;
const tarballKeys =
entry.tarball && typeof entry.tarball === "object"
? Object.keys(entry.tarball).toSorted()
: [];
if (
!["publish", "skip", "tag"].includes(entry.action) ||
typeof entry.newPackage !== "boolean" ||
!/^extensions\/[a-z0-9][a-z0-9._-]*$/u.test(entry.packageDir) ||
!SHA256_RE.test(entry.sourcePackageJsonSha256) ||
JSON.stringify(tarballKeys) !==
JSON.stringify(["integrity", "name", "sha256", "shasum", "sizeBytes"]) ||
entry.tarball.name !== expectedTarballName ||
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(entry.tarball.integrity) ||
!SHA256_RE.test(entry.tarball.sha256) ||
!/^[0-9a-f]{40}$/u.test(entry.tarball.shasum) ||
!Number.isSafeInteger(entry.tarball.sizeBytes) ||
entry.tarball.sizeBytes <= 0 ||
entry.tarball.sizeBytes > MAX_FILE_BYTES
) {
throw new Error(`${entry.packageName}: placeholder manifest package binding is invalid.`);
}
const normalizedTags = normalizeDistTags(entry.preExistingDistTags, entry.packageName);
if (
Object.hasOwn(entry.preExistingDistTags, PUBLISH_TAG) ||
JSON.stringify(normalizedTags) !== JSON.stringify(entry.preExistingDistTags) ||
(entry.newPackage &&
(entry.action !== "publish" || Object.keys(entry.preExistingDistTags).length !== 0)) ||
((entry.action === "tag" || entry.action === "skip") && entry.newPackage)
) {
throw new Error(
`${entry.packageName}: placeholder manifest dist-tag snapshot is not canonical.`,
);
}
}
return value;
}
export async function verifyPlaceholderArtifact(params) {
const targetSha = assertCommitSha(params.targetSha, "target SHA");
const workflowSha = assertCommitSha(params.workflowSha, "workflow SHA");
const artifactName = assertTrimmedString(params.artifactName, "artifact name");
const runId = Number(params.runId);
const producerRunAttempt = Number(params.producerRunAttempt);
if (artifactName !== `npm-placeholder-publication-${runId}-${producerRunAttempt}`) {
throw new Error("Placeholder artifact name does not match its workflow run and attempt.");
}
const expected = {
artifactDigest: assertTrimmedString(params.artifactDigest, "artifact digest"),
artifactId: Number(params.artifactId),
artifactName,
artifactSizeBytes: Number(params.artifactSizeBytes),
consumerRunAttempt: Number(params.consumerRunAttempt),
producerJobName: "Plan npm placeholder publication",
repository: assertTrimmedString(params.repository, "repository"),
runAttempt: producerRunAttempt,
runId,
runStatePolicy: "same-run-producer-success",
workflowEvent: "workflow_dispatch",
workflowHeadBranch: "main",
workflowPath: WORKFLOW_PATH,
workflowSha,
};
const archive = await readPublicationArtifactArchive({
archivePolicy: {
minEntries: 2,
maxEntries: 101,
maxArchiveBytes: MAX_ARTIFACT_BYTES,
maxExpandedBytes: MAX_ARTIFACT_BYTES,
allowPath: (name) =>
basename(name) === name &&
(name === MANIFEST_FILENAME || /^[A-Za-z0-9._-]+\.tgz$/u.test(name)),
maxEntryBytes: (name) => (name === MANIFEST_FILENAME ? MAX_FILE_BYTES : MAX_FILE_BYTES),
},
expected,
maxArchiveBytes: MAX_ARTIFACT_BYTES,
token: assertTrimmedString(params.token, "GitHub token"),
});
const manifestBytes = archive.files.get(MANIFEST_FILENAME);
if (!manifestBytes) {
throw new Error("Placeholder artifact is missing its manifest.");
}
const manifest = validateManifest(JSON.parse(manifestBytes.toString("utf8")), {
targetSha,
workflowSha,
});
if (archive.files.size !== manifest.packages.length + 1) {
throw new Error("Placeholder artifact file count does not match its manifest.");
}
const sources = resolveSelectedPackageSources(
params.targetRoot,
manifest.packages.map((entry) => entry.packageName),
);
const sourceByName = new Map(sources.map((source) => [source.packageName, source]));
const outputDir = resolve(params.outputDir);
assertFreshDirectory(outputDir);
for (const entry of manifest.packages) {
const source = sourceByName.get(entry.packageName);
if (
source?.packageDir !== entry.packageDir ||
source?.sourcePackageJsonSha256 !== entry.sourcePackageJsonSha256 ||
!SHA256_RE.test(entry.tarball?.sha256)
) {
throw new Error(`${entry.packageName}: source package binding changed.`);
}
const tarball = archive.files.get(entry.tarball.name);
const expectedTarball = createPlaceholderTarball(entry.packageName);
if (
!tarball ||
!tarball.equals(expectedTarball) ||
entry.tarball.sizeBytes !== tarball.length ||
entry.tarball.sha256 !== sha256(tarball) ||
entry.tarball.integrity !== npmIntegrity(tarball) ||
entry.tarball.shasum !== npmShasum(tarball)
) {
throw new Error(`${entry.packageName}: placeholder tarball is not canonical.`);
}
writeFileSync(join(outputDir, entry.tarball.name), tarball, { flag: "wx", mode: 0o600 });
}
writeFileSync(join(outputDir, MANIFEST_FILENAME), canonicalJson(manifest), {
flag: "wx",
mode: 0o600,
});
return { artifactSha256: sha256Digest(archive.archiveBytes), manifest };
}
function sameTags(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
export function assertFinalRegistryState(entry, registry) {
const state = classifyRegistryState({
expectedIntegrity: entry.tarball.integrity,
expectedShasum: entry.tarball.shasum,
packageName: entry.packageName,
registry,
});
if (state.action !== "skip") {
throw new Error(`${entry.packageName}: placeholder publication did not reach its final state.`);
}
if (!sameTags(state.nonPlaceholderTags, entry.preExistingDistTags)) {
throw new Error(`${entry.packageName}: a non-placeholder dist-tag changed during publication.`);
}
}
function defaultNpmRunner(args, options) {
const result = spawnSync("npm", args, {
cwd: options.cwd,
encoding: "utf8",
env: options.env,
killSignal: "SIGTERM",
stdio: ["ignore", "pipe", "pipe"],
timeout: 300_000,
});
if (result.status !== 0) {
throw new Error(
`npm ${args[0]} failed (${result.status ?? "signal"}): ${(result.stderr || result.stdout).trim()}`,
);
}
}
async function readFinalRegistry(entry, params) {
const attempts = params.registryAttempts ?? 8;
const sleep =
params.sleep ??
((delayMs) =>
new Promise((done) => {
setTimeout(done, delayMs);
}));
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const registry = await readRegistry(entry.packageName, params.fetchImpl);
assertFinalRegistryState(entry, registry);
return;
} catch (error) {
lastError = error;
if (attempt < attempts) {
await sleep(attempt * 1000);
}
}
}
throw lastError;
}
/** @param {PublishPlaceholdersParams} params */
export async function publishPlaceholders(params) {
const artifactDir = resolve(params.artifactDir);
const manifest = validateManifest(
readJsonRegularFile(join(artifactDir, MANIFEST_FILENAME), "Placeholder manifest"),
{
targetSha: params.targetSha,
workflowSha: params.workflowSha,
},
);
const npmToken = assertTrimmedString(params.npmToken, "NPM token");
const publishHome = mkdtempSync(
join(resolve(params.tempRoot ?? tmpdir()), "openclaw-npm-placeholder-"),
);
try {
const npmrc = join(publishHome, "npmrc");
writeFileSync(
npmrc,
`registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${npmToken}\n`,
{
mode: 0o600,
},
);
const childEnv = { ...process.env };
delete childEnv.NPM_TOKEN;
delete childEnv.NODE_AUTH_TOKEN;
delete childEnv.NODE_OPTIONS;
Object.assign(childEnv, {
HOME: publishHome,
NPM_CONFIG_GLOBALCONFIG: "/dev/null",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/",
NPM_CONFIG_USERCONFIG: npmrc,
});
const npmRunner = params.npmRunner ?? defaultNpmRunner;
const results = [];
for (const entry of manifest.packages) {
const current = await readRegistry(entry.packageName, params.fetchImpl);
const state = classifyRegistryState({
expectedIntegrity: entry.tarball.integrity,
expectedShasum: entry.tarball.shasum,
packageName: entry.packageName,
registry: current,
});
if (!sameTags(state.nonPlaceholderTags, entry.preExistingDistTags)) {
throw new Error(`${entry.packageName}: npm dist-tags changed after the immutable plan.`);
}
let mutationError;
try {
if (state.action === "publish") {
npmRunner(
[
"publish",
join(artifactDir, entry.tarball.name),
"--access",
"public",
"--ignore-scripts",
"--provenance",
"--tag",
PUBLISH_TAG,
],
{ cwd: artifactDir, env: childEnv },
);
} else if (state.action === "tag") {
npmRunner(["dist-tag", "add", `${entry.packageName}@${PACKAGE_VERSION}`, PUBLISH_TAG], {
cwd: artifactDir,
env: childEnv,
});
}
} catch (error) {
mutationError = error;
}
try {
await readFinalRegistry(entry, params);
} catch (readbackError) {
if (mutationError) {
throw new Error(
`${entry.packageName}: npm mutation failed and exact registry readback did not converge: ${mutationError.message}`,
{ cause: readbackError },
);
}
throw readbackError;
}
results.push({
action: state.action,
newPackage: entry.newPackage,
packageName: entry.packageName,
});
}
return { results };
} finally {
rmSync(publishHome, { force: true, recursive: true });
}
}
function parseCliArgs(argv) {
const [command, ...rest] = argv;
if (!["create", "verify-artifact", "publish"].includes(command)) {
throw new Error("Usage: npm-placeholder-publication.mjs <create|verify-artifact|publish> ...");
}
const values = {};
for (let index = 0; index < rest.length; index += 2) {
const key = rest[index];
const value = rest[index + 1];
if (!key?.startsWith("--") || value === undefined || value.startsWith("--")) {
throw new Error(`Invalid ${command} argument near ${key ?? "<missing>"}.`);
}
const name = key.slice(2).replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase());
if (values[name] !== undefined) {
throw new Error(`Duplicate ${command} option: ${key}`);
}
values[name] = value;
}
return { command, values };
}
function appendGithubOutput(path, values) {
if (!path) {
return;
}
writeFileSync(
path,
`${Object.entries(values)
.map(([key, value]) => `${key}=${value}`)
.join("\n")}\n`,
{ flag: "a" },
);
}
export async function main(argv = process.argv.slice(2)) {
const { command, values } = parseCliArgs(argv);
if (command === "create") {
const manifest = await createPlaceholderPublication({
outputDir: values.outputDir,
packages: values.packages,
repoRoot: values.repoRoot,
targetSha: values.targetSha,
workflowSha: values.workflowSha,
});
appendGithubOutput(values.githubOutput, {
existing_without_zero_count: manifest.packages.filter(
(entry) => !entry.newPackage && entry.action === "publish",
).length,
new_package_count: manifest.packages.filter((entry) => entry.newPackage).length,
package_count: manifest.packages.length,
});
return;
}
if (command === "verify-artifact") {
const result = await verifyPlaceholderArtifact({
artifactDigest: values.artifactDigest,
artifactId: values.artifactId,
artifactName: values.artifactName,
artifactSizeBytes: values.artifactSizeBytes,
consumerRunAttempt: values.consumerRunAttempt,
outputDir: values.outputDir,
producerRunAttempt: values.producerRunAttempt,
repository: values.repository,
runId: values.runId,
targetRoot: values.targetRoot,
targetSha: values.targetSha,
token: process.env.GH_TOKEN,
workflowSha: values.workflowSha,
});
appendGithubOutput(values.githubOutput, { artifact_sha256: result.artifactSha256 });
return;
}
const result = await publishPlaceholders({
artifactDir: values.artifactDir,
npmToken: process.env.NPM_TOKEN,
targetSha: values.targetSha,
workflowSha: values.workflowSha,
});
const resultPath = resolve(values.resultPath);
writeFileSync(resultPath, canonicalJson(result), { flag: "wx", mode: 0o600 });
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(
/** @param {unknown} error */ (error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
},
);
}