#!/usr/bin/env node /** * Release helper: bump the app version, move the changelog's unreleased * entries under the new version, commit, and tag. * * npm run release -- patch * npm run release -- minor --push * npm run release -- 4.1.0 --push * * Runs the type check, the test suite and the compiler compatibility check * first (skip with --no-tests) and refuses to run on a dirty (tracked) tree. * `--dry-run` prints the plan and stops before anything is written. * * The tag is what CI and the distro build see. The web site is deployed from * the `release` branch, so to publish: * * git push origin release */ import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { bumpVersion, releaseChangelog, today } from './release-lib.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const push = args.includes('--push'); const skipTests = args.includes('--no-tests'); const kind = args.find((arg) => !arg.startsWith('--')); function fail(message) { console.error(`release: ${message}`); process.exit(1); } if (!kind) { fail('usage: npm run release -- [--push] [--dry-run] [--no-tests]'); } const run = (cmd, cmdArgs) => execFileSync(cmd, cmdArgs, { cwd: root, stdio: 'inherit' }); const capture = (cmd, cmdArgs) => execFileSync(cmd, cmdArgs, { cwd: root, encoding: 'utf8' }).trim(); // Ignore untracked scratch files (there are usually a few in this repo), but // refuse to commit a release on top of uncommitted tracked changes. Only the // three release files are staged below, so untracked files are never added. if (capture('git', ['status', '--porcelain', '--untracked-files=no'])) { fail('working tree has uncommitted tracked changes; commit or stash first'); } if (capture('git', ['rev-parse', '--abbrev-ref', 'HEAD']) === 'HEAD') { fail('detached HEAD; check out a branch first'); } const packagePath = join(root, 'package.json'); const packageLockPath = join(root, 'package-lock.json'); const changelogPath = join(root, 'CHANGELOG.md'); const changelog = readFileSync(changelogPath, 'utf8'); const current = JSON.parse(readFileSync(packagePath, 'utf8')).version; const next = bumpVersion(current, kind); const tag = `v${next}`; const updatedChangelog = releaseChangelog(changelog, next, today()); if (capture('git', ['tag', '--list', tag])) { fail(`tag ${tag} already exists`); } if (next === current) { console.log(`release: releasing current version ${next} (no bump)`); } else { console.log(`release: ${current} -> ${next}`); } console.log(`release: changelog ${updatedChangelog === changelog ? 'unchanged' : 'updated'}`); if (dryRun) { console.log(`release: dry run, would commit and tag ${tag}`); process.exit(0); } if (!skipTests) { try { run('npm', ['run', 'tslint']); run('npm', ['test']); // the examples of an older release still have to build with the // compilers we ship now run('npm', ['run', 'compatcheck', '--', '--baseline', 'test/compat/3.12.1.json']); } catch { fail('typecheck, tests or the compatibility check failed; nothing was bumped'); } } // Bump package.json and package-lock.json. --no-git-tag-version keeps npm from // committing/tagging, so this script controls the commit contents and message. run('npm', ['version', next, '--no-git-tag-version', '--allow-same-version']); writeFileSync(changelogPath, updatedChangelog); run('git', ['add', packagePath, packageLockPath, changelogPath]); run('git', ['commit', '-m', tag]); run('git', ['tag', '-a', tag, '-m', tag]); if (push) { run('git', ['push', '--follow-tags']); console.log(`release: pushed ${tag}`); } else { console.log(`release: done. next:\n git push --follow-tags\n git push origin release # when the release branch is ready to deploy`); }