ci: fix conventional changelog immutable (#22668)

This commit is contained in:
Mykola Mokhnach
2026-08-24 22:47:13 +02:00
committed by GitHub
parent e916673e32
commit 2b39493e98
4 changed files with 157 additions and 22 deletions
+2
View File
@@ -197,3 +197,5 @@ jobs:
run: npm run lint:ci
- name: Format check
run: npm run format:check
- name: Verify changelog generation
run: npm run verify:changelog-generation
+9 -21
View File
@@ -36,7 +36,7 @@
"@types/ws": "8.18.1",
"@types/yargs": "17.0.35",
"asyncbox": "6.4.2",
"conventional-changelog-conventionalcommits": "7.0.2",
"conventional-changelog-conventionalcommits": "9.3.1",
"cpy-cli": "7.0.0",
"cross-env": "10.1.0",
"finalhandler": "2.1.1",
@@ -241,9 +241,9 @@
}
},
"node_modules/@conventional-changelog/template": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.2.1.tgz",
"integrity": "sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.4.0.tgz",
"integrity": "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8493,15 +8493,15 @@
}
},
"node_modules/conventional-changelog-conventionalcommits": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz",
"integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==",
"dev": true,
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz",
"integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==",
"license": "ISC",
"dependencies": {
"compare-func": "^2.0.0"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/conventional-changelog-preset-loader": {
@@ -23185,18 +23185,6 @@
"npm": ">=10"
}
},
"packages/semantic-release-config/node_modules/conventional-changelog-conventionalcommits": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz",
"integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==",
"license": "ISC",
"dependencies": {
"compare-func": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/storage-plugin": {
"name": "@appium/storage-plugin",
"version": "1.1.8",
+2 -1
View File
@@ -75,6 +75,7 @@
"test:types": "lerna run test:types",
"test:unit": "lerna run test",
"upload": "gulp github-upload",
"verify:changelog-generation": "node ./scripts/verify-changelog-generation.mjs",
"zip": "zip -qr ./appium.zip ./packages/appium",
"zip-and-upload": "run-s zip upload"
},
@@ -103,7 +104,7 @@
"@types/ws": "8.18.1",
"@types/yargs": "17.0.35",
"asyncbox": "6.4.2",
"conventional-changelog-conventionalcommits": "7.0.2",
"conventional-changelog-conventionalcommits": "9.3.1",
"cpy-cli": "7.0.0",
"cross-env": "10.1.0",
"finalhandler": "2.1.1",
+144
View File
@@ -0,0 +1,144 @@
#!/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();
}