1+ import argparse
12import datetime
23from github import Github
3- import random
4- import requests
5- import subprocess
6- import sys
74import json
8- import datetime
95import os
6+ import subprocess
107
118EMPTY_CHANGELOG = """# CodeQL Action and CodeQL Runner Changelog
129
1613
1714"""
1815
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+
2522# Name of the remote
2623ORIGIN = 'origin'
2724
@@ -38,8 +35,8 @@ def run_git(*args):
3835def branch_exists_on_remote (branch_name ):
3936 return run_git ('ls-remote' , '--heads' , ORIGIN , branch_name ).strip () != ''
4037
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 ):
4340 # Sort the commits into the pull requests that introduced them,
4441 # and any commits that don't have a pull request
4542 pull_requests = []
@@ -61,9 +58,8 @@ def open_pr(repo, all_commits, short_main_sha, branch_name):
6158
6259 # Start constructing the body text
6360 body = []
64- body .append ('Merging ' + short_main_sha + ' into ' + LATEST_RELEASE_BRANCH )
61+ body .append ('Merging ' + source_branch_short_sha + ' into ' + target_branch )
6562
66- conductor = get_conductor (repo , pull_requests , commits_without_pull_requests )
6763 body .append ('' )
6864 body .append ('Conductor for this PR is @' + conductor )
6965
@@ -80,43 +76,40 @@ def open_pr(repo, all_commits, short_main_sha, branch_name):
8076 body .append ('' )
8177 body .append ('Contains the following commits not from a pull request:' )
8278 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 )
8481
8582 body .append ('' )
8683 body .append ('Please review the following:' )
8784 body .append (' - [ ] The CHANGELOG displays the correct version and date.' )
8885 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.' )
9087 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.' )
9291
93- title = 'Merge ' + MAIN_BRANCH + ' into ' + LATEST_RELEASE_BRANCH
92+ title = 'Merge ' + source_branch + ' into ' + target_branch
9493
9594 # Create the pull request
9695 # PR checks won't be triggered on PRs created by Actions. Therefore mark the PR as draft so that
9796 # 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 )
9999 print ('Created PR #' + str (pr .number ))
100100
101101 # Assign the conductor
102102 pr .add_to_assignees (conductor )
103103 print ('Assigned PR to ' + conductor )
104104
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 ()
120113
121114 # Convert to full-fledged commit objects
122115 commits = [repo .get_commit (c ) for c in commits ]
@@ -136,7 +129,7 @@ def get_truncated_commit_message(commit):
136129 else :
137130 return message
138131
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.
140133# Returns the PR object, or None if no PR could be found.
141134def get_pr_for_commit (repo , commit ):
142135 prs = commit .get_pulls ()
@@ -179,29 +172,69 @@ def update_changelog(version):
179172
180173
181174def 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 } '" )
186214
187- repo = Github (github_token ).get_repo (repository_nwo )
215+ repo = Github (args . github_token ).get_repo (args . repository_nwo )
188216 version = get_current_version ()
189217
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+
190223 # 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 )
194227
195228 # 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 )
197230 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 )
199232 return
200233
201234 # The branch name is based off of the name of branch being merged into
202235 # and the SHA of the branch being merged from. Thus if the branch already
203236 # 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
205238 print ('Branch name is ' + new_branch_name )
206239
207240 # Check if the branch already exists. If so we can abort as this script
@@ -212,19 +245,79 @@ def main():
212245
213246 # Create the new branch and push it to the remote
214247 print ('Creating branch ' + new_branch_name )
215- run_git ('checkout' , '-b' , new_branch_name , ORIGIN + '/' + MAIN_BRANCH )
216248
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+ # Remove changelog notes from v2 that don't apply to v1
289+ subprocess .run (['sed' , '-i' , '/^- \[v2+ only\]/d' , 'CHANGELOG.md' ])
290+
291+ # Amend the commit generated by `npm version` to update the CHANGELOG
292+ run_git ('add' , 'CHANGELOG.md' )
293+ run_git ('commit' , '-m' , f'Update version and changelog for v{ version } ' )
294+ else :
295+ # If we're performing a standard release, there won't be any new commits on the target branch,
296+ # as these will have already been merged back into the source branch. Therefore we can just
297+ # start from the source branch.
298+ run_git ('checkout' , '-b' , new_branch_name , f'{ ORIGIN } /{ source_branch } ' )
299+
300+ print ('Updating changelog' )
301+ update_changelog (version )
219302
220- # Create a commit that updates the CHANGELOG
221- run_git ('add' , 'CHANGELOG.md' )
222- run_git ('commit' , '-m' , version )
303+ # Create a commit that updates the CHANGELOG
304+ run_git ('add' , 'CHANGELOG.md' )
305+ run_git ('commit' , '-m' , f'Update changelog for v { version } ' )
223306
224307 run_git ('push' , ORIGIN , new_branch_name )
225308
226309 # Open a PR to update the branch
227- open_pr (repo , commits , short_main_sha , new_branch_name )
310+ open_pr (
311+ repo ,
312+ commits ,
313+ source_branch_short_sha ,
314+ new_branch_name ,
315+ source_branch = source_branch ,
316+ target_branch = target_branch ,
317+ conductor = args .conductor ,
318+ is_v2_release = args .mode == V2_MODE ,
319+ labels = ['Update dependencies' ] if args .mode == V1_MODE else [],
320+ )
228321
229322if __name__ == '__main__' :
230323 main ()
0 commit comments