Skip to content

Commit e769b71

Browse files
authored
feat: better changelogs (#2284)
2 parents 1689f57 + d105cae commit e769b71

16 files changed

Lines changed: 101 additions & 27 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Monorepo of official Mendix pluggable web widgets. pnpm workspaces + Turbo.
1010
- Render loading/empty states until values are ready
1111
- SCSS for styling, prefer Atlas UI classes, BEM-like naming with widget prefix
1212
- Conventional commits enforced: `type(scope): description`
13-
- Semver + CHANGELOG.md per package for changes
13+
- Semver + CHANGELOG.md per package for pluggable widgets and modules, not for shared packages
1414
- Changelogs for users: include only widget behavior, not implementation details
1515
- Changelogs added during development, version bumps only at release time
1616
- Jest + RTL for unit tests (src/\*_/**tests**/_.spec.ts)

automation/utils/bin/rui-check-changelogs.ts

Lines changed: 85 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env ts-node-script
22

3-
import { exec } from "../src/shell";
43
import { Version } from "../src";
5-
import { parse as parseWidget } from "../src/changelog-parser/parser/widget/widget";
64
import { parse as parseModule } from "../src/changelog-parser/parser/module/module";
5+
import { parse as parseWidget } from "../src/changelog-parser/parser/widget/widget";
6+
import { exec } from "../src/shell";
77

88
interface ChangelogChange {
99
filePath: string;
@@ -62,21 +62,65 @@ function compareReleasedVersions(oldReleased: any[], newReleased: any[]): boolea
6262
return JSON.stringify(oldReleased) === JSON.stringify(newReleased);
6363
}
6464

65-
async function getChangedFiles(base: string, head: string): Promise<string[]> {
66-
const result = await exec(`git diff --name-only ${base}...${head}`, { stdio: "pipe" });
65+
async function getChangedFiles(headSha: string, mergedTreeSha: string): Promise<string[]> {
66+
const result = await exec(`git diff --name-only ${headSha} ${mergedTreeSha}`, { stdio: "pipe" });
6767
return result.stdout.trim().split("\n").filter(Boolean);
6868
}
6969

70-
async function getFileContent(filePath: string, commitSha: string): Promise<string | null> {
70+
/**
71+
* Simulate merging HEAD into BASE using `git merge-tree --write-tree` and return
72+
* the SHA of the resulting tree.
73+
*/
74+
async function getMergedTreeSha(baseSha: string, headSha: string): Promise<string | null> {
75+
let stdout = "";
76+
let hasConflicts = false;
77+
7178
try {
72-
const result = await exec(`git show ${commitSha}:${filePath}`, { stdio: "pipe" });
79+
const result = await exec(`git merge-tree --write-tree ${baseSha} ${headSha}`, { stdio: "pipe" });
80+
stdout = result.stdout;
81+
} catch (error) {
82+
// execa throws on non-zero exit. git merge-tree exits 1 when there are
83+
// conflicts but still prints the tree SHA to stdout.
84+
const execaError = error as { exitCode?: number; stdout?: string };
85+
if (execaError.exitCode === 1 && execaError.stdout) {
86+
stdout = execaError.stdout;
87+
hasConflicts = true;
88+
} else {
89+
console.error(` ❌ Failed to simulate merge: ${error instanceof Error ? error.message : String(error)}`);
90+
return null;
91+
}
92+
}
93+
94+
const treeSha = stdout.trim().split("\n")[0].trim();
95+
if (!treeSha) {
96+
console.error(` ❌ git merge-tree did not return a tree SHA`);
97+
return null;
98+
}
99+
100+
if (hasConflicts) {
101+
console.warn(` ⚠️ Merge simulation has conflicts – some files will contain conflict markers`);
102+
}
103+
104+
return treeSha;
105+
}
106+
107+
/**
108+
* Read a file from a tree SHA.
109+
*/
110+
async function getFileContentFromTree(treeSha: string, filePath: string): Promise<string | null> {
111+
try {
112+
const result = await exec(`git show ${treeSha}:${filePath}`, { stdio: "pipe" });
73113
return result.stdout;
74114
} catch (_error) {
75-
// File might not exist at this commit (newly added or deleted)
115+
// File not present in the tree
76116
return null;
77117
}
78118
}
79119

120+
function hasConflictMarkers(content: string): boolean {
121+
return content.includes("<<<<<<<") || content.includes(">>>>>>>");
122+
}
123+
80124
async function main(): Promise<void> {
81125
const base = process.env.BASE_SHA; // main
82126
const head = process.env.HEAD_SHA; // fix/blah-blah-blah
@@ -87,14 +131,21 @@ async function main(): Promise<void> {
87131

88132
console.log(`Checking CHANGELOG.md files between ${base} and ${head}...`);
89133

90-
// Get list of all changed files
91-
const changedFiles = await getChangedFiles(base, head);
92-
console.log(`Found ${changedFiles.length} changed file(s)`);
134+
// Simulate the merge first so we can find files actually modified by it.
135+
console.log(`\nSimulating merge of HEAD (${head}) into BASE (${base})...`);
136+
const mergedTreeSha = await getMergedTreeSha(base, head);
137+
if (!mergedTreeSha) {
138+
throw new Error("Cannot proceed: failed to compute merged tree. Check the error above.");
139+
}
140+
console.log(`Merged tree SHA: ${mergedTreeSha}`);
93141

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-
});
142+
// Diff HEAD against the merged tree: only files where both sides had changes
143+
// (requiring a 3-way merge) will appear here. Files exclusively changed in
144+
// the PR branch or exclusively in BASE are not included.
145+
const mergeChangedFiles = await getChangedFiles(head, mergedTreeSha);
146+
console.log(`\nFound ${mergeChangedFiles.length} file(s) modified by the merge`);
147+
148+
const changelogFiles = mergeChangedFiles.filter(file => file.endsWith("CHANGELOG.md"));
98149

99150
if (changelogFiles.length === 0) {
100151
console.log("No CHANGELOG.md files were changed.");
@@ -113,34 +164,43 @@ async function main(): Promise<void> {
113164
for (const filePath of changelogFiles) {
114165
console.log(`\nProcessing ${filePath}...`);
115166

116-
// Get old content (from base commit)
117-
const oldContent = await getFileContent(filePath, base);
167+
// Old content: what the file looks like on BASE (what is already in main)
168+
const oldContent = await getFileContentFromTree(base, filePath);
118169

119-
// Get new content (from head commit)
120-
const newContent = await getFileContent(filePath, head);
170+
// New content: what the file would look like *after* merging HEAD into BASE.
171+
// We read from the simulated merged tree rather than from HEAD directly so
172+
// that semantic merge conflicts (lines mangled or dropped by the 3-way merge)
173+
// are caught here, not silently accepted.
174+
const mergedContent = await getFileContentFromTree(mergedTreeSha, filePath);
121175

122-
if (!oldContent && !newContent) {
123-
console.log(` ⚠️ Warning: File not found in both commits, skipping`);
176+
if (!oldContent && !mergedContent) {
177+
console.log(` ⚠️ Warning: File not found in either BASE or merged tree, skipping`);
124178
continue;
125179
}
126180

127181
if (!oldContent) {
128-
console.log(` ℹ️ New file added (no comparison needed)`);
182+
console.log(` ℹ️ New file added in HEAD (no comparison needed)`);
183+
continue;
184+
}
185+
186+
if (!mergedContent) {
187+
console.log(` ℹ️ File will be deleted after merge (no comparison needed)`);
129188
continue;
130189
}
131190

132-
if (!newContent) {
133-
console.log(` ℹ️ File deleted (no comparison needed)`);
191+
if (hasConflictMarkers(mergedContent)) {
192+
console.error(` ❌ Merge conflict detected in ${filePath}! Resolve conflicts before merging.`);
193+
hasErrors = true;
134194
continue;
135195
}
136196

137197
// Determine changelog type
138198
const changelogType = getChangelogType(filePath);
139199

140200
if (changelogType === "module") {
141-
changes.push({ filePath, oldContent, newContent, type: "module" });
201+
changes.push({ filePath, oldContent, newContent: mergedContent, type: "module" });
142202
} else if (changelogType === "widget") {
143-
changes.push({ filePath, oldContent, newContent, type: "widget" });
203+
changes.push({ filePath, oldContent, newContent: mergedContent, type: "widget" });
144204
} else {
145205
console.log(` ⚠️ Warning: Unknown changelog type, skipping`);
146206
}

packages/shared/charts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "2.2.1",
44
"description": "Shared components for charts",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
6+
"license": "Apache-2.0",
67
"private": true,
78
"type": "module",
89
"files": [

packages/shared/filter-commons/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Common filter utilities and types for filter widgets",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
66
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

packages/shared/rollup-web-widgets/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Place to keep shared rollup configs.",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
66
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

packages/shared/tsconfig-web-widgets/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"name": "@mendix/tsconfig-web-widgets",
33
"version": "1.0.0",
44
"description": "Place to keep shared ts configs.",
5-
"license": "Apache-2.0",
65
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
6+
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

packages/shared/widget-plugin-component-kit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Common components for the pluggable widgets.",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
66
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

packages/shared/widget-plugin-dropdown-filter/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Drop-down filter widget plugin",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
66
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

packages/shared/widget-plugin-external-events/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Plugin for subscribing to and publishing widget events",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
66
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

packages/shared/widget-plugin-filtering/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Filtering API plugin.",
55
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
66
"license": "Apache-2.0",
7+
"private": true,
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/mendix/web-widgets.git"

0 commit comments

Comments
 (0)