1
+ import argparse
1
2
import datetime
2
3
from github import Github
3
- import random
4
- import requests
5
- import subprocess
6
- import sys
7
4
import json
8
- import datetime
9
5
import os
6
+ import subprocess
10
7
11
8
EMPTY_CHANGELOG = """# CodeQL Action and CodeQL Runner Changelog
12
9
16
13
17
14
"""
18
15
19
- # The branch being merged from.
20
- # This is the one that contains day-to-day development work.
21
- MAIN_BRANCH = 'main'
22
- # The branch being merged into.
23
- # This is the release branch that users reference.
24
- LATEST_RELEASE_BRANCH = 'v1'
16
+ # Value of the mode flag for a v1 release
17
+ V1_MODE = 'v1-release'
18
+
19
+ # Value of the mode flag for a v2 release
20
+ V2_MODE = 'v2- release'
21
+
25
22
# Name of the remote
26
23
ORIGIN = 'origin'
27
24
@@ -38,8 +35,8 @@ def run_git(*args):
38
35
def branch_exists_on_remote (branch_name ):
39
36
return run_git ('ls-remote' , '--heads' , ORIGIN , branch_name ).strip () != ''
40
37
41
- # Opens a PR from the given branch to the release branch
42
- def open_pr (repo , all_commits , short_main_sha , branch_name ):
38
+ # Opens a PR from the given branch to the target branch
39
+ def open_pr (repo , all_commits , source_branch_short_sha , new_branch_name , source_branch , target_branch , conductor , is_v2_release , labels ):
43
40
# Sort the commits into the pull requests that introduced them,
44
41
# and any commits that don't have a pull request
45
42
pull_requests = []
@@ -61,9 +58,8 @@ def open_pr(repo, all_commits, short_main_sha, branch_name):
61
58
62
59
# Start constructing the body text
63
60
body = []
64
- body .append ('Merging ' + short_main_sha + ' into ' + LATEST_RELEASE_BRANCH )
61
+ body .append ('Merging ' + source_branch_short_sha + ' into ' + target_branch )
65
62
66
- conductor = get_conductor (repo , pull_requests , commits_without_pull_requests )
67
63
body .append ('' )
68
64
body .append ('Conductor for this PR is @' + conductor )
69
65
@@ -80,43 +76,40 @@ def open_pr(repo, all_commits, short_main_sha, branch_name):
80
76
body .append ('' )
81
77
body .append ('Contains the following commits not from a pull request:' )
82
78
for commit in commits_without_pull_requests :
83
- body .append ('- ' + commit .sha + ' - ' + get_truncated_commit_message (commit ) + ' (@' + commit .author .login + ')' )
79
+ author_description = ' (@' + commit .author .login + ')' if commit .author is not None else ''
80
+ body .append ('- ' + commit .sha + ' - ' + get_truncated_commit_message (commit ) + author_description )
84
81
85
82
body .append ('' )
86
83
body .append ('Please review the following:' )
87
84
body .append (' - [ ] The CHANGELOG displays the correct version and date.' )
88
85
body .append (' - [ ] The CHANGELOG includes all relevant, user-facing changes since the last release.' )
89
- body .append (' - [ ] There are no unexpected commits being merged into the ' + LATEST_RELEASE_BRANCH + ' branch.' )
86
+ body .append (' - [ ] There are no unexpected commits being merged into the ' + target_branch + ' branch.' )
90
87
body .append (' - [ ] The docs team is aware of any documentation changes that need to be released.' )
91
- body .append (' - [ ] The mergeback PR is merged back into ' + MAIN_BRANCH + ' after this PR is merged.' )
88
+ if is_v2_release :
89
+ body .append (' - [ ] The mergeback PR is merged back into ' + source_branch + ' after this PR is merged.' )
90
+ body .append (' - [ ] The v1 release PR is merged after this PR is merged.' )
92
91
93
- title = 'Merge ' + MAIN_BRANCH + ' into ' + LATEST_RELEASE_BRANCH
92
+ title = 'Merge ' + source_branch + ' into ' + target_branch
94
93
95
94
# Create the pull request
96
95
# PR checks won't be triggered on PRs created by Actions. Therefore mark the PR as draft so that
97
96
# a maintainer can take the PR out of draft, thereby triggering the PR checks.
98
- pr = repo .create_pull (title = title , body = '\n ' .join (body ), head = branch_name , base = LATEST_RELEASE_BRANCH , draft = True )
97
+ pr = repo .create_pull (title = title , body = '\n ' .join (body ), head = new_branch_name , base = target_branch , draft = True )
98
+ pr .add_to_labels (* labels )
99
99
print ('Created PR #' + str (pr .number ))
100
100
101
101
# Assign the conductor
102
102
pr .add_to_assignees (conductor )
103
103
print ('Assigned PR to ' + conductor )
104
104
105
- # Gets the person who should be in charge of the mergeback PR
106
- def get_conductor (repo , pull_requests , other_commits ):
107
- # If there are any PRs then use whoever merged the last one
108
- if len (pull_requests ) > 0 :
109
- return get_merger_of_pr (repo , pull_requests [- 1 ])
110
-
111
- # Otherwise take the author of the latest commit
112
- return other_commits [- 1 ].author .login
113
-
114
- # Gets a list of the SHAs of all commits that have happened on main
115
- # since the release branched off.
116
- # This will not include any commits that exist on the release branch
117
- # that aren't on main.
118
- def get_commit_difference (repo ):
119
- commits = run_git ('log' , '--pretty=format:%H' , ORIGIN + '/' + LATEST_RELEASE_BRANCH + '..' + ORIGIN + '/' + MAIN_BRANCH ).strip ().split ('\n ' )
105
+ # Gets a list of the SHAs of all commits that have happened on the source branch
106
+ # since the last release to the target branch.
107
+ # This will not include any commits that exist on the target branch
108
+ # that aren't on the source branch.
109
+ def get_commit_difference (repo , source_branch , target_branch ):
110
+ # Passing split nothing means that the empty string splits to nothing: compare `''.split() == []`
111
+ # to `''.split('\n') == ['']`.
112
+ commits = run_git ('log' , '--pretty=format:%H' , ORIGIN + '/' + target_branch + '..' + ORIGIN + '/' + source_branch ).strip ().split ()
120
113
121
114
# Convert to full-fledged commit objects
122
115
commits = [repo .get_commit (c ) for c in commits ]
@@ -136,7 +129,7 @@ def get_truncated_commit_message(commit):
136
129
else :
137
130
return message
138
131
139
- # Converts a commit into the PR that introduced it to the main branch.
132
+ # Converts a commit into the PR that introduced it to the source branch.
140
133
# Returns the PR object, or None if no PR could be found.
141
134
def get_pr_for_commit (repo , commit ):
142
135
prs = commit .get_pulls ()
@@ -179,29 +172,69 @@ def update_changelog(version):
179
172
180
173
181
174
def main ():
182
- if len (sys .argv ) != 3 :
183
- raise Exception ('Usage: update-release.branch.py <github token> <repository nwo>' )
184
- github_token = sys .argv [1 ]
185
- repository_nwo = sys .argv [2 ]
175
+ parser = argparse .ArgumentParser ('update-release-branch.py' )
176
+
177
+ parser .add_argument (
178
+ '--github-token' ,
179
+ type = str ,
180
+ required = True ,
181
+ help = 'GitHub token, typically from GitHub Actions.'
182
+ )
183
+ parser .add_argument (
184
+ '--repository-nwo' ,
185
+ type = str ,
186
+ required = True ,
187
+ help = 'The nwo of the repository, for example github/codeql-action.'
188
+ )
189
+ parser .add_argument (
190
+ '--mode' ,
191
+ type = str ,
192
+ required = True ,
193
+ choices = [V2_MODE , V1_MODE ],
194
+ help = f"Which release to perform. '{ V2_MODE } ' uses main as the source branch and v2 as the target branch. " +
195
+ f"'{ V1_MODE } ' uses v2 as the source branch and v1 as the target branch."
196
+ )
197
+ parser .add_argument (
198
+ '--conductor' ,
199
+ type = str ,
200
+ required = True ,
201
+ help = 'The GitHub handle of the person who is conducting the release process.'
202
+ )
203
+
204
+ args = parser .parse_args ()
205
+
206
+ if args .mode == V2_MODE :
207
+ source_branch = 'main'
208
+ target_branch = 'v2'
209
+ elif args .mode == V1_MODE :
210
+ source_branch = 'v2'
211
+ target_branch = 'v1'
212
+ else :
213
+ raise ValueError (f"Unexpected value for release mode: '{ args .mode } '" )
186
214
187
- repo = Github (github_token ).get_repo (repository_nwo )
215
+ repo = Github (args . github_token ).get_repo (args . repository_nwo )
188
216
version = get_current_version ()
189
217
218
+ if args .mode == V1_MODE :
219
+ # Change the version number to a v1 equivalent
220
+ version = get_current_version ()
221
+ version = f'1{ version [1 :]} '
222
+
190
223
# Print what we intend to go
191
- print ('Considering difference between ' + MAIN_BRANCH + ' and ' + LATEST_RELEASE_BRANCH )
192
- short_main_sha = run_git ('rev-parse' , '--short' , ORIGIN + '/' + MAIN_BRANCH ).strip ()
193
- print ('Current head of ' + MAIN_BRANCH + ' is ' + short_main_sha )
224
+ print ('Considering difference between ' + source_branch + ' and ' + target_branch )
225
+ source_branch_short_sha = run_git ('rev-parse' , '--short' , ORIGIN + '/' + source_branch ).strip ()
226
+ print ('Current head of ' + source_branch + ' is ' + source_branch_short_sha )
194
227
195
228
# See if there are any commits to merge in
196
- commits = get_commit_difference (repo )
229
+ commits = get_commit_difference (repo = repo , source_branch = source_branch , target_branch = target_branch )
197
230
if len (commits ) == 0 :
198
- print ('No commits to merge from ' + MAIN_BRANCH + ' to ' + LATEST_RELEASE_BRANCH )
231
+ print ('No commits to merge from ' + source_branch + ' to ' + target_branch )
199
232
return
200
233
201
234
# The branch name is based off of the name of branch being merged into
202
235
# and the SHA of the branch being merged from. Thus if the branch already
203
236
# exists we can assume we don't need to recreate it.
204
- new_branch_name = 'update-v' + version + '-' + short_main_sha
237
+ new_branch_name = 'update-v' + version + '-' + source_branch_short_sha
205
238
print ('Branch name is ' + new_branch_name )
206
239
207
240
# Check if the branch already exists. If so we can abort as this script
@@ -212,19 +245,76 @@ def main():
212
245
213
246
# Create the new branch and push it to the remote
214
247
print ('Creating branch ' + new_branch_name )
215
- run_git ('checkout' , '-b' , new_branch_name , ORIGIN + '/' + MAIN_BRANCH )
216
248
217
- print ('Updating changelog' )
218
- update_changelog (version )
249
+ if args .mode == V1_MODE :
250
+ # If we're performing a backport, start from the v1 branch
251
+ print (f'Creating { new_branch_name } from the { ORIGIN } /v1 branch' )
252
+ run_git ('checkout' , '-b' , new_branch_name , f'{ ORIGIN } /v1' )
253
+
254
+ # Revert the commit that we made as part of the last release that updated the version number and
255
+ # changelog to refer to 1.x.x variants. This avoids merge conflicts in the changelog and
256
+ # package.json files when we merge in the v2 branch.
257
+ # This commit will not exist the first time we release the v1 branch from the v2 branch, so we
258
+ # use `git log --grep` to conditionally revert the commit.
259
+ print ('Reverting the 1.x.x version number and changelog updates from the last release to avoid conflicts' )
260
+ v1_update_commits = run_git ('log' , '--grep' , '^Update version and changelog for v' , '--format=%H' ).split ()
261
+
262
+ if len (v1_update_commits ) > 0 :
263
+ print (f' Reverting { v1_update_commits [0 ]} ' )
264
+ # Only revert the newest commit as older ones will already have been reverted in previous
265
+ # releases.
266
+ run_git ('revert' , v1_update_commits [0 ], '--no-edit' )
267
+
268
+ # Also revert the "Update checked-in dependencies" commit created by Actions.
269
+ update_dependencies_commit = run_git ('log' , '--grep' , '^Update checked-in dependencies' , '--format=%H' ).split ()[0 ]
270
+ print (f' Reverting { update_dependencies_commit } ' )
271
+ run_git ('revert' , update_dependencies_commit , '--no-edit' )
272
+
273
+ else :
274
+ print (' Nothing to revert.' )
275
+
276
+ print (f'Merging { ORIGIN } /{ source_branch } into the release prep branch' )
277
+ run_git ('merge' , f'{ ORIGIN } /{ source_branch } ' , '--no-edit' )
278
+
279
+ # Migrate the package version number from a v2 version number to a v1 version number
280
+ print (f'Setting version number to { version } ' )
281
+ subprocess .run (['npm' , 'version' , version , '--no-git-tag-version' ])
282
+ run_git ('add' , 'package.json' , 'package-lock.json' )
283
+
284
+ # Migrate the changelog notes from v2 version numbers to v1 version numbers
285
+ print ('Migrating changelog notes from v2 to v1' )
286
+ subprocess .run (['sed' , '-i' , 's/^## 2\./## 1./g' , 'CHANGELOG.md' ])
287
+
288
+ # Amend the commit generated by `npm version` to update the CHANGELOG
289
+ run_git ('add' , 'CHANGELOG.md' )
290
+ run_git ('commit' , '-m' , f'Update version and changelog for v{ version } ' )
291
+ else :
292
+ # If we're performing a standard release, there won't be any new commits on the target branch,
293
+ # as these will have already been merged back into the source branch. Therefore we can just
294
+ # start from the source branch.
295
+ run_git ('checkout' , '-b' , new_branch_name , f'{ ORIGIN } /{ source_branch } ' )
296
+
297
+ print ('Updating changelog' )
298
+ update_changelog (version )
219
299
220
- # Create a commit that updates the CHANGELOG
221
- run_git ('add' , 'CHANGELOG.md' )
222
- run_git ('commit' , '-m' , version )
300
+ # Create a commit that updates the CHANGELOG
301
+ run_git ('add' , 'CHANGELOG.md' )
302
+ run_git ('commit' , '-m' , f'Update changelog for v { version } ' )
223
303
224
304
run_git ('push' , ORIGIN , new_branch_name )
225
305
226
306
# Open a PR to update the branch
227
- open_pr (repo , commits , short_main_sha , new_branch_name )
307
+ open_pr (
308
+ repo ,
309
+ commits ,
310
+ source_branch_short_sha ,
311
+ new_branch_name ,
312
+ source_branch = source_branch ,
313
+ target_branch = target_branch ,
314
+ conductor = args .conductor ,
315
+ is_v2_release = args .mode == V2_MODE ,
316
+ labels = ['Update dependencies' ] if args .mode == V1_MODE else [],
317
+ )
228
318
229
319
if __name__ == '__main__' :
230
320
main ()
0 commit comments