Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update release process to maintain both v2 and v1 releases #995

Merged
merged 22 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2a7a517
Remove unused `repository_dispatch` trigger
henrymercer Mar 17, 2022
b386fd4
Parameterize release branch workflow over source and target branches
henrymercer Mar 17, 2022
81827d3
Use the person triggering the release workflow as the conductor
henrymercer Mar 17, 2022
ccda44c
Handle missing author information when generating changelog
henrymercer Mar 17, 2022
33f749f
Set up `main -> v2`, `v2 -> v1`, and `v2 -> main` merges
henrymercer Mar 17, 2022
d76b182
Add functionality for `v2 -> v1` backports
henrymercer Mar 17, 2022
4b465cb
Dump environment and GitHub context
henrymercer Mar 22, 2022
b8f3a37
Fix exception when there are no commits to merge
henrymercer Mar 22, 2022
124e7d9
Stop versioning the runner
henrymercer Mar 22, 2022
5fb01dd
Avoid commits with duplicate names during v2 to v1 backport
henrymercer Mar 22, 2022
bd4757c
Update the changelog and version number in a single commit
henrymercer Mar 22, 2022
1668e0a
Only mention merging the mergeback PR in the checklist when relevant
henrymercer Mar 22, 2022
0b037b4
Add merging the v1 release PR to the checklist
henrymercer Mar 23, 2022
f143182
Add "Update dependencies" label to v1 release PR
henrymercer Mar 23, 2022
3359990
Avoid conflicts by reverting 1.x version num commit from last v1 release
henrymercer Mar 23, 2022
da7944b
Update release process doc
henrymercer Mar 24, 2022
9d26fe0
Use source branch and target branch names consistently
henrymercer Mar 25, 2022
bed132d
Use a more restrictive `sed` pattern
henrymercer Mar 25, 2022
d0bd808
Expose a more restrictive interface to the release script
henrymercer Mar 25, 2022
f784647
Merge branch 'main' into henrymercer/update-release-process
henrymercer Mar 25, 2022
044f112
Update branch protection instructions
henrymercer Mar 25, 2022
839aa81
Merge branch 'main' into henrymercer/update-release-process
henrymercer Mar 25, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 132 additions & 48 deletions .github/update-release-branch.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import argparse
import datetime
from github import Github
import random
import requests
import subprocess
import sys
import json
import datetime
import os
import subprocess

EMPTY_CHANGELOG = """# CodeQL Action and CodeQL Runner Changelog

Expand All @@ -16,12 +13,6 @@

"""

# The branch being merged from.
# This is the one that contains day-to-day development work.
MAIN_BRANCH = 'main'
# The branch being merged into.
# This is the release branch that users reference.
LATEST_RELEASE_BRANCH = 'v1'
# Name of the remote
ORIGIN = 'origin'

Expand All @@ -39,7 +30,7 @@ def branch_exists_on_remote(branch_name):
return run_git('ls-remote', '--heads', ORIGIN, branch_name).strip() != ''

# Opens a PR from the given branch to the release branch
def open_pr(repo, all_commits, short_main_sha, branch_name):
def open_pr(repo, all_commits, short_main_sha, new_branch_name, source_branch, target_branch, conductor, is_v2_to_v1_backport, labels):
# Sort the commits into the pull requests that introduced them,
# and any commits that don't have a pull request
pull_requests = []
Expand All @@ -61,9 +52,8 @@ def open_pr(repo, all_commits, short_main_sha, branch_name):

# Start constructing the body text
body = []
body.append('Merging ' + short_main_sha + ' into ' + LATEST_RELEASE_BRANCH)
body.append('Merging ' + short_main_sha + ' into ' + target_branch)

conductor = get_conductor(repo, pull_requests, commits_without_pull_requests)
body.append('')
body.append('Conductor for this PR is @' + conductor)

Expand All @@ -80,43 +70,40 @@ def open_pr(repo, all_commits, short_main_sha, branch_name):
body.append('')
body.append('Contains the following commits not from a pull request:')
for commit in commits_without_pull_requests:
body.append('- ' + commit.sha + ' - ' + get_truncated_commit_message(commit) + ' (@' + commit.author.login + ')')
author_description = ' (@' + commit.author.login + ')' if commit.author is not None else ''
body.append('- ' + commit.sha + ' - ' + get_truncated_commit_message(commit) + author_description)

body.append('')
body.append('Please review the following:')
body.append(' - [ ] The CHANGELOG displays the correct version and date.')
henrymercer marked this conversation as resolved.
Show resolved Hide resolved
body.append(' - [ ] The CHANGELOG includes all relevant, user-facing changes since the last release.')
body.append(' - [ ] There are no unexpected commits being merged into the ' + LATEST_RELEASE_BRANCH + ' branch.')
body.append(' - [ ] There are no unexpected commits being merged into the ' + target_branch + ' branch.')
body.append(' - [ ] The docs team is aware of any documentation changes that need to be released.')
body.append(' - [ ] The mergeback PR is merged back into ' + MAIN_BRANCH + ' after this PR is merged.')
if not is_v2_to_v1_backport:
body.append(' - [ ] The mergeback PR is merged back into ' + source_branch + ' after this PR is merged.')
body.append(' - [ ] The v1 release PR is merged after this PR is merged.')

title = 'Merge ' + MAIN_BRANCH + ' into ' + LATEST_RELEASE_BRANCH
title = 'Merge ' + source_branch + ' into ' + target_branch

# Create the pull request
# PR checks won't be triggered on PRs created by Actions. Therefore mark the PR as draft so that
# a maintainer can take the PR out of draft, thereby triggering the PR checks.
pr = repo.create_pull(title=title, body='\n'.join(body), head=branch_name, base=LATEST_RELEASE_BRANCH, draft=True)
pr = repo.create_pull(title=title, body='\n'.join(body), head=new_branch_name, base=target_branch, draft=True)
pr.add_to_labels(*labels)
print('Created PR #' + str(pr.number))

# Assign the conductor
pr.add_to_assignees(conductor)
print('Assigned PR to ' + conductor)

# Gets the person who should be in charge of the mergeback PR
def get_conductor(repo, pull_requests, other_commits):
# If there are any PRs then use whoever merged the last one
if len(pull_requests) > 0:
return get_merger_of_pr(repo, pull_requests[-1])

# Otherwise take the author of the latest commit
return other_commits[-1].author.login

# Gets a list of the SHAs of all commits that have happened on main
henrymercer marked this conversation as resolved.
Show resolved Hide resolved
# since the release branched off.
# This will not include any commits that exist on the release branch
# that aren't on main.
def get_commit_difference(repo):
commits = run_git('log', '--pretty=format:%H', ORIGIN + '/' + LATEST_RELEASE_BRANCH + '..' + ORIGIN + '/' + MAIN_BRANCH).strip().split('\n')
def get_commit_difference(repo, source_branch, target_branch):
# Passing split nothing means that the empty string splits to nothing: compare `''.split() == []`
# to `''.split('\n') == ['']`.
commits = run_git('log', '--pretty=format:%H', ORIGIN + '/' + target_branch + '..' + ORIGIN + '/' + source_branch).strip().split()

# Convert to full-fledged commit objects
commits = [repo.get_commit(c) for c in commits]
Expand Down Expand Up @@ -179,23 +166,63 @@ def update_changelog(version):


def main():
if len(sys.argv) != 3:
raise Exception('Usage: update-release.branch.py <github token> <repository nwo>')
github_token = sys.argv[1]
repository_nwo = sys.argv[2]

repo = Github(github_token).get_repo(repository_nwo)
parser = argparse.ArgumentParser('update-release-branch.py')

parser.add_argument(
'--github-token',
type=str,
required=True,
help='GitHub token, typically from GitHub Actions.'
)
parser.add_argument(
'--repository-nwo',
type=str,
required=True,
help='The nwo of the repository, for example github/codeql-action.'
)
parser.add_argument(
'--source-branch',
henrymercer marked this conversation as resolved.
Show resolved Hide resolved
type=str,
required=True,
help='The branch being merged from, typically "main" for a v2 release or "v2" for a v1 release.'
)
parser.add_argument(
'--target-branch',
type=str,
required=True,
help='The branch being merged into, typically "v2" for a v2 release or "v1" for a v1 release.'
)
parser.add_argument(
'--conductor',
type=str,
required=True,
help='The GitHub handle of the person who is conducting the release process.'
)
parser.add_argument(
'--perform-v2-to-v1-backport',
action='store_true',
help='Pass this flag if this release is a backport from v2 to v1.'
)

args = parser.parse_args()

repo = Github(args.github_token).get_repo(args.repository_nwo)
version = get_current_version()

if args.perform_v2_to_v1_backport:
# Change the version number to a v1 equivalent
version = get_current_version()
version = f'1{version[1:]}'

# Print what we intend to go
print('Considering difference between ' + MAIN_BRANCH + ' and ' + LATEST_RELEASE_BRANCH)
short_main_sha = run_git('rev-parse', '--short', ORIGIN + '/' + MAIN_BRANCH).strip()
print('Current head of ' + MAIN_BRANCH + ' is ' + short_main_sha)
print('Considering difference between ' + args.source_branch + ' and ' + args.target_branch)
short_main_sha = run_git('rev-parse', '--short', ORIGIN + '/' + args.source_branch).strip()
print('Current head of ' + args.source_branch + ' is ' + short_main_sha)

# See if there are any commits to merge in
commits = get_commit_difference(repo)
commits = get_commit_difference(repo=repo, source_branch=args.source_branch, target_branch=args.target_branch)
if len(commits) == 0:
print('No commits to merge from ' + MAIN_BRANCH + ' to ' + LATEST_RELEASE_BRANCH)
print('No commits to merge from ' + args.source_branch + ' to ' + args.target_branch)
return

# The branch name is based off of the name of branch being merged into
Expand All @@ -212,19 +239,76 @@ def main():

# Create the new branch and push it to the remote
print('Creating branch ' + new_branch_name)
run_git('checkout', '-b', new_branch_name, ORIGIN + '/' + MAIN_BRANCH)

print('Updating changelog')
update_changelog(version)
if args.perform_v2_to_v1_backport:
# If we're performing a backport, start from the v1 branch
print(f'Creating {new_branch_name} from the {ORIGIN}/v1 branch')
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/v1')

# Revert the commit that we made as part of the last release that updated the version number and
# changelog to refer to 1.x.x variants. This avoids merge conflicts in the changelog and
# package.json files when we merge in the v2 branch.
# This commit will not exist the first time we release the v1 branch from the v2 branch, so we
# use `git log --grep` to conditionally revert the commit.
print('Reverting the 1.x.x version number and changelog updates from the last release to avoid conflicts')
v1_update_commits = run_git('log', '--grep', '^Update version and changelog for v', '--format=%H').split()

if len(v1_update_commits) > 0:
print(f' Reverting {v1_update_commits[0]}')
# Only revert the newest commit as older ones will already have been reverted in previous
# releases.
run_git('revert', v1_update_commits[0], '--no-edit')

# Also revert the "Update checked-in dependencies" commit created by Actions.
henrymercer marked this conversation as resolved.
Show resolved Hide resolved
update_dependencies_commit = run_git('log', '--grep', '^Update checked-in dependencies', '--format=%H').split()[0]
print(f' Reverting {update_dependencies_commit}')
run_git('revert', update_dependencies_commit, '--no-edit')

else:
print(' Nothing to revert.')

print(f'Merging {ORIGIN}/{args.source_branch} into the release prep branch')
run_git('merge', f'{ORIGIN}/{args.source_branch}', '--no-edit')

# Migrate the package version number from a v2 version number to a v1 version number
print(f'Setting version number to {version}')
subprocess.run(['npm', 'version', version, '--no-git-tag-version'])
run_git('add', 'package.json', 'package-lock.json')

# Migrate the changelog notes from v2 version numbers to v1 version numbers
print('Migrating changelog notes from v2 to v1')
subprocess.run(['sed', '-i', 's/## 2\./## 1\./g', 'CHANGELOG.md'])
henrymercer marked this conversation as resolved.
Show resolved Hide resolved

# Amend the commit generated by `npm version` to update the CHANGELOG
run_git('add', 'CHANGELOG.md')
run_git('commit', '-m', f'Update version and changelog for v{version}')
else:
# If we're performing a standard release, there won't be any new commits on the target branch,
# as these will have already been merged back into the source branch. Therefore we can just
# start from the source branch.
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/{args.source_branch}')

print('Updating changelog')
update_changelog(version)

# Create a commit that updates the CHANGELOG
run_git('add', 'CHANGELOG.md')
run_git('commit', '-m', version)
# Create a commit that updates the CHANGELOG
run_git('add', 'CHANGELOG.md')
run_git('commit', '-m', f'Update changelog for v{version}')

run_git('push', ORIGIN, new_branch_name)

# Open a PR to update the branch
open_pr(repo, commits, short_main_sha, new_branch_name)
open_pr(
repo,
commits,
short_main_sha,
new_branch_name,
source_branch=args.source_branch,
target_branch=args.target_branch,
conductor=args.conductor,
is_v2_to_v1_backport=args.perform_v2_to_v1_backport,
labels=['Update dependencies'] if args.perform_v2_to_v1_backport else [],
)

if __name__ == '__main__':
main()
16 changes: 11 additions & 5 deletions .github/workflows/post-release-mergeback.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
push:
branches:
- v1
henrymercer marked this conversation as resolved.
Show resolved Hide resolved
- v2

jobs:
merge-back:
Expand All @@ -25,10 +26,13 @@ jobs:
HEAD_BRANCH: "${{ github.head_ref || github.ref }}"

steps:
- name: Dump GitHub Event context
- name: Dump environment
run: env

- name: Dump GitHub context
env:
GITHUB_EVENT_CONTEXT: "${{ toJson(github.event) }}"
run: echo "$GITHUB_EVENT_CONTEXT"
GITHUB_CONTEXT: '${{ toJson(github) }}'
run: echo "$GITHUB_CONTEXT"

- uses: actions/checkout@v2
- uses: actions/setup-node@v2
Expand Down Expand Up @@ -90,7 +94,7 @@ jobs:
git push origin --follow-tags "$VERSION"

- name: Create mergeback branch
if: steps.check.outputs.exists != 'true'
if: steps.check.outputs.exists != 'true' && contains(github.ref, 'v2')
env:
VERSION: "${{ steps.getVersion.outputs.version }}"
NEW_BRANCH: "${{ steps.getVersion.outputs.newBranch }}"
Expand All @@ -100,11 +104,13 @@ jobs:
PR_TITLE="Mergeback $VERSION $HEAD_BRANCH into $BASE_BRANCH"
PR_BODY="Updates version and changelog."

# Update the version number ready for the next release
npm version patch --no-git-tag-version

# Update the changelog
perl -i -pe 's/^/## \[UNRELEASED\]\n\nNo user facing changes.\n\n/ if($.==3)' CHANGELOG.md
git add .
git commit -m "Update changelog and version after $VERSION"
npm version patch

git push origin "$NEW_BRANCH"

Expand Down
44 changes: 36 additions & 8 deletions .github/workflows/update-release-branch.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
name: Update release branch
on:
repository_dispatch:
# Example of how to trigger this:
# curl -H "Authorization: Bearer <token>" -X POST https://api.github.com/repos/github/codeql-action/dispatches -d '{"event_type":"update-release-branch"}'
# Replace <token> with a personal access token from this page: https://github.com/settings/tokens
types: [update-release-branch]
# You can trigger this workflow via workflow dispatch to start a release.
# This will open a PR to update the v2 release branch.
workflow_dispatch:

# When the v2 release is complete, this workflow will open a PR to update the v1 release branch.
push:
branches:
- v2

jobs:
update:
timeout-minutes: 45
runs-on: ubuntu-latest
if: ${{ github.repository == 'github/codeql-action' }}
if: github.repository == 'github/codeql-action'
steps:
- name: Dump environment
run: env

- name: Dump GitHub context
env:
GITHUB_CONTEXT: '${{ toJson(github) }}'
run: echo "$GITHUB_CONTEXT"

- uses: actions/checkout@v2
with:
# Need full history so we calculate diffs
Expand All @@ -33,5 +43,23 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "github-actions[bot]"

- name: Update release branch
run: python .github/update-release-branch.py ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }}
- name: Update v2 release branch
if: github.event_name == 'workflow_dispatch'
run: |
python .github/update-release-branch.py \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--repository-nwo ${{ github.repository }} \
--source-branch main \
--target-branch v2 \
--conductor ${GITHUB_ACTOR}

- name: Update v1 release branch
if: github.event_name == 'push'
run: |
python .github/update-release-branch.py \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--repository-nwo ${{ github.repository }} \
--source-branch v2 \
--target-branch v1 \
--conductor ${GITHUB_ACTOR} \
--perform-v2-to-v1-backport
Loading