@@ -5,38 +5,46 @@ const tl = require('azure-pipelines-task-lib/task');
5
5
const util = require ( './util' ) ;
6
6
7
7
const { Octokit } = require ( "@octokit/rest" ) ;
8
+ const { graphql } = require ( "@octokit/graphql" ) ;
9
+ const fetch = require ( 'node-fetch' ) ;
8
10
9
11
const OWNER = 'microsoft' ;
10
- const REPO = 'azure-pipelines-agent' ;
12
+ const REPO = 'azure-pipelines-agent' ;
11
13
const GIT = 'git' ;
12
14
const VALID_RELEASE_RE = / ^ [ 0 - 9 ] { 1 , 3 } \. [ 0 - 9 ] { 1 , 3 } \. [ 0 - 9 ] { 1 , 3 } $ / ;
13
15
const octokit = new Octokit ( { } ) ; // only read-only operations, no need to auth
14
16
17
+ const graphqlWithFetch = graphql . defaults ( { // Create a reusable GraphQL instance with fetch
18
+ request : {
19
+ fetch,
20
+ } ,
21
+ headers : {
22
+ authorization : process . env . PAT ? `token ${ process . env . PAT } ` : undefined ,
23
+ }
24
+ } ) ;
25
+
15
26
process . env . EDITOR = process . env . EDITOR === undefined ? 'code --wait' : process . env . EDITOR ;
16
27
17
28
var opt = require ( 'node-getopt' ) . create ( [
18
- [ '' , 'dryrun' , 'Dry run only, do not actually commit new release' ] ,
19
- [ '' , 'derivedFrom=version' , 'Used to get PRs merged since this release was created' , 'latest' ] ,
20
- [ '' , 'branch=branch' , 'Branch to select PRs merged into' , 'master' ] ,
21
- [ 'h' , 'help' , 'Display this help' ] ,
22
- ] )
23
- . setHelp (
24
- 'Usage: node createReleaseBranch.js [OPTION] <version>\n' +
25
- '\n' +
26
- '[[OPTIONS]]\n'
27
- )
28
- . bindHelp ( ) // bind option 'help' to default action
29
- . parseSystem ( ) ; // parse command line
30
-
31
- async function verifyNewReleaseTagOk ( newRelease )
32
- {
33
- if ( ! newRelease || ! newRelease . match ( VALID_RELEASE_RE ) || newRelease . endsWith ( '.999.999' ) )
34
- {
29
+ [ '' , 'dryrun' , 'Dry run only, do not actually commit new release' ] ,
30
+ [ '' , 'derivedFrom=version' , 'Used to get PRs merged since this release was created' , 'lastMinorRelease' ] ,
31
+ [ '' , 'branch=branch' , 'Branch to select PRs merged into' , 'master' ] ,
32
+ [ 'h' , 'help' , 'Display this help' ] ,
33
+ ] )
34
+ . setHelp (
35
+ 'Usage: node createReleaseBranch.js [OPTION] <version>\n' +
36
+ '\n' +
37
+ '[[OPTIONS]]\n'
38
+ )
39
+ . bindHelp ( ) // bind option 'help' to default action
40
+ . parseSystem ( ) ; // parse command line
41
+
42
+ async function verifyNewReleaseTagOk ( newRelease ) {
43
+ if ( ! newRelease || ! newRelease . match ( VALID_RELEASE_RE ) || newRelease . endsWith ( '.999.999' ) ) {
35
44
console . log ( `Invalid version '${ newRelease } '. Version must be in the form of <major>.<minor>.<patch> where each level is 0-999` ) ;
36
45
process . exit ( - 1 ) ;
37
46
}
38
- try
39
- {
47
+ try {
40
48
var tag = 'v' + newRelease ;
41
49
await octokit . repos . getReleaseByTag ( {
42
50
owner : OWNER ,
@@ -47,33 +55,129 @@ async function verifyNewReleaseTagOk(newRelease)
47
55
console . log ( `Version ${ newRelease } is already in use` ) ;
48
56
process . exit ( - 1 ) ;
49
57
}
50
- catch
51
- {
58
+ catch {
52
59
console . log ( `Version ${ newRelease } is available for use` ) ;
53
60
}
54
61
}
55
62
56
- function writeAgentVersionFile ( newRelease )
57
- {
63
+ function writeAgentVersionFile ( newRelease ) {
58
64
console . log ( 'Writing agent version file' )
59
- if ( ! opt . options . dryrun )
60
- {
65
+ if ( ! opt . options . dryrun ) {
61
66
fs . writeFileSync ( path . join ( __dirname , '..' , 'src' , 'agentversion' ) , `${ newRelease } \n` ) ;
62
67
}
63
68
return newRelease ;
64
69
}
65
70
66
- async function fetchPRsSinceLastReleaseAndEditReleaseNotes ( newRelease , callback )
67
- {
71
+ async function fetchPRsForSHAsGraphQL ( commitSHAs ) {
72
+
73
+ var queryParts = commitSHAs . map ( ( sha , index ) => `
74
+ commit${ index + 1 } : object(expression: "${ sha } ") { ... on Commit { associatedPullRequests(first: 1) {
75
+ edges { node { title number createdAt closedAt labels(first: 10) { edges { node { name } } } } } } } }` ) ;
76
+
77
+ var fullQuery = `
78
+ query ($repo: String!, $owner: String!) {
79
+ repository(name: $repo, owner: $owner) {
80
+ ${ queryParts . join ( '\n' ) }
81
+ }
82
+ }
83
+ ` ;
84
+
85
+ try {
86
+ var response = await graphqlWithFetch ( fullQuery , {
87
+ repo : REPO ,
88
+ owner : OWNER ,
89
+ } ) ;
90
+
91
+ var prs = [ ] ;
92
+ Object . keys ( response . repository ) . forEach ( commitKey => {
93
+ var commit = response . repository [ commitKey ] ;
94
+ if ( commit && commit . associatedPullRequests ) {
95
+ commit . associatedPullRequests . edges . forEach ( pr => {
96
+ prs . push ( {
97
+ title : pr . node . title ,
98
+ number : pr . node . number ,
99
+ createdAt : pr . node . createdAt ,
100
+ closedAt : pr . node . closedAt ,
101
+ labels : pr . node . labels . edges . map ( label => ( { name : label . node . name } ) ) , // Extract label names
102
+ } ) ;
103
+ } ) ;
104
+ }
105
+ } ) ;
106
+ return prs ;
107
+ } catch ( e ) {
108
+ console . log ( e ) ;
109
+ console . error ( `Error fetching PRs via GraphQL.` ) ;
110
+ process . exit ( - 1 ) ;
111
+ }
112
+ }
113
+
114
+ async function fetchPRsSincePreviousReleaseAndEditReleaseNotes ( newRelease , callback ) {
115
+ try {
116
+ var latestReleases = await octokit . repos . listReleases ( {
117
+ owner : OWNER ,
118
+ repo : REPO
119
+ } )
120
+
121
+ var filteredReleases = latestReleases . data . filter ( release => ! release . draft ) ; // consider only pre-releases and published releases
122
+
123
+ var releaseTagPrefix = 'v' + newRelease . split ( '.' ) [ 0 ] ;
124
+ console . log ( `Getting latest release starting with ${ releaseTagPrefix } ` ) ;
125
+
126
+ var latestReleaseInfo = filteredReleases . find ( release => release . tag_name . toLowerCase ( ) . startsWith ( releaseTagPrefix . toLowerCase ( ) ) ) ;
127
+ console . log ( `Previous release tag with ${ latestReleaseInfo . tag_name } and published date is: ${ latestReleaseInfo . published_at } ` )
128
+
129
+ var headBranchTag = 'v' + newRelease
130
+ try {
131
+ var comparison = await octokit . repos . compareCommits ( {
132
+ owner : OWNER ,
133
+ repo : REPO ,
134
+ base : latestReleaseInfo . tag_name ,
135
+ head : headBranchTag ,
136
+ } ) ;
137
+
138
+ var commitSHAs = comparison . data . commits . map ( commit => commit . sha ) ;
139
+
140
+ try {
141
+
142
+ var allPRs = await fetchPRsForSHAsGraphQL ( commitSHAs ) ;
143
+ editReleaseNotesFile ( { items : allPRs } ) ;
144
+ } catch ( e ) {
145
+ console . log ( e ) ;
146
+ console . log ( `Error: Problem in fetching PRs using commit SHA. Aborting.` ) ;
147
+ process . exit ( - 1 ) ;
148
+ }
149
+
150
+ } catch ( e ) {
151
+ console . log ( e ) ;
152
+ console . log ( `Error: Cannot find commits changes. Aborting.` ) ;
153
+ process . exit ( - 1 ) ;
154
+ }
155
+ }
156
+ catch ( e ) {
157
+ console . log ( e ) ;
158
+ console . log ( `Error: Cannot find releases. Aborting.` ) ;
159
+ process . exit ( - 1 ) ;
160
+ }
161
+ }
162
+
163
+
164
+ async function fetchPRsSinceLastReleaseAndEditReleaseNotes ( newRelease , callback ) {
68
165
var derivedFrom = opt . options . derivedFrom ;
69
166
console . log ( "Derived from %o" , derivedFrom ) ;
70
167
71
- try
72
- {
168
+ try {
73
169
var releaseInfo ;
74
170
75
- if ( derivedFrom !== 'latest' )
76
- {
171
+ // If derivedFrom is 'lastMinorRelease', fetch PRs by comparing with the previous release.
172
+ // For example:
173
+ // - If newRelease = 4.255.0, it will compare changes with the latest RELEASE/PRE-RELEASE tag starting with 4.xxx.xxx.
174
+ // - If newRelease = 3.255.1, it will compare changes with the latest RELEASE/PRE-RELEASE tag starting with 3.xxx.xxx.
175
+ if ( derivedFrom === 'lastMinorRelease' ) {
176
+ console . log ( "Fetching PRs by comparing with the previous release." )
177
+ await fetchPRsSincePreviousReleaseAndEditReleaseNotes ( newRelease , callback ) ;
178
+ return ;
179
+ }
180
+ else if ( derivedFrom !== 'latest' ) {
77
181
var tag = 'v' + derivedFrom ;
78
182
79
183
console . log ( `Getting release by tag ${ tag } ` ) ;
@@ -84,8 +188,7 @@ async function fetchPRsSinceLastReleaseAndEditReleaseNotes(newRelease, callback)
84
188
tag : tag
85
189
} ) ;
86
190
}
87
- else
88
- {
191
+ else {
89
192
console . log ( "Getting latest release" ) ;
90
193
91
194
releaseInfo = await octokit . repos . getLatestRelease ( {
@@ -97,55 +200,47 @@ async function fetchPRsSinceLastReleaseAndEditReleaseNotes(newRelease, callback)
97
200
var branch = opt . options . branch ;
98
201
var lastReleaseDate = releaseInfo . data . published_at ;
99
202
console . log ( `Fetching PRs merged since ${ lastReleaseDate } on ${ branch } ` ) ;
100
- try
101
- {
203
+ try {
102
204
var results = await octokit . search . issuesAndPullRequests ( {
103
- q :`type:pr+is:merged+repo:${ OWNER } /${ REPO } +base:${ branch } +merged:>=${ lastReleaseDate } ` ,
205
+ q : `type:pr+is:merged+repo:${ OWNER } /${ REPO } +base:${ branch } +merged:>=${ lastReleaseDate } ` ,
104
206
order : 'asc' ,
105
207
sort : 'created'
106
208
} )
107
209
editReleaseNotesFile ( results . data ) ;
108
210
}
109
- catch ( e )
110
- {
211
+ catch ( e ) {
111
212
console . log ( `Error: Problem fetching PRs: ${ e } ` ) ;
112
213
process . exit ( - 1 ) ;
113
214
}
114
215
}
115
- catch ( e )
116
- {
216
+ catch ( e ) {
117
217
console . log ( e ) ;
118
218
console . log ( `Error: Cannot find release ${ opt . options . derivedFrom } . Aborting.` ) ;
119
219
process . exit ( - 1 ) ;
120
220
}
121
221
}
122
222
123
- function editReleaseNotesFile ( body )
124
- {
223
+
224
+ function editReleaseNotesFile ( body ) {
125
225
var releaseNotesFile = path . join ( __dirname , '..' , 'releaseNote.md' ) ;
126
226
var existingReleaseNotes = fs . readFileSync ( releaseNotesFile ) ;
127
227
var newPRs = { 'Features' : [ ] , 'Bugs' : [ ] , 'Misc' : [ ] } ;
128
228
body . items . forEach ( function ( item ) {
129
229
var category = 'Misc' ;
130
230
item . labels . forEach ( function ( label ) {
131
- if ( category )
132
- {
133
- if ( label . name === 'bug' )
134
- {
231
+ if ( category ) {
232
+ if ( label . name === 'bug' ) {
135
233
category = 'Bugs' ;
136
234
}
137
- if ( label . name === 'enhancement' )
138
- {
235
+ if ( label . name === 'enhancement' ) {
139
236
category = 'Features' ;
140
237
}
141
- if ( label . name === 'internal' )
142
- {
238
+ if ( label . name === 'internal' ) {
143
239
category = null ;
144
240
}
145
241
}
146
242
} ) ;
147
- if ( category )
148
- {
243
+ if ( category ) {
149
244
newPRs [ category ] . push ( ` - ${ item . title } (#${ item . number } )` ) ;
150
245
}
151
246
} ) ;
@@ -158,39 +253,33 @@ function editReleaseNotesFile(body)
158
253
newReleaseNotes += existingReleaseNotes ;
159
254
var editorCmd = `${ process . env . EDITOR } ${ releaseNotesFile } ` ;
160
255
console . log ( editorCmd ) ;
161
- if ( opt . options . dryrun )
162
- {
256
+ if ( opt . options . dryrun ) {
163
257
console . log ( 'Found the following PRs = %o' , newPRs ) ;
164
258
console . log ( '\n\n' ) ;
165
259
console . log ( newReleaseNotes ) ;
166
260
console . log ( '\n' ) ;
167
261
}
168
- else
169
- {
262
+ else {
170
263
fs . writeFileSync ( releaseNotesFile , newReleaseNotes ) ;
171
- try
172
- {
264
+ try {
173
265
cp . execSync ( `${ process . env . EDITOR } ${ releaseNotesFile } ` , {
174
266
stdio : [ process . stdin , process . stdout , process . stderr ]
175
267
} ) ;
176
268
}
177
- catch ( err )
178
- {
269
+ catch ( err ) {
179
270
console . log ( err . message ) ;
180
271
process . exit ( - 1 ) ;
181
272
}
182
273
}
183
274
}
184
275
185
- function commitAndPush ( directory , release , branch )
186
- {
276
+ function commitAndPush ( directory , release , branch ) {
187
277
util . execInForeground ( GIT + " checkout -b " + branch , directory , opt . options . dryrun ) ;
188
278
util . execInForeground ( `${ GIT } commit -m "Agent Release ${ release } " ` , directory , opt . options . dryrun ) ;
189
279
util . execInForeground ( `${ GIT } -c credential.helper='!f() { echo "username=pat"; echo "password=$PAT"; };f' push --set-upstream origin ${ branch } ` , directory , opt . options . dryrun ) ;
190
280
}
191
281
192
- function commitAgentChanges ( directory , release )
193
- {
282
+ function commitAgentChanges ( directory , release ) {
194
283
var newBranch = `releases/${ release } ` ;
195
284
util . execInForeground ( `${ GIT } add ${ path . join ( 'src' , 'agentversion' ) } ` , directory , opt . options . dryrun ) ;
196
285
util . execInForeground ( `${ GIT } add releaseNote.md` , directory , opt . options . dryrun ) ;
@@ -199,31 +288,25 @@ function commitAgentChanges(directory, release)
199
288
commitAndPush ( directory , release , newBranch ) ;
200
289
}
201
290
202
- function checkGitStatus ( )
203
- {
204
- var git_status = cp . execSync ( `${ GIT } status --untracked-files=no --porcelain` , { encoding : 'utf-8' } ) ;
205
- if ( git_status )
206
- {
291
+ function checkGitStatus ( ) {
292
+ var git_status = cp . execSync ( `${ GIT } status --untracked-files=no --porcelain` , { encoding : 'utf-8' } ) ;
293
+ if ( git_status ) {
207
294
console . log ( 'You have uncommited changes in this clone. Aborting.' ) ;
208
295
console . log ( git_status ) ;
209
- if ( ! opt . options . dryrun )
210
- {
296
+ if ( ! opt . options . dryrun ) {
211
297
process . exit ( - 1 ) ;
212
298
}
213
299
}
214
- else
215
- {
300
+ else {
216
301
console . log ( 'Git repo is clean.' ) ;
217
302
}
218
303
return git_status ;
219
304
}
220
305
221
- async function main ( )
222
- {
306
+ async function main ( ) {
223
307
try {
224
308
var newRelease = opt . argv [ 0 ] ;
225
- if ( newRelease === undefined )
226
- {
309
+ if ( newRelease === undefined ) {
227
310
console . log ( 'Error: You must supply a version' ) ;
228
311
process . exit ( - 1 ) ;
229
312
}
@@ -242,4 +325,4 @@ async function main()
242
325
}
243
326
}
244
327
245
- main ( ) ;
328
+ main ( ) ;
0 commit comments