feat: track frontend changelog consumption (#5218)
changelog-filename-gate / validate (pull_request) Successful in 2s
changelog-filename-gate / validate (pull_request) Successful in 2s
这个提交包含在:
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
parseFrontmatter,
|
||||
runFrontmatterValidation,
|
||||
validateFrontendState,
|
||||
validateFrontendTransition,
|
||||
validateV2Document,
|
||||
} from '../scripts/validate-changelog-frontmatter.mjs';
|
||||
|
||||
|
||||
const FILE = 'changelogs-v2/2026-07/24_5218_工作流治理-修改接口-管理后台.md';
|
||||
|
||||
function metadata(overrides = {}) {
|
||||
return {
|
||||
schema: 'hl-changelog/v2',
|
||||
ticket: '5218',
|
||||
title: '工作流治理',
|
||||
consumer: 'admin',
|
||||
change_type: '修改接口',
|
||||
backend_status: 'deployed',
|
||||
gateway_status: 'verified',
|
||||
frontend_status: 'pending',
|
||||
frontend_owner: '',
|
||||
frontend_ref: '',
|
||||
target_release: '',
|
||||
verified_at: '',
|
||||
status_note: '',
|
||||
updated_at: '2026-07-24',
|
||||
base: 'dev-v3',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function document(overrides = {}) {
|
||||
const fields = metadata(overrides);
|
||||
const frontmatter = Object.entries(fields)
|
||||
.map(([key, value]) => `${key}: "${value}"`)
|
||||
.join('\n');
|
||||
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- 无业务接口变化。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
|
||||
}
|
||||
|
||||
test('parses quoted flat YAML frontmatter', () => {
|
||||
const parsed = parseFrontmatter(document());
|
||||
assert.equal(parsed.metadata.schema, 'hl-changelog/v2');
|
||||
assert.equal(parsed.metadata.frontend_status, 'pending');
|
||||
});
|
||||
|
||||
test('accepts a complete v2 handoff with pending frontend consumption', () => {
|
||||
assert.deepEqual(validateV2Document(FILE, document(), { requireV2: true }), []);
|
||||
});
|
||||
|
||||
test('rejects backend and gateway pending at publication', () => {
|
||||
const errors = validateV2Document(
|
||||
FILE,
|
||||
document({ backend_status: 'pending', gateway_status: 'pending' }),
|
||||
{ requireV2: true },
|
||||
);
|
||||
assert.ok(errors.some(({ code }) => code === 'E_BACKEND_PENDING'));
|
||||
assert.ok(errors.some(({ code }) => code === 'E_GATEWAY_PENDING'));
|
||||
});
|
||||
|
||||
test('requires frontend evidence as status advances', () => {
|
||||
assert.deepEqual(
|
||||
validateFrontendState(metadata({ frontend_status: 'claimed' })),
|
||||
['claimed 必须填写 frontend_owner'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
validateFrontendState(metadata({
|
||||
frontend_status: 'implemented',
|
||||
frontend_owner: 'frontend-team',
|
||||
frontend_ref: 'mmg/hl-ui@abc1234',
|
||||
})),
|
||||
[],
|
||||
);
|
||||
assert.ok(
|
||||
validateFrontendState(metadata({
|
||||
frontend_status: 'verified',
|
||||
frontend_owner: 'frontend-team',
|
||||
frontend_ref: 'mmg/hl-ui@abc1234',
|
||||
target_release: 'prod-2026.07.24',
|
||||
verified_at: '2026-02-30',
|
||||
})).includes('verified_at 必须是 ISO 日期或时间'),
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects skipped transitions and requires a rollback reason', () => {
|
||||
assert.ok(validateFrontendTransition('pending', 'implemented').length > 0);
|
||||
assert.deepEqual(validateFrontendTransition('pending', 'claimed'), []);
|
||||
assert.ok(validateFrontendTransition('released', 'implemented').length > 0);
|
||||
assert.deepEqual(
|
||||
validateFrontendTransition('released', 'implemented', '测试发布已回滚'),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test('new changelog requires v2 while a modified legacy file remains compatible', () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'hl-frontmatter-'));
|
||||
try {
|
||||
const file = path.join(root, ...FILE.split('/'));
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
writeFileSync(file, '# legacy\n');
|
||||
const newResult = runFrontmatterValidation([{ status: 'A', targetPath: FILE }], root);
|
||||
assert.ok(newResult.errors.some(({ code }) => code === 'E_FRONTMATTER'));
|
||||
const modifiedResult = runFrontmatterValidation([{ status: 'M', targetPath: FILE }], root);
|
||||
assert.deepEqual(modifiedResult.errors, []);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('detects metadata and filename mismatches', () => {
|
||||
const errors = validateV2Document(
|
||||
FILE,
|
||||
document({ ticket: '9999', change_type: '新增接口' }),
|
||||
{ requireV2: true },
|
||||
);
|
||||
assert.ok(errors.some(({ code }) => code === 'E_TICKET_MISMATCH'));
|
||||
assert.ok(errors.some(({ code }) => code === 'E_TYPE_MISMATCH'));
|
||||
});
|
||||
|
||||
test('rejects unsupported consumers and impossible dates', () => {
|
||||
const errors = validateV2Document(
|
||||
FILE,
|
||||
document({ consumer: 'browser', updated_at: '2026-02-30' }),
|
||||
{ requireV2: true },
|
||||
);
|
||||
assert.ok(errors.some(({ code }) => code === 'E_CONSUMER'));
|
||||
assert.ok(errors.some(({ code }) => code === 'E_UPDATED_AT'));
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户