Skip to content

Commit 4d6e9c0

Browse files
authored
Merge pull request #995 from github/henrymercer/update-release-process
Update release process to maintain both v2 and v1 releases
2 parents 6d1f0a0 + 839aa81 commit 4d6e9c0

File tree

5 files changed

+203
-77
lines changed

5 files changed

+203
-77
lines changed

Diff for: .github/update-release-branch.py

+144-54
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1+
import argparse
12
import datetime
23
from github import Github
3-
import random
4-
import requests
5-
import subprocess
6-
import sys
74
import json
8-
import datetime
95
import os
6+
import subprocess
107

118
EMPTY_CHANGELOG = """# CodeQL Action and CodeQL Runner Changelog
129
@@ -16,12 +13,12 @@
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
2623
ORIGIN = 'origin'
2724

@@ -38,8 +35,8 @@ def run_git(*args):
3835
def 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.
141134
def get_pr_for_commit(repo, commit):
142135
prs = commit.get_pulls()
@@ -179,29 +172,69 @@ def update_changelog(version):
179172

180173

181174
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}'")
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,76 @@ 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+
# 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)
219299

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}')
223303

224304
run_git('push', ORIGIN, new_branch_name)
225305

226306
# 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+
)
228318

229319
if __name__ == '__main__':
230320
main()

Diff for: .github/workflows/post-release-mergeback.yml

+11-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ on:
1515
push:
1616
branches:
1717
- v1
18+
- v2
1819

1920
jobs:
2021
merge-back:
@@ -25,10 +26,13 @@ jobs:
2526
HEAD_BRANCH: "${{ github.head_ref || github.ref }}"
2627

2728
steps:
28-
- name: Dump GitHub Event context
29+
- name: Dump environment
30+
run: env
31+
32+
- name: Dump GitHub context
2933
env:
30-
GITHUB_EVENT_CONTEXT: "${{ toJson(github.event) }}"
31-
run: echo "$GITHUB_EVENT_CONTEXT"
34+
GITHUB_CONTEXT: '${{ toJson(github) }}'
35+
run: echo "$GITHUB_CONTEXT"
3236

3337
- uses: actions/checkout@v2
3438
- uses: actions/setup-node@v2
@@ -90,7 +94,7 @@ jobs:
9094
git push origin --follow-tags "$VERSION"
9195
9296
- name: Create mergeback branch
93-
if: steps.check.outputs.exists != 'true'
97+
if: steps.check.outputs.exists != 'true' && contains(github.ref, 'v2')
9498
env:
9599
VERSION: "${{ steps.getVersion.outputs.version }}"
96100
NEW_BRANCH: "${{ steps.getVersion.outputs.newBranch }}"
@@ -100,11 +104,13 @@ jobs:
100104
PR_TITLE="Mergeback $VERSION $HEAD_BRANCH into $BASE_BRANCH"
101105
PR_BODY="Updates version and changelog."
102106
107+
# Update the version number ready for the next release
108+
npm version patch --no-git-tag-version
109+
103110
# Update the changelog
104111
perl -i -pe 's/^/## \[UNRELEASED\]\n\nNo user facing changes.\n\n/ if($.==3)' CHANGELOG.md
105112
git add .
106113
git commit -m "Update changelog and version after $VERSION"
107-
npm version patch
108114
109115
git push origin "$NEW_BRANCH"
110116

Diff for: .github/workflows/update-release-branch.yml

+33-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
name: Update release branch
22
on:
3-
repository_dispatch:
4-
# Example of how to trigger this:
5-
# curl -H "Authorization: Bearer <token>" -X POST https://api.github.com/repos/github/codeql-action/dispatches -d '{"event_type":"update-release-branch"}'
6-
# Replace <token> with a personal access token from this page: https://github.com/settings/tokens
7-
types: [update-release-branch]
3+
# You can trigger this workflow via workflow dispatch to start a release.
4+
# This will open a PR to update the v2 release branch.
85
workflow_dispatch:
96

7+
# When the v2 release is complete, this workflow will open a PR to update the v1 release branch.
8+
push:
9+
branches:
10+
- v2
11+
1012
jobs:
1113
update:
1214
timeout-minutes: 45
1315
runs-on: ubuntu-latest
14-
if: ${{ github.repository == 'github/codeql-action' }}
16+
if: github.repository == 'github/codeql-action'
1517
steps:
18+
- name: Dump environment
19+
run: env
20+
21+
- name: Dump GitHub context
22+
env:
23+
GITHUB_CONTEXT: '${{ toJson(github) }}'
24+
run: echo "$GITHUB_CONTEXT"
25+
1626
- uses: actions/checkout@v2
1727
with:
1828
# Need full history so we calculate diffs
@@ -33,5 +43,20 @@ jobs:
3343
git config --global user.email "[email protected]"
3444
git config --global user.name "github-actions[bot]"
3545
36-
- name: Update release branch
37-
run: python .github/update-release-branch.py ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }}
46+
- name: Update v2 release branch
47+
if: github.event_name == 'workflow_dispatch'
48+
run: |
49+
python .github/update-release-branch.py \
50+
--github-token ${{ secrets.GITHUB_TOKEN }} \
51+
--repository-nwo ${{ github.repository }} \
52+
--mode release-v2 \
53+
--conductor ${GITHUB_ACTOR}
54+
55+
- name: Update v1 release branch
56+
if: github.event_name == 'push'
57+
run: |
58+
python .github/update-release-branch.py \
59+
--github-token ${{ secrets.GITHUB_TOKEN }} \
60+
--repository-nwo ${{ github.repository }} \
61+
--mode release-v1 \
62+
--conductor ${GITHUB_ACTOR}

0 commit comments

Comments
 (0)