-
Notifications
You must be signed in to change notification settings - Fork 375
update release process to support multiple version #2014
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
b7f289e
update release process to support multiple version
nickfyson 2dbffae
add note in CONTRIBUTING.md on how to deprecate an action version
nickfyson 65a2bb5
Merge branch 'main' into nickfyson/update-release-process
nickfyson 3537bea
Apply suggestions from code review
nickfyson a6ea3c5
define backport commit message in constant
nickfyson 57932be
remove unused function
nickfyson ee748cf
respond to more review comments
nickfyson 47e90f2
Merge branch 'main' into nickfyson/update-release-process
nickfyson 0e9a210
update workflows to run on all release branches
nickfyson Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
name: 'Release branches' | ||
description: 'Determine branches for release & backport' | ||
inputs: | ||
major_version: | ||
description: 'The version as extracted from the package.json file' | ||
required: true | ||
latest_tag: | ||
description: 'The most recent tag published to the repository' | ||
required: true | ||
outputs: | ||
backport_source_branch: | ||
description: "The release branch for the given tag" | ||
value: ${{ steps.branches.outputs.backport_source_branch }} | ||
backport_target_branches: | ||
description: "JSON encoded list of branches to target with backports" | ||
value: ${{ steps.branches.outputs.backport_target_branches }} | ||
runs: | ||
using: "composite" | ||
steps: | ||
- id: branches | ||
run: | | ||
python ${{ github.action_path }}/release-branches.py \ | ||
--major-version ${{ inputs.major_version }} \ | ||
--latest-tag ${{ inputs.latest_tag }} | ||
shell: bash |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import argparse | ||
import os, json | ||
import subprocess | ||
|
||
# Name of the remote | ||
ORIGIN = 'origin' | ||
|
||
OLDEST_SUPPORTED_MAJOR_VERSION = 2 | ||
|
||
# Runs git with the given args and returns the stdout. | ||
# Raises an error if git does not exit successfully (unless passed | ||
# allow_non_zero_exit_code=True). | ||
def run_git(*args, allow_non_zero_exit_code=False): | ||
cmd = ['git', *args] | ||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
if not allow_non_zero_exit_code and p.returncode != 0: | ||
raise Exception(f'Call to {" ".join(cmd)} exited with code {p.returncode} stderr: {p.stderr.decode("ascii")}.') | ||
return p.stdout.decode('ascii') | ||
henrymercer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def main(): | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument("--major-version", required=True, type=str, help="The major version of the release") | ||
parser.add_argument("--latest-tag", required=True, type=str, help="The most recent tag published to the repository") | ||
args = parser.parse_args() | ||
|
||
major_version = args.major_version | ||
latest_tag = args.latest_tag | ||
|
||
print("major_version: " + major_version) | ||
print("latest_tag: " + latest_tag) | ||
|
||
# If this is a primary release, we backport to all supported branches, | ||
# so we check whether the major_version taken from the package.json | ||
# is greater than or equal to the latest tag pulled from the repo. | ||
# For example... | ||
# 'v1' >= 'v2' is False # we're operating from an older release branch and should not backport | ||
# 'v2' >= 'v2' is True # the normal case where we're updating the current version | ||
# 'v3' >= 'v2' is True # in this case we are making the first release of a new major version | ||
consider_backports = ( major_version >= latest_tag.split(".")[0] ) | ||
|
||
with open(os.environ["GITHUB_OUTPUT"], "a") as f: | ||
|
||
f.write(f"backport_source_branch=releases/{major_version}\n") | ||
|
||
backport_target_branches = [] | ||
|
||
if consider_backports: | ||
for i in range(int(major_version.strip("v"))-1, 0, -1): | ||
branch_name = f"releases/v{i}" | ||
if i >= OLDEST_SUPPORTED_MAJOR_VERSION: | ||
backport_target_branches.append(branch_name) | ||
|
||
f.write("backport_target_branches="+json.dumps(backport_target_branches)+"\n") | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: 'Prepare release job' | ||
description: 'Executed preparatory steps before update a release branch' | ||
nickfyson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
|
||
- name: Dump environment | ||
run: env | ||
shell: bash | ||
|
||
- name: Dump GitHub context | ||
env: | ||
GITHUB_CONTEXT: '${{ toJson(github) }}' | ||
run: echo "$GITHUB_CONTEXT" | ||
shell: bash | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: 3.8 | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install PyGithub==1.55 requests | ||
shell: bash | ||
|
||
- name: Update git config | ||
run: | | ||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
git config --global user.name "github-actions[bot]" | ||
shell: bash |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,8 +13,6 @@ | |
|
||
""" | ||
|
||
SOURCE_BRANCH = 'main' | ||
TARGET_BRANCH = 'releases/v2' | ||
|
||
# Name of the remote | ||
ORIGIN = 'origin' | ||
|
@@ -34,7 +32,9 @@ 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 target branch | ||
def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conductor): | ||
def open_pr( | ||
repo, all_commits, source_branch_short_sha, new_branch_name, source_branch, target_branch, | ||
conductor, is_primary_release, conflicted_files): | ||
# Sort the commits into the pull requests that introduced them, | ||
# and any commits that don't have a pull request | ||
pull_requests = [] | ||
|
@@ -56,7 +56,7 @@ def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conduct | |
|
||
# Start constructing the body text | ||
body = [] | ||
body.append(f'Merging {source_branch_short_sha} into {TARGET_BRANCH}.') | ||
body.append(f'Merging {source_branch_short_sha} into {target_branch}.') | ||
|
||
body.append('') | ||
body.append(f'Conductor for this PR is @{conductor}.') | ||
|
@@ -79,20 +79,38 @@ def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conduct | |
|
||
body.append('') | ||
body.append('Please do the following:') | ||
if len(conflicted_files) > 0: | ||
body.append(' - [ ] Ensure `package.json` file contains the correct version.') | ||
body.append(' - [ ] Add commits to this branch to resolve the merge conflicts ' + | ||
'in the following files:') | ||
body.extend([f' - [ ] `{file}`' for file in conflicted_files]) | ||
body.append(' - [ ] Ensure another maintainer has reviewed the additional commits you added to this ' + | ||
'branch to resolve the merge conflicts.') | ||
body.append(' - [ ] Ensure the CHANGELOG displays the correct version and date.') | ||
body.append(' - [ ] Ensure the CHANGELOG includes all relevant, user-facing changes since the last release.') | ||
body.append(f' - [ ] Check that there are not any unexpected commits being merged into the {TARGET_BRANCH} branch.') | ||
body.append(f' - [ ] Check that there are not any unexpected commits being merged into the {target_branch} branch.') | ||
body.append(' - [ ] Ensure the docs team is aware of any documentation changes that need to be released.') | ||
|
||
if not is_primary_release: | ||
body.append(' - [ ] Remove and re-add the "Update dependencies" label to the PR to trigger just this workflow.') | ||
body.append(' - [ ] Wait for the "Update dependencies" workflow to push a commit updating the dependencies.') | ||
body.append(' - [ ] Mark the PR as ready for review to trigger the full set of PR checks.') | ||
henrymercer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
body.append(' - [ ] Approve and merge this PR. Make sure `Create a merge commit` is selected rather than `Squash and merge` or `Rebase and merge`.') | ||
body.append(' - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.') | ||
|
||
title = f'Merge {SOURCE_BRANCH} into {TARGET_BRANCH}' | ||
if is_primary_release: | ||
body.append(' - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.') | ||
body.append(' - [ ] Merge the v1 release PR that will automatically be created once this PR is merged.') | ||
henrymercer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
title = f'Merge {source_branch} into {target_branch}' | ||
labels = ['Update dependencies'] if not is_primary_release else [] | ||
|
||
# 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=new_branch_name, base=TARGET_BRANCH, draft=True) | ||
print(f'Created PR #{pr.number}') | ||
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(f'Created PR #{str(pr.number)}') | ||
|
||
# Assign the conductor | ||
pr.add_to_assignees(conductor) | ||
|
@@ -102,10 +120,10 @@ def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conduct | |
# since the last release to the target branch. | ||
# This will not include any commits that exist on the target branch | ||
# that aren't on the source branch. | ||
def get_commit_difference(repo): | ||
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', f'{ORIGIN}/{TARGET_BRANCH}..{ORIGIN}/{SOURCE_BRANCH}').strip().split() | ||
commits = run_git('log', '--pretty=format:%H', f'{ORIGIN}/{target_branch}..{ORIGIN}/{source_branch}').strip().split() | ||
|
||
# Convert to full-fledged commit objects | ||
commits = [repo.get_commit(c) for c in commits] | ||
|
@@ -182,6 +200,24 @@ def main(): | |
required=True, | ||
help='The nwo of the repository, for example github/codeql-action.' | ||
) | ||
parser.add_argument( | ||
'--source-branch', | ||
type=str, | ||
required=True, | ||
help='Source branch for release branch update.' | ||
) | ||
parser.add_argument( | ||
'--target-branch', | ||
type=str, | ||
required=True, | ||
help='Target branch for release branch update.' | ||
) | ||
parser.add_argument( | ||
'--is-primary-release', | ||
action='store_true', | ||
default=False, | ||
help='Whether this update is the primary release for the current major version.' | ||
) | ||
parser.add_argument( | ||
'--conductor', | ||
type=str, | ||
|
@@ -191,18 +227,29 @@ def main(): | |
|
||
args = parser.parse_args() | ||
|
||
source_branch = args.source_branch | ||
target_branch = args.target_branch | ||
is_primary_release = args.is_primary_release | ||
|
||
repo = Github(args.github_token).get_repo(args.repository_nwo) | ||
version = get_current_version() | ||
|
||
# the target branch will be of the form releases/vN, where N is the major version number | ||
target_branch_major_version = target_branch.strip('releases/v') | ||
|
||
# split version into major, minor, patch | ||
_, v_minor, v_patch = get_current_version().split('.') | ||
|
||
version = f"{target_branch_major_version}.{v_minor}.{v_patch}" | ||
|
||
# Print what we intend to go | ||
print(f'Considering difference between {SOURCE_BRANCH} and {TARGET_BRANCH}...') | ||
source_branch_short_sha = run_git('rev-parse', '--short', f'{ORIGIN}/{SOURCE_BRANCH}').strip() | ||
print(f'Current head of {SOURCE_BRANCH} is {source_branch_short_sha}.') | ||
print(f'Considering difference between {source_branch} and {target_branch}...') | ||
source_branch_short_sha = run_git('rev-parse', '--short', f'{ORIGIN}/{source_branch}').strip() | ||
print(f'Current head of {source_branch} is {source_branch_short_sha}.') | ||
|
||
# See if there are any commits to merge in | ||
commits = get_commit_difference(repo=repo) | ||
commits = get_commit_difference(repo=repo, source_branch=source_branch, target_branch=target_branch) | ||
if len(commits) == 0: | ||
print(f'No commits to merge from {SOURCE_BRANCH} to {TARGET_BRANCH}.') | ||
print(f'No commits to merge from {source_branch} to {target_branch}.') | ||
return | ||
|
||
# The branch name is based off of the name of branch being merged into | ||
|
@@ -220,17 +267,81 @@ def main(): | |
# Create the new branch and push it to the remote | ||
print(f'Creating branch {new_branch_name}.') | ||
|
||
# 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}/{SOURCE_BRANCH}') | ||
# The process of creating the v{Older} release can run into merge conflicts. We commit the unresolved | ||
# conflicts so a maintainer can easily resolve them (vs erroring and requiring maintainers to | ||
# reconstruct the release manually) | ||
conflicted_files = [] | ||
|
||
if not is_primary_release: | ||
|
||
# the source branch will be of the form releases/vN, where N is the major version number | ||
source_branch_major_version = source_branch.strip('releases/v') | ||
|
||
# If we're performing a backport, start from the target branch | ||
print(f'Creating {new_branch_name} from the {ORIGIN}/{target_branch} branch') | ||
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/{target_branch}') | ||
|
||
# Revert the commit that we made as part of the last release that updated the version number and | ||
# changelog to refer to {older}.x.x variants. This avoids merge conflicts in the changelog and | ||
# package.json files when we merge in the v{latest} branch. | ||
# This commit will not exist the first time we release the v{N-1} branch from the v{N} branch, so we | ||
# use `git log --grep` to conditionally revert the commit. | ||
print('Reverting the version number and changelog updates from the last release to avoid conflicts') | ||
vOlder_update_commits = run_git('log', '--grep', '^Update version and changelog for v', '--format=%H').split() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Super nit:
henrymercer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if len(vOlder_update_commits) > 0: | ||
print(f' Reverting {vOlder_update_commits[0]}') | ||
# Only revert the newest commit as older ones will already have been reverted in previous | ||
# releases. | ||
run_git('revert', vOlder_update_commits[0], '--no-edit') | ||
|
||
# Also revert the "Update checked-in dependencies" commit created by Actions. | ||
update_dependencies_commit = run_git('log', '--grep', '^Update checked-in dependencies', '--format=%H').split()[0] | ||
# TODO: why is this failing for the v2 branch currently...? | ||
henrymercer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print(f' Reverting {update_dependencies_commit}') | ||
run_git('revert', update_dependencies_commit, '--no-edit') | ||
|
||
else: | ||
print(' Nothing to revert.') | ||
|
||
print(f'Merging {ORIGIN}/{source_branch} into the release prep branch') | ||
# Commit any conflicts (see the comment for `conflicted_files`) | ||
run_git('merge', f'{ORIGIN}/{source_branch}', allow_non_zero_exit_code=True) | ||
conflicted_files = run_git('diff', '--name-only', '--diff-filter', 'U').splitlines() | ||
if len(conflicted_files) > 0: | ||
run_git('add', '.') | ||
run_git('commit', '--no-edit') | ||
|
||
# Migrate the package version number from a vLatest version number to a vOlder version number | ||
print(f'Setting version number to {version}') | ||
subprocess.check_output(['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 | ||
nickfyson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print(f'Migrating changelog notes from v{source_branch_major_version} to v{target_branch_major_version}') | ||
subprocess.check_output(['sed', '-i', f's/^## {source_branch_major_version}\./## {target_branch_major_version}./g', 'CHANGELOG.md']) | ||
|
||
# Remove changelog notes from all versions that do not apply to the vOlder branch | ||
print(f'Removing changelog notes that do not apply to v{target_branch_major_version}') | ||
for v in range(int(target_branch_major_version)+1, int(source_branch_major_version)+1): | ||
henrymercer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print(f'Removing changelog notes that are tagged [v{v}+ only\]') | ||
subprocess.check_output(['sed', '-i', f'/^- \[v{v}+ only\]/d', 'CHANGELOG.md']) | ||
|
||
# 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}/{source_branch}') | ||
|
||
print('Updating changelog') | ||
update_changelog(version) | ||
print('Updating changelog') | ||
update_changelog(version) | ||
|
||
# Create a commit that updates the CHANGELOG | ||
run_git('add', 'CHANGELOG.md') | ||
run_git('commit', '-m', f'Update changelog for v{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) | ||
|
||
|
@@ -240,7 +351,11 @@ def main(): | |
commits, | ||
source_branch_short_sha, | ||
new_branch_name, | ||
source_branch=source_branch, | ||
target_branch=target_branch, | ||
conductor=args.conductor, | ||
is_primary_release=is_primary_release, | ||
conflicted_files=conflicted_files | ||
) | ||
|
||
if __name__ == '__main__': | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.