planRelease

tcmts.cli.releases.planRelease · #rszbgjlo · term · runs on the server

uses tcmts.cli.manifests.packagesIn, which uses tcmts.cli.config.configValue, which uses node:path

The next release of a package on this branch: its version from the API's changes since its previous release (or as given, if that's at least as far).

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { exportsOf, packagesIn } from "#codebase/tcmts/cli/manifests";
import {
  apiReport, blockers, nextVersion, packageState, previousRelease, ReleaseError, rowsAt,
} from "#codebase/tcmts/cli/releases";
import type { PackageState, Plan } from "#codebase/tcmts/cli/releases";
import { highest, levelOf } from "#codebase/tcmts/cli/semver_checks";
import { shortHash } from "#codebase/tcmts/core/hash";
import { compareVersions, isExactVersion } from "#codebase/tcmts/core/semver";

export function planRelease(
  cb: Codebase,
  name: string | undefined,
  version: string | undefined,
  force: boolean,
): Plan {
  const head = cb.store.head();
  const rows = cb.store.names();
  const all = packagesIn(cb, rows);
  if (!all.length)
    throw new ReleaseError(
      "no packages here: a definition named `manifest` makes its namespace one (see docs/publishing.md)",
    );
  if (name === undefined && all.length > 1)
    throw new ReleaseError(`which package? ${all.map((p) => p.name).join(", ")}`);
  const pkg = name === undefined ? all[0]! : all.find((p) => p.name === name);
  if (!pkg)
    throw new ReleaseError(`no package ${name} (there's ${all.map((p) => p.name).join(", ")})`);
  const now: PackageState = { pkg, rows, exported: exportsOf(pkg, all, rows) };
  const prev = previousRelease(cb, pkg.name, head);
  // (its code then, as `rows` is its code now: the files kept with it, a README, aren't its API)
  const then =
    prev &&
    packageState(
      cb,
      rowsAt(cb, prev.causal).filter((r) => r.kind !== "file"),
      pkg.name,
    );
  const report = then ? apiReport(cb, then, now) : undefined;
  const level = report && highest(report.findings);
  const computed = nextVersion(prev?.version, level);
  const v = version ?? computed;
  const at = `${pkg.name}@${v}`;
  if (!isExactVersion(v))
    throw new ReleaseError(`'${v}' isn't a version (major.minor.patch, like ${computed})`);
  const existing = cb.store.release(pkg.name, v);
  if (existing)
    throw new ReleaseError(
      `${at} is already released (as ${shortHash(existing.causal)}); releases don't change`,
    );
  if (prev && compareVersions(v, prev.version) <= 0)
    throw new ReleaseError(
      `${v} isn't after ${prev.version}, the release of ${pkg.name} this branch's history has`,
    );
  if (!force) {
    const blocked = blockers(cb, pkg, all, rows);
    if (blocked) throw new ReleaseError(blocked);
    if (report && !report.findings.length && version === undefined) {
      throw new ReleaseError(
        `${pkg.name} has nothing new since ${prev!.version}${report.following.length ? " (only what it uses changed, in other packages)" : ""}: no release to make (give a version, or --force, to make one anyway)`,
      );
    }
    if (compareVersions(v, computed) < 0) {
      const why = [
        ...new Set(report!.findings.filter((f) => levelOf(f) === level).map((f) => f.path)),
      ];
      throw new ReleaseError(
        `${v} is too small a step from ${prev!.version}: ${level === "major" ? "breaking changes" : "additions"} (${why.slice(0, 5).join(", ")}${why.length > 5 ? ", …" : ""}) call for ${computed} (\`tcm release check ${pkg.name}\` shows why; --force releases anyway)`,
      );
    }
  }
  return { pkg, version: v, computed, head, now, prev, report, level };
}

uses

used by