feat(CI): CHANGELOG.mdの追記個所をチェックするCIを追加 (#12963)
* feat(CI): CHANGELOG.mdの追記個所をチェックするCIを追加 * fix * remove strategy * fix * fix
This commit is contained in:
87
scripts/changelog-checker/src/checker.ts
Normal file
87
scripts/changelog-checker/src/checker.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Release } from './parser.js';
|
||||
|
||||
export class Result {
|
||||
public readonly success: boolean;
|
||||
public readonly message?: string;
|
||||
|
||||
private constructor(success: boolean, message?: string) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static ofSuccess(): Result {
|
||||
return new Result(true);
|
||||
}
|
||||
|
||||
static ofFailed(message?: string): Result {
|
||||
return new Result(false, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* develop -> masterまたはrelease -> masterを想定したパターン。
|
||||
* base側の先頭とhead側で追加された分のリリースより1つ前のバージョンが等価であるかチェックする。
|
||||
*/
|
||||
export function checkNewRelease(base: Release[], head: Release[]): Result {
|
||||
const releaseCountDiff = head.length - base.length;
|
||||
if (releaseCountDiff <= 0) {
|
||||
return Result.ofFailed('Invalid release count.');
|
||||
}
|
||||
|
||||
const baseLatest = base[0];
|
||||
const headPrevious = head[releaseCountDiff];
|
||||
|
||||
if (baseLatest.releaseName !== headPrevious.releaseName) {
|
||||
return Result.ofFailed('Contains unexpected releases.');
|
||||
}
|
||||
|
||||
return Result.ofSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* topic -> developまたはtopic -> masterを想定したパターン。
|
||||
* head側の最新リリース配下に書き加えられているかをチェックする。
|
||||
*/
|
||||
export function checkNewTopic(base: Release[], head: Release[]): Result {
|
||||
if (head.length !== base.length) {
|
||||
return Result.ofFailed('Invalid release count.');
|
||||
}
|
||||
|
||||
const headLatest = head[0];
|
||||
for (let relIdx = 0; relIdx < base.length; relIdx++) {
|
||||
const baseItem = base[relIdx];
|
||||
const headItem = head[relIdx];
|
||||
if (baseItem.releaseName !== headItem.releaseName) {
|
||||
// リリースの順番が変わってると成立しないのでエラーにする
|
||||
return Result.ofFailed(`Release is different. base:${baseItem.releaseName}, head:${headItem.releaseName}`);
|
||||
}
|
||||
|
||||
if (baseItem.categories.length !== headItem.categories.length) {
|
||||
// カテゴリごと書き加えられたパターン
|
||||
if (headLatest.releaseName !== headItem.releaseName) {
|
||||
// 最新リリース以外に追記されていた場合
|
||||
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
|
||||
}
|
||||
} else {
|
||||
// カテゴリ数の変動はないのでリスト項目の数をチェック
|
||||
for (let catIdx = 0; catIdx < baseItem.categories.length; catIdx++) {
|
||||
const baseCategory = baseItem.categories[catIdx];
|
||||
const headCategory = headItem.categories[catIdx];
|
||||
|
||||
if (baseCategory.categoryName !== headCategory.categoryName) {
|
||||
// カテゴリの順番が変わっていると成立しないのでエラーにする
|
||||
return Result.ofFailed(`Category is different. base:${baseCategory.categoryName}, head:${headCategory.categoryName}`);
|
||||
}
|
||||
|
||||
if (baseCategory.items.length !== headCategory.items.length) {
|
||||
if (headLatest.releaseName !== headItem.releaseName) {
|
||||
// 最新リリース以外に追記されていた場合
|
||||
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ofSuccess();
|
||||
}
|
33
scripts/changelog-checker/src/index.ts
Normal file
33
scripts/changelog-checker/src/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as process from 'process';
|
||||
import * as fs from 'fs';
|
||||
import { parseChangeLog } from './parser.js';
|
||||
import { checkNewRelease, checkNewTopic } from './checker.js';
|
||||
|
||||
function abort(message?: string) {
|
||||
if (message) {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync('./CHANGELOG-base.md') || !fs.existsSync('./CHANGELOG-head.md')) {
|
||||
console.error('CHANGELOG-base.md or CHANGELOG-head.md is missing.');
|
||||
return;
|
||||
}
|
||||
|
||||
const base = parseChangeLog('./CHANGELOG-base.md');
|
||||
const head = parseChangeLog('./CHANGELOG-head.md');
|
||||
|
||||
const result = (base.length < head.length)
|
||||
? checkNewRelease(base, head)
|
||||
: checkNewTopic(base, head);
|
||||
|
||||
if (!result.success) {
|
||||
abort(result.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
62
scripts/changelog-checker/src/parser.ts
Normal file
62
scripts/changelog-checker/src/parser.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { unified } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { Heading, List, Node } from 'mdast';
|
||||
import { toString } from 'mdast-util-to-string';
|
||||
|
||||
export class Release {
|
||||
public readonly releaseName: string;
|
||||
public readonly categories: ReleaseCategory[];
|
||||
|
||||
constructor(releaseName: string, categories: ReleaseCategory[] = []) {
|
||||
this.releaseName = releaseName;
|
||||
this.categories = [...categories];
|
||||
}
|
||||
}
|
||||
|
||||
export class ReleaseCategory {
|
||||
public readonly categoryName: string;
|
||||
public readonly items: string[];
|
||||
|
||||
constructor(categoryName: string, items: string[] = []) {
|
||||
this.categoryName = categoryName;
|
||||
this.items = [...items];
|
||||
}
|
||||
}
|
||||
|
||||
function isHeading(node: Node): node is Heading {
|
||||
return node.type === 'heading';
|
||||
}
|
||||
|
||||
function isList(node: Node): node is List {
|
||||
return node.type === 'list';
|
||||
}
|
||||
|
||||
export function parseChangeLog(path: string): Release[] {
|
||||
const input = fs.readFileSync(path, { encoding: 'utf8' });
|
||||
const processor = unified().use(remarkParse);
|
||||
|
||||
const releases: Release[] = [];
|
||||
const root = processor.parse(input);
|
||||
|
||||
let release: Release | null = null;
|
||||
let category: ReleaseCategory | null = null;
|
||||
for (const it of root.children) {
|
||||
if (isHeading(it) && it.depth === 2) {
|
||||
// リリース
|
||||
release = new Release(toString(it));
|
||||
releases.push(release);
|
||||
} else if (isHeading(it) && it.depth === 3 && release) {
|
||||
// リリース配下のカテゴリ
|
||||
category = new ReleaseCategory(toString(it));
|
||||
release.categories.push(category);
|
||||
} else if (isList(it) && category) {
|
||||
for (const listItem of it.children) {
|
||||
// カテゴリ配下のリスト項目
|
||||
category.items.push(toString(listItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
Reference in New Issue
Block a user