13
13
14
14
"""
15
15
16
- SOURCE_BRANCH = 'main'
17
- TARGET_BRANCH = 'releases/v2'
18
16
19
17
# Name of the remote
20
18
ORIGIN = 'origin'
@@ -34,7 +32,9 @@ def branch_exists_on_remote(branch_name):
34
32
return run_git ('ls-remote' , '--heads' , ORIGIN , branch_name ).strip () != ''
35
33
36
34
# Opens a PR from the given branch to the target branch
37
- def open_pr (repo , all_commits , source_branch_short_sha , new_branch_name , conductor ):
35
+ def open_pr (
36
+ repo , all_commits , source_branch_short_sha , new_branch_name , source_branch , target_branch ,
37
+ conductor , is_primary_release , conflicted_files ):
38
38
# Sort the commits into the pull requests that introduced them,
39
39
# and any commits that don't have a pull request
40
40
pull_requests = []
@@ -56,7 +56,7 @@ def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conduct
56
56
57
57
# Start constructing the body text
58
58
body = []
59
- body .append (f'Merging { source_branch_short_sha } into { TARGET_BRANCH } .' )
59
+ body .append (f'Merging { source_branch_short_sha } into { target_branch } .' )
60
60
61
61
body .append ('' )
62
62
body .append (f'Conductor for this PR is @{ conductor } .' )
@@ -79,20 +79,38 @@ def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conduct
79
79
80
80
body .append ('' )
81
81
body .append ('Please do the following:' )
82
+ if len (conflicted_files ) > 0 :
83
+ body .append (' - [ ] Ensure `package.json` file contains the correct version.' )
84
+ body .append (' - [ ] Add commits to this branch to resolve the merge conflicts ' +
85
+ 'in the following files:' )
86
+ body .extend ([f' - [ ] `{ file } `' for file in conflicted_files ])
87
+ body .append (' - [ ] Ensure another maintainer has reviewed the additional commits you added to this ' +
88
+ 'branch to resolve the merge conflicts.' )
82
89
body .append (' - [ ] Ensure the CHANGELOG displays the correct version and date.' )
83
90
body .append (' - [ ] Ensure the CHANGELOG includes all relevant, user-facing changes since the last release.' )
84
- body .append (f' - [ ] Check that there are not any unexpected commits being merged into the { TARGET_BRANCH } branch.' )
91
+ body .append (f' - [ ] Check that there are not any unexpected commits being merged into the { target_branch } branch.' )
85
92
body .append (' - [ ] Ensure the docs team is aware of any documentation changes that need to be released.' )
93
+
94
+ if not is_primary_release :
95
+ body .append (' - [ ] Remove and re-add the "Update dependencies" label to the PR to trigger just this workflow.' )
96
+ body .append (' - [ ] Wait for the "Update dependencies" workflow to push a commit updating the dependencies.' )
97
+ body .append (' - [ ] Mark the PR as ready for review to trigger the full set of PR checks.' )
98
+
86
99
body .append (' - [ ] Approve and merge this PR. Make sure `Create a merge commit` is selected rather than `Squash and merge` or `Rebase and merge`.' )
87
- body .append (' - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.' )
88
100
89
- title = f'Merge { SOURCE_BRANCH } into { TARGET_BRANCH } '
101
+ if is_primary_release :
102
+ body .append (' - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.' )
103
+ body .append (' - [ ] Merge the v1 release PR that will automatically be created once this PR is merged.' )
104
+
105
+ title = f'Merge { source_branch } into { target_branch } '
106
+ labels = ['Update dependencies' ] if not is_primary_release else []
90
107
91
108
# Create the pull request
92
109
# PR checks won't be triggered on PRs created by Actions. Therefore mark the PR as draft so that
93
110
# a maintainer can take the PR out of draft, thereby triggering the PR checks.
94
- pr = repo .create_pull (title = title , body = '\n ' .join (body ), head = new_branch_name , base = TARGET_BRANCH , draft = True )
95
- print (f'Created PR #{ pr .number } ' )
111
+ pr = repo .create_pull (title = title , body = '\n ' .join (body ), head = new_branch_name , base = target_branch , draft = True )
112
+ pr .add_to_labels (* labels )
113
+ print (f'Created PR #{ str (pr .number )} ' )
96
114
97
115
# Assign the conductor
98
116
pr .add_to_assignees (conductor )
@@ -102,10 +120,10 @@ def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conduct
102
120
# since the last release to the target branch.
103
121
# This will not include any commits that exist on the target branch
104
122
# that aren't on the source branch.
105
- def get_commit_difference (repo ):
123
+ def get_commit_difference (repo , source_branch , target_branch ):
106
124
# Passing split nothing means that the empty string splits to nothing: compare `''.split() == []`
107
125
# to `''.split('\n') == ['']`.
108
- commits = run_git ('log' , '--pretty=format:%H' , f'{ ORIGIN } /{ TARGET_BRANCH } ..{ ORIGIN } /{ SOURCE_BRANCH } ' ).strip ().split ()
126
+ commits = run_git ('log' , '--pretty=format:%H' , f'{ ORIGIN } /{ target_branch } ..{ ORIGIN } /{ source_branch } ' ).strip ().split ()
109
127
110
128
# Convert to full-fledged commit objects
111
129
commits = [repo .get_commit (c ) for c in commits ]
@@ -182,6 +200,24 @@ def main():
182
200
required = True ,
183
201
help = 'The nwo of the repository, for example github/codeql-action.'
184
202
)
203
+ parser .add_argument (
204
+ '--source-branch' ,
205
+ type = str ,
206
+ required = True ,
207
+ help = 'Source branch for release branch update.'
208
+ )
209
+ parser .add_argument (
210
+ '--target-branch' ,
211
+ type = str ,
212
+ required = True ,
213
+ help = 'Target branch for release branch update.'
214
+ )
215
+ parser .add_argument (
216
+ '--is-primary-release' ,
217
+ action = 'store_true' ,
218
+ default = False ,
219
+ help = 'Whether this update is the primary release for the current major version.'
220
+ )
185
221
parser .add_argument (
186
222
'--conductor' ,
187
223
type = str ,
@@ -191,18 +227,29 @@ def main():
191
227
192
228
args = parser .parse_args ()
193
229
230
+ source_branch = args .source_branch
231
+ target_branch = args .target_branch
232
+ is_primary_release = args .is_primary_release
233
+
194
234
repo = Github (args .github_token ).get_repo (args .repository_nwo )
195
- version = get_current_version ()
235
+
236
+ # the target branch will be of the form releases/vN, where N is the major version number
237
+ target_branch_major_version = target_branch .strip ('releases/v' )
238
+
239
+ # split version into major, minor, patch
240
+ _ , v_minor , v_patch = get_current_version ().split ('.' )
241
+
242
+ version = f"{ target_branch_major_version } .{ v_minor } .{ v_patch } "
196
243
197
244
# Print what we intend to go
198
- print (f'Considering difference between { SOURCE_BRANCH } and { TARGET_BRANCH } ...' )
199
- source_branch_short_sha = run_git ('rev-parse' , '--short' , f'{ ORIGIN } /{ SOURCE_BRANCH } ' ).strip ()
200
- print (f'Current head of { SOURCE_BRANCH } is { source_branch_short_sha } .' )
245
+ print (f'Considering difference between { source_branch } and { target_branch } ...' )
246
+ source_branch_short_sha = run_git ('rev-parse' , '--short' , f'{ ORIGIN } /{ source_branch } ' ).strip ()
247
+ print (f'Current head of { source_branch } is { source_branch_short_sha } .' )
201
248
202
249
# See if there are any commits to merge in
203
- commits = get_commit_difference (repo = repo )
250
+ commits = get_commit_difference (repo = repo , source_branch = source_branch , target_branch = target_branch )
204
251
if len (commits ) == 0 :
205
- print (f'No commits to merge from { SOURCE_BRANCH } to { TARGET_BRANCH } .' )
252
+ print (f'No commits to merge from { source_branch } to { target_branch } .' )
206
253
return
207
254
208
255
# The branch name is based off of the name of branch being merged into
@@ -220,17 +267,81 @@ def main():
220
267
# Create the new branch and push it to the remote
221
268
print (f'Creating branch { new_branch_name } .' )
222
269
223
- # If we're performing a standard release, there won't be any new commits on the target branch,
224
- # as these will have already been merged back into the source branch. Therefore we can just
225
- # start from the source branch.
226
- run_git ('checkout' , '-b' , new_branch_name , f'{ ORIGIN } /{ SOURCE_BRANCH } ' )
270
+ # The process of creating the v{Older} release can run into merge conflicts. We commit the unresolved
271
+ # conflicts so a maintainer can easily resolve them (vs erroring and requiring maintainers to
272
+ # reconstruct the release manually)
273
+ conflicted_files = []
274
+
275
+ if not is_primary_release :
276
+
277
+ # the source branch will be of the form releases/vN, where N is the major version number
278
+ source_branch_major_version = source_branch .strip ('releases/v' )
279
+
280
+ # If we're performing a backport, start from the target branch
281
+ print (f'Creating { new_branch_name } from the { ORIGIN } /{ target_branch } branch' )
282
+ run_git ('checkout' , '-b' , new_branch_name , f'{ ORIGIN } /{ target_branch } ' )
283
+
284
+ # Revert the commit that we made as part of the last release that updated the version number and
285
+ # changelog to refer to {older}.x.x variants. This avoids merge conflicts in the changelog and
286
+ # package.json files when we merge in the v{latest} branch.
287
+ # This commit will not exist the first time we release the v{N-1} branch from the v{N} branch, so we
288
+ # use `git log --grep` to conditionally revert the commit.
289
+ print ('Reverting the version number and changelog updates from the last release to avoid conflicts' )
290
+ vOlder_update_commits = run_git ('log' , '--grep' , '^Update version and changelog for v' , '--format=%H' ).split ()
291
+
292
+ if len (vOlder_update_commits ) > 0 :
293
+ print (f' Reverting { vOlder_update_commits [0 ]} ' )
294
+ # Only revert the newest commit as older ones will already have been reverted in previous
295
+ # releases.
296
+ run_git ('revert' , vOlder_update_commits [0 ], '--no-edit' )
297
+
298
+ # Also revert the "Update checked-in dependencies" commit created by Actions.
299
+ update_dependencies_commit = run_git ('log' , '--grep' , '^Update checked-in dependencies' , '--format=%H' ).split ()[0 ]
300
+ # TODO: why is this failing for the v2 branch currently...?
301
+ print (f' Reverting { update_dependencies_commit } ' )
302
+ run_git ('revert' , update_dependencies_commit , '--no-edit' )
303
+
304
+ else :
305
+ print (' Nothing to revert.' )
306
+
307
+ print (f'Merging { ORIGIN } /{ source_branch } into the release prep branch' )
308
+ # Commit any conflicts (see the comment for `conflicted_files`)
309
+ run_git ('merge' , f'{ ORIGIN } /{ source_branch } ' , allow_non_zero_exit_code = True )
310
+ conflicted_files = run_git ('diff' , '--name-only' , '--diff-filter' , 'U' ).splitlines ()
311
+ if len (conflicted_files ) > 0 :
312
+ run_git ('add' , '.' )
313
+ run_git ('commit' , '--no-edit' )
314
+
315
+ # Migrate the package version number from a vLatest version number to a vOlder version number
316
+ print (f'Setting version number to { version } ' )
317
+ subprocess .check_output (['npm' , 'version' , version , '--no-git-tag-version' ])
318
+ run_git ('add' , 'package.json' , 'package-lock.json' )
319
+
320
+ # Migrate the changelog notes from v2 version numbers to v1 version numbers
321
+ print (f'Migrating changelog notes from v{ source_branch_major_version } to v{ target_branch_major_version } ' )
322
+ subprocess .check_output (['sed' , '-i' , f's/^## { source_branch_major_version } \./## { target_branch_major_version } ./g' , 'CHANGELOG.md' ])
323
+
324
+ # Remove changelog notes from all versions that do not apply to the vOlder branch
325
+ print (f'Removing changelog notes that do not apply to v{ target_branch_major_version } ' )
326
+ for v in range (int (target_branch_major_version )+ 1 , int (source_branch_major_version )+ 1 ):
327
+ print (f'Removing changelog notes that are tagged [v{ v } + only\]' )
328
+ subprocess .check_output (['sed' , '-i' , f'/^- \[v{ v } + only\]/d' , 'CHANGELOG.md' ])
329
+
330
+ # Amend the commit generated by `npm version` to update the CHANGELOG
331
+ run_git ('add' , 'CHANGELOG.md' )
332
+ run_git ('commit' , '-m' , f'Update version and changelog for v{ version } ' )
333
+ else :
334
+ # If we're performing a standard release, there won't be any new commits on the target branch,
335
+ # as these will have already been merged back into the source branch. Therefore we can just
336
+ # start from the source branch.
337
+ run_git ('checkout' , '-b' , new_branch_name , f'{ ORIGIN } /{ source_branch } ' )
227
338
228
- print ('Updating changelog' )
229
- update_changelog (version )
339
+ print ('Updating changelog' )
340
+ update_changelog (version )
230
341
231
- # Create a commit that updates the CHANGELOG
232
- run_git ('add' , 'CHANGELOG.md' )
233
- run_git ('commit' , '-m' , f'Update changelog for v{ version } ' )
342
+ # Create a commit that updates the CHANGELOG
343
+ run_git ('add' , 'CHANGELOG.md' )
344
+ run_git ('commit' , '-m' , f'Update changelog for v{ version } ' )
234
345
235
346
run_git ('push' , ORIGIN , new_branch_name )
236
347
@@ -240,7 +351,11 @@ def main():
240
351
commits ,
241
352
source_branch_short_sha ,
242
353
new_branch_name ,
354
+ source_branch = source_branch ,
355
+ target_branch = target_branch ,
243
356
conductor = args .conductor ,
357
+ is_primary_release = is_primary_release ,
358
+ conflicted_files = conflicted_files
244
359
)
245
360
246
361
if __name__ == '__main__' :
0 commit comments