Ctrl K
ComunidadGuías…

scripts/check-release-metadata.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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;
  });
}