Ctrl K
ComunidadGuías…

scripts/release.js

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

Archivo original ↗Preguntar a KiBot
// Turns changelog.d/ fragments into a release: run by .github/workflows/release.yml
// on every push to main. PRs never edit VERSION or CHANGELOG.md, so parallel
// PRs cannot conflict on them; each adds its own changelog.d/<slug>.md:
//
//   ---
//   bump: patch
//   ---
//   ### Fixed
//
//   - ...
//
// `node scripts/release.js` collects every fragment, bumps VERSION by the
// largest level among them, writes package.json and package-lock.json's
// MAJOR.MINOR.PATCH, adds a "## [X] - date" section to the top of CHANGELOG.md,
// deletes the fragments and prints the new version. With no fragments it
// changes nothing and prints nothing. See CONTRIBUTING.md "Releases".
import { readFileSync, writeFileSync, readdirSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { pathToFileURL } from 'url';

export const LEVELS = ['major', 'minor', 'patch', 'micro'];

export function parseFragment(text, name = 'fragment') {
  const m = text.replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
  const bump = m?.[1].match(/^bump:\s*(\S+)\s*$/m)?.[1];
  if (!LEVELS.includes(bump)) {
    throw new Error(`${name}: needs front matter "---\\nbump: ${LEVELS.join('|')}\\n---"`);
  }
  const body = m[2].trim();
  if (!body) throw new Error(`${name}: has no release notes under its front matter`);
  return { bump, body };
}

// The largest bump wins and zeroes the parts after it: 0.6.6.3 + patch = 0.6.7.0.
export function nextVersion(version, bumps) {
  const parts = version.trim().split('.').map(Number);
  if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0)) {
    throw new Error(`VERSION must be MAJOR.MINOR.PATCH.MICRO, got "${version.trim()}"`);
  }
  const i = Math.min(...bumps.map((b) => LEVELS.indexOf(b)));
  return parts.map((n, j) => (j < i ? n : j === i ? n + 1 : 0)).join('.');
}

export function changelogWith(changelog, version, date, bodies) {
  const section = `## [${version}] - ${date}\n\n${bodies.join('\n\n')}\n\n`;
  const at = changelog.search(/^## \[/m);
  return at === -1 ? `${changelog.trimEnd()}\n\n${section}` : changelog.slice(0, at) + section + changelog.slice(at);
}

export function release(root, date = new Date().toISOString().slice(0, 10)) {
  const dir = join(root, 'changelog.d');
  const names = existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'README.md').sort() : [];
  if (!names.length) return null;
  const fragments = names.map((n) => parseFragment(readFileSync(join(dir, n), 'utf8'), `changelog.d/${n}`));

  const version = nextVersion(readFileSync(join(root, 'VERSION'), 'utf8'), fragments.map((f) => f.bump));
  const semver = version.split('.').slice(0, 3).join('.');
  writeFileSync(join(root, 'VERSION'), `${version}\n`);
  for (const file of ['package.json', 'package-lock.json']) {
    const path = join(root, file);
    if (!existsSync(path)) continue;
    const json = JSON.parse(readFileSync(path, 'utf8'));
    json.version = semver;
    if (json.packages?.['']) json.packages[''].version = semver;
    writeFileSync(path, `${JSON.stringify(json, null, 2)}\n`);
  }
  const changelog = join(root, 'CHANGELOG.md');
  writeFileSync(changelog, changelogWith(readFileSync(changelog, 'utf8'), version, date, fragments.map((f) => f.body)));
  for (const n of names) rmSync(join(dir, n));
  return version;
}

if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
  const version = release(process.argv[2] || '.');
  if (version) console.log(version);
}