Files
main/test-release.cjs
T

87 lines
4.7 KiB
JavaScript

// Exercise the actual release command in disposable repositories, never the workspace Git.
const assert = require('node:assert/strict');
const { execFileSync, spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const source = path.resolve(process.argv[2] || __dirname);
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'wealthledger-release-'));
const env = { ...process.env, HUSKY: '0', NO_COLOR: '1' };
const git = (...args) => execFileSync('git', args, { cwd, env, encoding: 'utf8' }).trim();
const read = file => fs.readFileSync(path.join(cwd, file), 'utf8');
const write = (file, text) => fs.writeFileSync(path.join(cwd, file), text);
const prepare = (...args) => spawnSync(process.execPath,
[path.join(source, 'node_modules/release-it/bin/release-it.js'), '--ci', ...args],
{ cwd, env, encoding: 'utf8' });
const mustPass = result => assert.equal(result.status, 0, result.stdout + result.stderr);
const mustFail = result => assert.notEqual(result.status, 0, result.stdout + result.stderr);
const commit = message => git('commit', '--allow-empty', '-qm', message);
try {
fs.mkdirSync(path.join(cwd, 'scripts'));
fs.symlinkSync(path.join(source, 'node_modules'), path.join(cwd, 'node_modules'), 'dir');
for (const file of ['.release-it.json', 'scripts/check-release.cjs', 'commitlint.config.cjs']) {
fs.copyFileSync(path.join(source, file), path.join(cwd, file));
}
write('.gitignore', 'node_modules/\nremote.git/\n');
write('CHANGELOG.md', '# Changelog\n');
write('package.json', JSON.stringify({ name: 'release-fixture', private: true, version: '0.0.0' }));
write('package-lock.json', JSON.stringify({ name: 'release-fixture', version: '0.0.0', lockfileVersion: 3, packages: { '': { name: 'release-fixture', version: '0.0.0' } } }));
git('init', '-q', '-b', 'main');
git('config', 'user.name', 'Release test');
git('config', 'user.email', 'release@example.invalid');
git('config', 'core.hooksPath', '/dev/null');
git('init', '--bare', '-q', 'remote.git');
git('remote', 'add', 'origin', path.join(cwd, 'remote.git'));
git('add', '.');
commit('chore: initialize fixture');
mustFail(prepare('0.1.0')); // main cannot prepare releases.
git('switch', '-qc', 'release/test');
write('untracked.txt', 'must block\n');
mustFail(prepare('0.1.0'));
fs.unlinkSync(path.join(cwd, 'untracked.txt'));
const before = ['package.json', 'package-lock.json', 'CHANGELOG.md'].map(read);
const head = git('rev-parse', 'HEAD');
mustPass(prepare('0.1.0', '--dry-run'));
assert.deepEqual(['package.json', 'package-lock.json', 'CHANGELOG.md'].map(read), before);
assert.equal(git('rev-parse', 'HEAD'), head);
mustPass(prepare('0.1.0'));
assert.equal(git('tag', '--list'), '', 'Preparation must not create tags');
assert.equal(git('log', '-1', '--format=%s'), 'chore(release): 0.1.0');
mustFail(prepare()); // Prepared version cannot be bumped again before tagging.
git('tag', '-a', 'v0.1.0', '-m', 'Release 0.1.0');
mustFail(prepare()); // No new commits.
for (const [message, expected] of [
['fix: correct calculation', '0.1.1'],
['feat: add export', '0.2.0'],
['docs: explain export', '0.2.1'],
['spec: define export', '0.2.2'],
['chore(deps): update dependencies', '0.2.3'],
['fix!: remove legacy contract', '1.0.0'],
['feat: change export\n\nBREAKING CHANGE: replace export format', '2.0.0'],
]) {
commit(message);
mustPass(prepare());
const pkg = JSON.parse(read('package.json'));
const lock = JSON.parse(read('package-lock.json'));
assert.equal(pkg.version, expected);
assert.equal(lock.version, expected);
assert.equal(lock.packages[''].version, expected);
assert.ok(read('CHANGELOG.md').includes(expected));
assert.equal(git('status', '--porcelain'), '');
git('tag', '-a', `v${expected}`, '-m', `Release ${expected}`);
}
assert.ok(read('CHANGELOG.md').includes('/commit/'), read('CHANGELOG.md'));
assert.ok(read('CHANGELOG.md').includes('/compare/v0.1.0...v0.1.1'));
assert.ok(!read('CHANGELOG.md').includes('chore(release)'));
assert.equal(read('CHANGELOG.md').split('correct calculation').length, 2, 'Prior changes must not be repeated');
assert.equal(spawnSync('git', ['--git-dir=remote.git', 'show-ref'], { cwd }).status, 1, 'Preparation must not push refs');
const lint = message => spawnSync(process.execPath,
[path.join(source, 'node_modules/@commitlint/cli/cli.js'), '--strict'],
{ cwd, env, input: message, encoding: 'utf8' });
mustPass(lint('spec: define contract'));
mustFail(lint('unstructured message'));
console.log(`PASS: release preparation, guards, version rules, lockfile and changelog (${source})`);
} finally {
fs.rmSync(cwd, { recursive: true, force: true });
}