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