Files
appium/scripts/verify-changelog-generation.mjs
T
2026-08-24 22:47:13 +02:00

145 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
// Regression guard: lerna, its conventional-changelog dependency, and the changelog preset
// package can drift out of sync (e.g. a preset transform that mutates commits, fed to a writer
// that freezes them) and only fail during an actual release. This replays lerna's own preset
// resolution + legacy-config normalization (parserOpts/writerOpts -> parser/writer, replicated
// below since lerna doesn't expose it publicly) and a real changelog write, failing if it throws.
//
// If lerna changes its preset normalization, re-sync from its bundle ("normalizePresetConfig").
import {readFile} from 'node:fs/promises';
import path from 'node:path';
import {pathToFileURL} from 'node:url';
import {ConventionalChangelog} from 'conventional-changelog';
import {loadPreset} from 'conventional-changelog-preset-loader';
// Ported from lerna, which hardcodes Handlebars for legacy string templates - not an assumption
// this script adds on top. Not a devDependency of its own: lerna depends on it directly, so it's
// always present alongside lerna itself; `main()` fails loudly if that ever stops being true.
function normalizeLegacyWriterOptions(writer, Handlebars) {
if (!writer || typeof writer !== 'object') {
return writer;
}
const normalized = {...writer};
const legacyMainTemplate =
typeof writer.mainTemplate === 'string' ? Handlebars.compile(writer.mainTemplate) : undefined;
const legacyHeaderPartial =
typeof writer.headerPartial === 'string' ? Handlebars.compile(writer.headerPartial) : undefined;
const legacyCommitPartial =
typeof writer.commitPartial === 'string' ? Handlebars.compile(writer.commitPartial) : undefined;
const legacyFooterPartial =
typeof writer.footerPartial === 'string' ? Handlebars.compile(writer.footerPartial) : undefined;
if (legacyMainTemplate) {
normalized.template = (context) =>
legacyMainTemplate(context, {
data: {root: context},
partials: {
header:
legacyHeaderPartial ||
((partialContext) => (writer.headerPartial || context.headerPartial)(partialContext)),
commit:
legacyCommitPartial ||
((commit, options) => (writer.commitPartial || context.commitPartial)(options.data.root, commit)),
footer:
legacyFooterPartial ||
((partialContext) => (writer.footerPartial || context.footerPartial)(partialContext)),
},
});
delete normalized.mainTemplate;
delete normalized.headerPartial;
delete normalized.commitPartial;
delete normalized.footerPartial;
} else {
if (legacyHeaderPartial) {
normalized.headerPartial = (context) => legacyHeaderPartial(context);
}
if (legacyCommitPartial) {
normalized.commitPartial = (context, commit) => legacyCommitPartial(commit, {data: {root: context}});
}
if (legacyFooterPartial) {
normalized.footerPartial = (context) => legacyFooterPartial(context);
}
}
return normalized;
}
function normalizePresetConfig(config, Handlebars) {
if (
config &&
(config.parser || config.writer || config.whatBump) &&
!config.parserOpts &&
!config.writerOpts &&
!config.conventionalChangelog
) {
return {...config, writer: normalizeLegacyWriterOptions(config.writer, Handlebars)};
}
if (
config &&
(config.parserOpts ||
config.writerOpts ||
config.conventionalChangelog ||
config.recommendedBumpOpts ||
config.gitRawCommitsOpts)
) {
const normalized = {...config};
const cc = config.conventionalChangelog || config;
normalized.parser ||= cc.parserOpts || config.parserOpts;
normalized.writer ||= cc.writerOpts || config.writerOpts;
normalized.writer = normalizeLegacyWriterOptions(normalized.writer, Handlebars);
normalized.commits ||= cc.gitRawCommitsOpts || config.gitRawCommitsOpts;
normalized.whatBump ||= config.recommendedBumpOpts?.whatBump || config.whatBump;
return normalized;
}
return config;
}
async function main() {
let Handlebars;
try {
({default: Handlebars} = await import('handlebars'));
} catch (err) {
console.error(
'Could not resolve "handlebars". This script relies on it being installed alongside ' +
'lerna (which depends on it directly) rather than declaring its own dependency - if ' +
'lerna stopped depending on it, add "handlebars" back as a devDependency here.',
);
console.error(err);
process.exitCode = 1;
return;
}
const rootDir = path.resolve(import.meta.dirname, '..');
const lernaConfig = JSON.parse(await readFile(path.join(rootDir, 'lerna.json'), 'utf8'));
const preset = lernaConfig.changelogPreset ?? 'conventional-changelog-angular';
const config = normalizePresetConfig(await loadPreset(preset), Handlebars);
const generator = new ConventionalChangelog(rootDir);
generator.config(config);
generator.context({version: '0.0.0-verify'});
try {
// draining the stream is enough to exercise commit parsing and template rendering
for await (const chunk of generator.writeStream()) {
void chunk;
}
} catch (err) {
console.error(`Changelog generation with preset "${preset}" (as configured in lerna.json) threw an error:\n`);
console.error(err);
console.error(
'\nThis usually means the installed versions of "lerna", its "conventional-changelog" ' +
'dependency, and the changelog preset package are incompatible with each other.',
);
process.exitCode = 1;
return;
}
console.log(`OK: changelog generation with preset "${preset}" completed without error.`);
}
// Check if this module is being run directly
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}