// 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 }); }