@@ -7,6 +7,18 @@ import * as gulp from 'gulp';
77import * as fs from 'fs' ;
88import * as path from 'path' ;
99import * as os from 'os' ;
10+ import { exec } from 'child_process' ;
11+ import { promisify } from 'util' ;
12+ import { findTagsByVersion } from './gitTasks' ;
13+
14+ const execAsync = promisify ( exec ) ;
15+
16+ function logWarning ( message : string , error ?: unknown ) : void {
17+ console . log ( `##vso[task.logissue type=warning]${ message } ` ) ;
18+ if ( error instanceof Error && error . stack ) {
19+ console . log ( `##[debug]${ error . stack } ` ) ;
20+ }
21+ }
1022
1123gulp . task ( 'incrementVersion' , async ( ) : Promise < void > => {
1224 // Get the current version from version.json
@@ -64,3 +76,113 @@ gulp.task('incrementVersion', async (): Promise<void> => {
6476 changelogLines . splice ( lineToInsertAt , 0 , ...linesToInsert ) ;
6577 fs . writeFileSync ( changelogPath , changelogLines . join ( os . EOL ) ) ;
6678} ) ;
79+
80+ gulp . task ( 'updateChangelog' , async ( ) : Promise < void > => {
81+ // Add a new changelog section for the new version.
82+ console . log ( 'Determining version from CHANGELOG' ) ;
83+
84+ const changelogPath = path . join ( path . resolve ( __dirname , '..' ) , 'CHANGELOG.md' ) ;
85+ const changelogContent = fs . readFileSync ( changelogPath , 'utf8' ) ;
86+ const changelogLines = changelogContent . split ( os . EOL ) ;
87+
88+ // Find all the headers in the changelog (and their line numbers)
89+ const [ currentHeaderLine , currentVersion ] = findNextVersionHeaderLine ( changelogLines ) ;
90+ if ( currentHeaderLine === - 1 ) {
91+ throw new Error ( 'Could not find the current header in the CHANGELOG' ) ;
92+ }
93+
94+ console . log ( `Adding PRs for ${ currentVersion } to CHANGELOG` ) ;
95+
96+ const [ previousHeaderLine , previousVersion ] = findNextVersionHeaderLine ( changelogLines , currentHeaderLine + 1 ) ;
97+ if ( previousHeaderLine === - 1 ) {
98+ throw new Error ( 'Could not find the previous header in the CHANGELOG' ) ;
99+ }
100+
101+ const presentPrIds = getPrIdsBetweenHeaders ( changelogLines , currentHeaderLine , previousHeaderLine ) ;
102+ console . log ( `PRs [#${ presentPrIds . join ( ', #' ) } ] already in the CHANGELOG` ) ;
103+
104+ const versionTags = await findTagsByVersion ( previousVersion ! ) ;
105+ if ( versionTags . length === 0 ) {
106+ throw new Error ( `Could not find any tags for version ${ previousVersion } ` ) ;
107+ }
108+
109+ // The last tag is the most recent one created.
110+ const versionTag = versionTags . pop ( ) ;
111+ console . log ( `Using tag ${ versionTag } for previous version ${ previousVersion } ` ) ;
112+
113+ console . log ( `Generating PR list from ${ versionTag } to HEAD` ) ;
114+ const currentPrs = await generatePRList ( versionTag ! , 'HEAD' ) ;
115+
116+ const newPrs = [ ] ;
117+ for ( const pr of currentPrs ) {
118+ const match = prRegex . exec ( pr ) ;
119+ if ( ! match ) {
120+ continue ;
121+ }
122+
123+ const prId = match [ 1 ] ;
124+ if ( presentPrIds . includes ( prId ) ) {
125+ console . log ( `PR #${ prId } is already present in the CHANGELOG` ) ;
126+ continue ;
127+ }
128+
129+ console . log ( `Adding new PR to CHANGELOG: ${ pr } ` ) ;
130+ newPrs . push ( pr ) ;
131+ }
132+
133+ if ( newPrs . length === 0 ) {
134+ console . log ( 'No new PRs to add to the CHANGELOG' ) ;
135+ return ;
136+ }
137+
138+ console . log ( `Writing ${ newPrs . length } new PRs to the CHANGELOG` ) ;
139+
140+ changelogLines . splice ( currentHeaderLine + 1 , 0 , ...newPrs ) ;
141+ fs . writeFileSync ( changelogPath , changelogLines . join ( os . EOL ) ) ;
142+ } ) ;
143+
144+ const prRegex = / ^ \* .+ \( P R : \[ # ( \d + ) \] \( / g;
145+
146+ function findNextVersionHeaderLine ( changelogLines : string [ ] , startLine : number = 0 ) : [ number , string ] {
147+ const headerRegex = / ^ # \s ( \d + \. \d + ) \. ( x | \d + ) $ / gm;
148+ for ( let i = startLine ; i < changelogLines . length ; i ++ ) {
149+ const line = changelogLines . at ( i ) ;
150+ const match = headerRegex . exec ( line ! ) ;
151+ if ( match ) {
152+ return [ i , match [ 1 ] ] ;
153+ }
154+ }
155+ return [ - 1 , '' ] ;
156+ }
157+
158+ function getPrIdsBetweenHeaders ( changelogLines : string [ ] , startLine : number , endLine : number ) : string [ ] {
159+ const prs : string [ ] = [ ] ;
160+ for ( let i = startLine ; i < endLine ; i ++ ) {
161+ const line = changelogLines . at ( i ) ;
162+ const match = prRegex . exec ( line ! ) ;
163+ if ( match && match [ 1 ] ) {
164+ prs . push ( match [ 1 ] ) ;
165+ }
166+ }
167+ return prs ;
168+ }
169+
170+ async function generatePRList ( startSHA : string , endSHA : string ) : Promise < string [ ] > {
171+ try {
172+ console . log ( `Executing: roslyn-tools pr-finder -s "${ startSHA } " -e "${ endSHA } " --format o#` ) ;
173+ let { stdout } = await execAsync (
174+ `roslyn-tools pr-finder -s "${ startSHA } " -e "${ endSHA } " --format o#` ,
175+ { maxBuffer : 10 * 1024 * 1024 } // 10MB buffer
176+ ) ;
177+
178+ stdout = stdout . trim ( ) ;
179+ if ( stdout . length === 0 ) {
180+ return [ ] ;
181+ }
182+
183+ return stdout . split ( os . EOL ) . filter ( ( pr ) => pr . length > 0 ) ;
184+ } catch ( error ) {
185+ logWarning ( `PR finder failed: ${ error instanceof Error ? error . message : error } ` , error ) ;
186+ throw error ;
187+ }
188+ }
0 commit comments