'use strict';
const { readPackageJSON, spawn, spawnOutput } = require('./utils.js');
let args;
try {
args = parseArgs();
validateBranchState(args.releaseBranch);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
console.log('Installing dependencies...');
spawn('npm', ['ci', '--ignore-scripts']);
console.log('Bumping package version without creating a tag...');
spawn('npm', ['version', ...args.npmVersionArgs, '--no-git-tag-version']);
console.log('Updating src/version.ts...');
spawn('node', ['resources/gen-version.js']);
console.log('Running test suite...');
spawn('npm', ['run', 'test']);
const { version } = readPackageJSON();
console.log(`Generating changelog for v${version}...`);
const changelogArgs = ['run', '--silent', 'changelog'];
if (args.fromRev != null) {
changelogArgs.push('--', args.fromRev);
}
const releaseChangelog = spawnOutput('npm', changelogArgs);
const releaseCommitTitle = `chore(release): v${version}`;
console.log('Creating release commit...');
spawn('git', ['add', 'package.json', 'package-lock.json', 'src/version.ts']);
spawn('git', ['commit', '-m', releaseCommitTitle, '-m', releaseChangelog]);
const currentBranch = spawnOutput('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
console.log('');
console.log(`Release commit created for v${version}.`);
console.log(
`Next steps: push "${currentBranch}", open a PR to "${args.releaseBranch}", wait for CI, then merge.`,
);
function parseArgs() {
const rawArgs = process.argv.slice(2);
const fromRevArgName = '--fromRev';
let fromRev = null;
let releasePrepareArgs = rawArgs;
if (rawArgs[0] === fromRevArgName) {
fromRev = rawArgs[1] || null;
releasePrepareArgs = rawArgs.slice(2);
} else if (rawArgs.includes(fromRevArgName)) {
throwUsage(`${fromRevArgName} must be the first argument when provided.`);
}
const releaseBranch = releasePrepareArgs[0];
if (releaseBranch == null || releaseBranch.trim() === '') {
throwUsage('Missing required release branch as the first argument.');
}
if (releaseBranch.startsWith('-')) {
throwUsage(
'Missing required release branch as the first argument (before options).',
);
}
const npmVersionArgs = releasePrepareArgs.slice(1);
if (npmVersionArgs.length === 0) {
throwUsage(
'Missing npm version arguments (e.g. patch, major, prerelease --preid alpha).',
);
}
return {
fromRev,
releaseBranch,
npmVersionArgs,
};
}
function validateBranchState(releaseBranch) {
const checkedBranch = spawnOutput('git', [
'rev-parse',
'--abbrev-ref',
'HEAD',
]);
if (checkedBranch === 'HEAD') {
throw new Error(
'Git is in detached HEAD state (not on a local branch). ' +
'Switch to a local branch based on the release branch first, for example:\n' +
` git switch -c release-${releaseBranch.replace(
/[^a-zA-Z0-9._-]/g,
'-',
)} ${releaseBranch}`,
);
}
if (checkedBranch === releaseBranch) {
throw new Error(
`Release prepare must not run on "${releaseBranch}". Create a local release branch first.`,
);
}
const status = spawnOutput('git', ['status', '--porcelain']).trim();
if (status !== '') {
throw new Error(
'Working directory must be clean before running release:prepare.',
);
}
const branchStatus = spawnOutput('git', [
'status',
'--porcelain',
'--branch',
]);
const branchSummary = branchStatus.split('\n')[0] || '';
if (/\[[^\]]+\]/.test(branchSummary)) {
throw new Error(
`Current branch "${checkedBranch}" is not up to date with its upstream.`,
);
}
let releaseBranchHead;
try {
releaseBranchHead = spawnOutput('git', ['rev-parse', releaseBranch]);
} catch (error) {
throw new Error(
`Release branch "${releaseBranch}" does not exist locally.`,
{
cause: error,
},
);
}
let releaseBranchUpstream;
try {
releaseBranchUpstream = spawnOutput('git', [
'rev-parse',
'--abbrev-ref',
`${releaseBranch}@{upstream}`,
]);
} catch (error) {
throw new Error(
`Release branch "${releaseBranch}" does not track a remote branch. ` +
'Set one first (for example: git branch --set-upstream-to ' +
`<remote>/${releaseBranch} ${releaseBranch}).`,
{ cause: error },
);
}
const upstreamRemote = releaseBranchUpstream.split('/')[0];
try {
spawn('git', ['fetch', '--quiet', '--tags', upstreamRemote, releaseBranch]);
} catch (error) {
throw new Error(
`Failed to fetch "${releaseBranchUpstream}" and tags from "${upstreamRemote}". ` +
'Check remote access, authentication, git remote configuration, ' +
'and local/remote tag state.',
{ cause: error },
);
}
const upstreamReleaseBranchHead = spawnOutput('git', [
'rev-parse',
`${releaseBranch}@{upstream}`,
]);
const localOnlyCommitsRaw = spawnOutput('git', [
'rev-list',
`${upstreamReleaseBranchHead}..${releaseBranchHead}`,
]);
const upstreamOnlyCommitsRaw = spawnOutput('git', [
'rev-list',
`${releaseBranchHead}..${upstreamReleaseBranchHead}`,
]);
const localOnlyCommits =
localOnlyCommitsRaw === '' ? [] : localOnlyCommitsRaw.split('\n');
const upstreamOnlyCommits =
upstreamOnlyCommitsRaw === '' ? [] : upstreamOnlyCommitsRaw.split('\n');
if (localOnlyCommits.length > 0 && upstreamOnlyCommits.length > 0) {
throw new Error(
`Local "${releaseBranch}" has diverged from "${releaseBranchUpstream}". ` +
'Resolve conflicts and synchronize first (for example: ' +
`git switch ${releaseBranch} && git pull --rebase).`,
);
}
if (upstreamOnlyCommits.length > 0) {
throw new Error(
`Local "${releaseBranch}" is behind "${releaseBranchUpstream}". ` +
`Update it first (for example: git switch ${releaseBranch} && git pull --ff-only).`,
);
}
if (localOnlyCommits.length > 0) {
throw new Error(
`Local "${releaseBranch}" is ahead of "${releaseBranchUpstream}". ` +
`Push or reset it before release prepare (for example: git switch ${releaseBranch} && git push).`,
);
}
const currentHead = spawnOutput('git', ['rev-parse', 'HEAD']);
if (currentHead !== releaseBranchHead) {
throw new Error(
`Current branch "${checkedBranch}" must match "${releaseBranch}" before preparing a release.`,
);
}
}
function throwUsage(message) {
throw new Error(
`${message}\n` +
'Usage: npm run release:prepare -- [--fromRev <fromRev>] <release-branch> <npm version args>\n' +
'Examples:\n' +
' npm run release:prepare -- 16.x.x patch\n' +
' npm run release:prepare -- 16.x.x prerelease --preid alpha\n' +
' npm run release:prepare -- --fromRev <fromRev> 16.x.x prerelease --preid alpha',
);
}