scripts/check-release-metadata.js
Rama main · réplica de lectura del repositorio original.
import { access, readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const RELEASE_HEADING = /^## \[([^\]]+)\] - \d{4}-\d{2}-\d{2}$/mu;
const SEMVER =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:(?:0|[1-9]\d*)|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
// Where readers are told which release is stable: "stable 1.2.3", "stable Relmio 1.2.3" or
// "Relmio 1.2.3 is the current stable release" in the READMEs, and the website's fallback constant.
const README_STABLE_VERSION =
/\bstable\s+(?:Relmio\s+)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]*[0-9A-Za-z])?)|\bRelmio\s+(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]*[0-9A-Za-z])?)\s+is\s+the\s+current\s+stable\b/gu;
const STABLE_VERSION_SOURCES = Object.freeze({
"README.md": README_STABLE_VERSION,
"npm/README.md": README_STABLE_VERSION,
"web/app/project-version.ts": /\bpreparedReleaseVersion\s*=\s*"([^"]*)"/gu,
});
export function validateReleaseMetadata({
packageJson,
packageLock,
changelog,
tag,
requirePackageLock = true,
stableVersionSources = {},
requireStableVersionSources = false,
}) {
const version = packageJson?.version;
if (typeof version !== "string" || !SEMVER.test(version)) {
throw new Error("package.json must contain a valid semantic version.");
}
if (packageLock || requirePackageLock) {
if (packageLock?.version !== version) {
throw new Error(
`package-lock.json version ${packageLock?.version ?? "missing"} does not match ${version}.`,
);
}
if (packageLock?.packages?.[""]?.version !== version) {
throw new Error(
`package-lock.json root package version ${packageLock?.packages?.[""]?.version ?? "missing"} does not match ${version}.`,
);
}
}
const changelogVersion = changelog.match(RELEASE_HEADING)?.[1];
if (changelogVersion !== version) {
throw new Error(
`newest CHANGELOG.md version ${changelogVersion ?? "missing"} does not match ${version}.`,
);
}
if (tag && tag !== `v${version}`) {
throw new Error(`release tag ${tag} does not match v${version}.`);
}
// A prerelease keeps naming the last stable release, so only a stable version is compared.
if (!version.split("+", 1)[0].includes("-")) {
for (const [file, pattern] of Object.entries(STABLE_VERSION_SOURCES)) {
const contents = stableVersionSources[file];
if (contents === undefined) {
if (requireStableVersionSources) {
throw new Error(`${file} is missing; it must name stable version ${version}.`);
}
continue;
}
const named = [...contents.matchAll(pattern)].map((match) => match[1] ?? match[2]);
if (named.length === 0) {
throw new Error(`${file} does not name stable version ${version}.`);
}
const stale = named.find((candidate) => candidate !== version);
if (stale !== undefined) {
throw new Error(`${file} names stable version ${stale} instead of ${version}.`);
}
}
}
return { version };
}
export function npmReleaseTag({ version, tag, prerelease }) {
if (typeof version !== "string" || !SEMVER.test(version)) {
throw new Error("package.json must contain a valid semantic version.");
}
if (tag !== `v${version}`) {
throw new Error(`release tag ${tag ?? "missing"} does not match v${version}.`);
}
if (typeof prerelease !== "boolean") {
throw new Error("GitHub release prerelease flag must be a boolean.");
}
const versionIsPrerelease = version.split("+", 1)[0].includes("-");
if (prerelease !== versionIsPrerelease) {
throw new Error("GitHub release prerelease flag does not match the package version.");
}
return prerelease ? "experimental" : "latest";
}
async function readOptionalFile(path) {
try {
return await readFile(path, "utf8");
} catch (error) {
if (error.code === "ENOENT") {
return undefined;
}
throw error;
}
}
async function pathExists(path) {
try {
await access(path);
return true;
} catch (error) {
if (error.code === "ENOENT") {
return false;
}
throw error;
}
}
async function main() {
const stableVersionFiles = Object.keys(STABLE_VERSION_SOURCES);
const [packageJsonContents, packageLockContents, changelog, isGitCheckout, ...stableVersionContents] =
await Promise.all([
readFile("package.json", "utf8"),
readOptionalFile("package-lock.json"),
readFile("CHANGELOG.md", "utf8"),
pathExists(".git"),
...stableVersionFiles.map(readOptionalFile),
]);
const selectingNpmTag = process.argv[2] === "--npm-dist-tag";
const tag = selectingNpmTag
? process.env.RELEASE_TAG
: process.env.GITHUB_REF_TYPE === "tag"
? process.env.GITHUB_REF_NAME
: undefined;
const result = validateReleaseMetadata({
packageJson: JSON.parse(packageJsonContents),
packageLock:
packageLockContents === undefined
? undefined
: JSON.parse(packageLockContents),
changelog,
tag,
requirePackageLock: isGitCheckout,
// A packed install has only its own README; a Git checkout must have all three sources.
stableVersionSources: Object.fromEntries(
stableVersionFiles.map((file, index) => [file, stableVersionContents[index]]),
),
requireStableVersionSources: isGitCheckout,
});
if (selectingNpmTag) {
const flag = process.env.RELEASE_PRERELEASE;
if (flag !== "true" && flag !== "false") {
throw new Error("GitHub release prerelease flag must be true or false.");
}
console.log(npmReleaseTag({
version: result.version,
tag,
prerelease: flag === "true",
}));
return;
}
console.log(`Release metadata is synchronized at v${result.version}.`);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}