11#!/usr/bin/env ts-node-script
22
3- import { exec } from "../src/shell" ;
43import { Version } from "../src" ;
5- import { parse as parseWidget } from "../src/changelog-parser/parser/widget/widget" ;
64import { 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
88interface 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+
80124async 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 }
0 commit comments