Skip to content

Commit a8dc14c

Browse files
authored
feat: add check for changelog entries (#2055)
2 parents 18c1c12 + 2e45054 commit a8dc14c

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

.github/workflows/BuildJobs.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ jobs:
6363
run: pnpm run verify ${{ env.since_flag }}
6464
- name: Run unit tests
6565
run: pnpm run test ${{ env.since_flag }}
66+
- name: Verify changelog entries
67+
run: pnpm run -w check-changelogs
68+
if: >-
69+
${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'mendix/web-widgets' && github.event.pull_request.user.login != 'uicontent' }}
70+
env:
71+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
72+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
6673

6774
mxversion:
6875
name: Read versions file
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env ts-node-script
2+
3+
import { exec } from "../src/shell";
4+
import { Version } from "../src";
5+
import { parse as parseWidget } from "../src/changelog-parser/parser/widget/widget";
6+
import { parse as parseModule } from "../src/changelog-parser/parser/module/module";
7+
8+
interface ChangelogChange {
9+
filePath: string;
10+
oldContent: string;
11+
newContent: string;
12+
type: "module" | "widget";
13+
}
14+
15+
type ChangelogType = "module" | "widget" | "other";
16+
17+
function getChangelogType(filePath: string): ChangelogType {
18+
if (filePath.includes("packages/modules/")) {
19+
return "module";
20+
}
21+
if (filePath.includes("packages/pluggableWidgets/")) {
22+
return "widget";
23+
}
24+
return "other";
25+
}
26+
27+
function compareChangelogContent(change: ChangelogChange): boolean {
28+
try {
29+
const oldParsed =
30+
change.type === "module"
31+
? parseModule(change.oldContent, { moduleName: "tmp", Version })
32+
: parseWidget(change.oldContent, { Version });
33+
const newParsed =
34+
change.type === "module"
35+
? parseModule(change.newContent, { moduleName: "tmp", Version })
36+
: parseWidget(change.newContent, { Version });
37+
38+
const [, ...oldReleased] = oldParsed.content;
39+
const [newUnreleased, ...newReleased] = newParsed.content;
40+
41+
const releasedVersionsMatch = compareReleasedVersions(oldReleased, newReleased);
42+
43+
if (!releasedVersionsMatch) {
44+
console.error(` ❌ Released versions have been modified!`);
45+
return false;
46+
}
47+
48+
const sectionTypes = newUnreleased.sections.map(s => s.type);
49+
if (sectionTypes.length !== new Set(sectionTypes).size) {
50+
console.error(` ❌ There are duplicated changelog types in Unreleased!`);
51+
return false;
52+
}
53+
} catch (error) {
54+
console.error(` ❌ Failed to parse changelog: ${error instanceof Error ? error.message : String(error)}`);
55+
return false;
56+
}
57+
58+
return true;
59+
}
60+
61+
function compareReleasedVersions(oldReleased: any[], newReleased: any[]): boolean {
62+
return JSON.stringify(oldReleased) === JSON.stringify(newReleased);
63+
}
64+
65+
async function getChangedFiles(base: string, head: string): Promise<string[]> {
66+
const result = await exec(`git diff --name-only ${base}...${head}`, { stdio: "pipe" });
67+
return result.stdout.trim().split("\n").filter(Boolean);
68+
}
69+
70+
async function getFileContent(filePath: string, commitSha: string): Promise<string | null> {
71+
try {
72+
const result = await exec(`git show ${commitSha}:${filePath}`, { stdio: "pipe" });
73+
return result.stdout;
74+
} catch (_error) {
75+
// File might not exist at this commit (newly added or deleted)
76+
return null;
77+
}
78+
}
79+
80+
async function main(): Promise<void> {
81+
const base = process.env.BASE_SHA; // main
82+
const head = process.env.HEAD_SHA; // fix/blah-blah-blah
83+
84+
if (!base || !head) {
85+
throw new Error("BASE_SHA and HEAD_SHA environment variables must be set");
86+
}
87+
88+
console.log(`Checking CHANGELOG.md files between ${base} and ${head}...`);
89+
90+
// Get list of all changed files
91+
const changedFiles = await getChangedFiles(base, head);
92+
console.log(`Found ${changedFiles.length} changed file(s)`);
93+
94+
// Filter for CHANGELOG.md files in packages/modules or packages/pluggableWidgets
95+
const changelogFiles = changedFiles.filter(file => {
96+
return file.endsWith("CHANGELOG.md");
97+
});
98+
99+
if (changelogFiles.length === 0) {
100+
console.log("No CHANGELOG.md files were changed.");
101+
return;
102+
}
103+
104+
console.log(`Found ${changelogFiles.length} CHANGELOG.md file(s) to check:`);
105+
changelogFiles.forEach(file => {
106+
const type = getChangelogType(file);
107+
console.log(` - ${file} (${type})`);
108+
});
109+
110+
const changes: ChangelogChange[] = [];
111+
let hasErrors = false;
112+
113+
for (const filePath of changelogFiles) {
114+
console.log(`\nProcessing ${filePath}...`);
115+
116+
// Get old content (from base commit)
117+
const oldContent = await getFileContent(filePath, base);
118+
119+
// Get new content (from head commit)
120+
const newContent = await getFileContent(filePath, head);
121+
122+
if (!oldContent && !newContent) {
123+
console.log(` ⚠️ Warning: File not found in both commits, skipping`);
124+
continue;
125+
}
126+
127+
if (!oldContent) {
128+
console.log(` ℹ️ New file added (no comparison needed)`);
129+
continue;
130+
}
131+
132+
if (!newContent) {
133+
console.log(` ℹ️ File deleted (no comparison needed)`);
134+
continue;
135+
}
136+
137+
// Determine changelog type
138+
const changelogType = getChangelogType(filePath);
139+
140+
if (changelogType === "module") {
141+
changes.push({ filePath, oldContent, newContent, type: "module" });
142+
} else if (changelogType === "widget") {
143+
changes.push({ filePath, oldContent, newContent, type: "widget" });
144+
} else {
145+
console.log(` ⚠️ Warning: Unknown changelog type, skipping`);
146+
}
147+
}
148+
149+
for (const change of changes) {
150+
const isValid = compareChangelogContent(change);
151+
if (!isValid) {
152+
console.error(` ❌ Invalid changes detected in ${change.filePath}`);
153+
hasErrors = true;
154+
} else {
155+
console.log(` ✅ Valid changes`);
156+
}
157+
}
158+
159+
if (hasErrors) {
160+
console.error("\n❌ Some CHANGELOG.md files have invalid changes");
161+
process.exit(1);
162+
} else {
163+
console.log(`\n✅ All ${changes.length} CHANGELOG.md file(s) have valid changes`);
164+
}
165+
}
166+
167+
main().catch(e => {
168+
console.error(e);
169+
process.exit(1);
170+
});

automation/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"scripts": {
2828
"agent-rules": "ts-node bin/rui-agent-rules.ts",
2929
"changelog": "ts-node bin/rui-changelog-helper.ts",
30+
"check-changelogs": "ts-node bin/rui-check-changelogs.ts",
3031
"compile:parser:module": "peggy -o ./src/changelog-parser/parser/widget/widget.js ./src/changelog-parser/parser/widget/widget.pegjs",
3132
"compile:parser:widget": "peggy -o ./src/changelog-parser/parser/module/module.js ./src/changelog-parser/parser/module/module.pegjs",
3233
"format": "prettier --write .",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"scripts": {
1010
"build": "turbo run build",
1111
"changelog": "pnpm --filter @mendix/automation-utils run changelog",
12+
"check-changelogs": "pnpm --filter @mendix/automation-utils run check-changelogs",
1213
"create-gh-release": "turbo run create-gh-release --concurrency 1",
1314
"create-translation": "turbo run create-translation",
1415
"include-oss-in-artifact": "pnpm --filter @mendix/automation-utils run include-oss-in-artifact",

0 commit comments

Comments
 (0)