127 lines
4.5 KiB
JavaScript
127 lines
4.5 KiB
JavaScript
import { appendFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { setTimeout } from "node:timers/promises";
|
|
import { z } from "zod";
|
|
import { prerelease, valid } from "semver";
|
|
import { releaseNpmTag } from "../release-npm-tag.mjs";
|
|
import { verifyPublishedPackageIntegrity } from "./verified-package.mjs";
|
|
|
|
/** Parse one npm metadata snapshot; errors and malformed records are never absence. */
|
|
export async function readNpmRelease(version, request = fetch) {
|
|
const tag = releaseNpmTag(version);
|
|
const response = await request("https://registry.npmjs.org/rea-agents", {
|
|
headers: {
|
|
Accept: "application/vnd.npm.install-v1+json",
|
|
"Cache-Control": "no-cache",
|
|
},
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
if (response.status === 404) return { status: "absent" };
|
|
if (!response.ok)
|
|
throw new Error(`npm registry lookup failed: HTTP ${response.status}`);
|
|
const metadata = z
|
|
.object({
|
|
name: z.literal("rea-agents"),
|
|
versions: z.record(z.string(), z.unknown()),
|
|
"dist-tags": z.record(z.string(), z.string()),
|
|
})
|
|
.parse(await response.json());
|
|
if (!Object.hasOwn(metadata.versions, version)) return { status: "absent" };
|
|
const published = z
|
|
.object({
|
|
name: z.literal("rea-agents"),
|
|
version: z.literal(version),
|
|
dist: z.object({ integrity: z.string().min(1) }),
|
|
})
|
|
.parse(metadata.versions[version]);
|
|
return {
|
|
status: "published",
|
|
integrity: published.dist.integrity,
|
|
tag,
|
|
taggedVersion: metadata["dist-tags"][tag] ?? null,
|
|
stableVersion: metadata["dist-tags"].latest ?? null,
|
|
};
|
|
}
|
|
|
|
/** REA update follows stable latest even when the candidate is published on next. */
|
|
export function stableUpdateVersion(version, published) {
|
|
if (published.status !== "published" || published.taggedVersion !== version)
|
|
throw new Error(`npm does not select ${version} on its release channel`);
|
|
const stable = published.stableVersion;
|
|
if (
|
|
typeof stable !== "string" ||
|
|
valid(stable) !== stable ||
|
|
prerelease(stable) !== null
|
|
)
|
|
throw new Error(
|
|
"npm latest does not select a stable release for the update canary",
|
|
);
|
|
return stable;
|
|
}
|
|
|
|
/** Validate content before accepting a previously published or newly visible version. */
|
|
export async function requireMatchingNpmRelease(tarball, version, published) {
|
|
if (published.status !== "published")
|
|
throw new Error(`rea-agents@${version} is not published`);
|
|
await verifyPublishedPackageIntegrity(tarball, published.integrity);
|
|
if (published.taggedVersion !== version)
|
|
throw new Error(
|
|
`Expected npm ${published.tag} to select ${version}, observed ${published.taggedVersion ?? "missing"}; refusing to change an existing dist-tag`,
|
|
);
|
|
}
|
|
|
|
/** Retry only confirmed absence during the existing registry propagation window. */
|
|
export async function waitForNpmRelease(
|
|
version,
|
|
lookup = readNpmRelease,
|
|
pause = setTimeout,
|
|
) {
|
|
const attempts = 120;
|
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
const published = await lookup(version);
|
|
if (published.status === "published") return published;
|
|
if (attempt < attempts) await pause(15_000);
|
|
}
|
|
throw new Error(
|
|
`rea-agents@${version} was not visible on npm after 120 attempts over 30 minutes`,
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
const [operation, tarball, version, ...extra] = process.argv.slice(2);
|
|
if (
|
|
!["inspect", "verify"].includes(operation) ||
|
|
tarball === undefined ||
|
|
version === undefined ||
|
|
extra.length
|
|
)
|
|
throw new Error(
|
|
"Usage: node scripts/ci/npm-release.mjs inspect|verify <qualified-tarball> <version>",
|
|
);
|
|
const published =
|
|
operation === "inspect"
|
|
? await readNpmRelease(version)
|
|
: await waitForNpmRelease(version);
|
|
if (published.status === "published")
|
|
await requireMatchingNpmRelease(tarball, version, published);
|
|
if (operation === "inspect") {
|
|
await appendFile(
|
|
z.string().min(1).parse(process.env.GITHUB_OUTPUT),
|
|
`published=${published.status === "published"}\n`,
|
|
);
|
|
} else if (process.env.GITHUB_STEP_SUMMARY !== undefined) {
|
|
await appendFile(
|
|
process.env.GITHUB_STEP_SUMMARY,
|
|
`npm: rea-agents@${version} is published, matches the qualified tarball, and is selected by ${published.tag}.\n`,
|
|
);
|
|
}
|
|
process.stdout.write(`${JSON.stringify({ version, ...published })}\n`);
|
|
}
|
|
|
|
if (
|
|
process.argv[1] !== undefined &&
|
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
)
|
|
await main();
|