fix(changelog): restore stable path aliases for #5252
changelog-filename-gate / validate (pull_request) Successful in 2s

这个提交包含在:
wx
2026-07-26 15:02:13 +08:00
父节点 e35ff35c58
当前提交 5c7fa5dbe6
共修改 11 个文件,包含 675 行新增和 15 行删除
+62 -3
查看文件
@@ -9,6 +9,7 @@ const CONTROLLED_ROOTS = new Map([
['changelogs-v2-mp', '小程序端'],
]);
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
function ruleError(code, path, message, expected) {
return { code, path, message, expected };
@@ -76,6 +77,40 @@ export function collectNewTargetPaths(records) {
.map(({ targetPath }) => targetPath);
}
export function pathAliasMapFromManifest(value) {
if (value?.schema !== 'hl-changelog-path-aliases/v1' || !Array.isArray(value.aliases)) {
throw new Error('changelog-path-aliases.json schema 或 aliases 非法');
}
const aliases = new Map();
for (const [index, entry] of value.aliases.entries()) {
if (
!entry
|| typeof entry.alias !== 'string'
|| typeof entry.canonical !== 'string'
|| entry.alias.length === 0
|| entry.canonical.length === 0
) {
throw new Error(`changelog-path-aliases.json aliases[${index}] 缺少 alias/canonical`);
}
if (aliases.has(entry.alias)) {
throw new Error(`changelog-path-aliases.json alias 重复: ${entry.alias}`);
}
aliases.set(entry.alias, entry.canonical);
}
return aliases;
}
export function loadPathAliasMap(root = process.cwd()) {
try {
return pathAliasMapFromManifest(JSON.parse(readFileSync(`${root}/${PATH_ALIAS_MANIFEST}`, 'utf8')));
} catch (error) {
if (error?.code === 'ENOENT') {
return new Map();
}
throw error;
}
}
export function controlledRootForPath(inputPath) {
const candidate = String(inputPath);
for (const root of CONTROLLED_ROOTS.keys()) {
@@ -178,10 +213,32 @@ export function validateChangelogPath(inputPath, now = new Date()) {
return errors;
}
export function runValidation(records, now = new Date()) {
export function runValidation(records, now = new Date(), { pathAliases = new Map() } = {}) {
const targetPaths = collectNewTargetPaths(records);
const controlledPaths = targetPaths.filter((targetPath) => controlledRootForPath(targetPath));
const errors = controlledPaths.flatMap((targetPath) => validateChangelogPath(targetPath, now));
const errors = controlledPaths.flatMap((targetPath) => (
pathAliases.has(targetPath) ? [] : validateChangelogPath(targetPath, now)
));
for (const record of records) {
const sourcePath = /^R\d{1,3}$/.test(record.status)
? record.sourcePath
: record.status === 'D' ? record.targetPath : undefined;
if (!sourcePath || !controlledRootForPath(sourcePath) || !sourcePath.endsWith('.md')) {
continue;
}
const expectedTarget = pathAliases.get(sourcePath);
const actualTarget = /^R\d{1,3}$/.test(record.status) ? record.targetPath : undefined;
if (!expectedTarget || (actualTarget && expectedTarget !== actualTarget)) {
errors.push(ruleError(
'E_PATH_STABILITY',
sourcePath,
'已发布 changelog 不得在缺少精确 alias 关系时删除或重命名',
actualTarget
? `在 ${PATH_ALIAS_MANIFEST} 登记 ${sourcePath} -> ${actualTarget} 并保留兼容入口`
: `在 ${PATH_ALIAS_MANIFEST} 登记迁移关系并保留兼容入口`,
));
}
}
return {
targetCount: targetPaths.length,
checkedCount: controlledPaths.length,
@@ -294,7 +351,9 @@ export function main(argv = process.argv.slice(2)) {
try {
const options = parseArguments(argv);
const records = parseNameStatusZ(diffFromOptions(options));
const result = runValidation(records, new Date());
const result = runValidation(records, new Date(), {
pathAliases: loadPathAliasMap(),
});
if (result.errors.length > 0) {
for (const error of result.errors) {
console.error(`[${error.code}] ${error.path}: ${error.message}; expected ${error.expected}`);