diff --git a/.github/update-release-branch.py b/.github/update-release-branch.py index 6a8c35a236..a90a840d58 100644 --- a/.github/update-release-branch.py +++ b/.github/update-release-branch.py @@ -13,14 +13,8 @@ """ -# Value of the mode flag for a v1 release -V1_MODE = 'v1-release' - -# Value of the mode flag for a v2 release -V2_MODE = 'v2-release' - -SOURCE_BRANCH_FOR_MODE = { V1_MODE: 'releases/v2', V2_MODE: 'main' } -TARGET_BRANCH_FOR_MODE = { V1_MODE: 'releases/v1', V2_MODE: 'releases/v2' } +SOURCE_BRANCH = 'main' +TARGET_BRANCH = 'releases/v2' # Name of the remote ORIGIN = 'origin' @@ -32,7 +26,7 @@ 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('Call to ' + ' '.join(cmd) + ' exited with code ' + str(p.returncode) + ' stderr:' + p.stderr.decode('ascii')) + raise Exception(f'Call to {" ".join(cmd)} exited with code {p.returncode} stderr: {p.stderr.decode("ascii")}.') return p.stdout.decode('ascii') # Returns true if the given branch exists on the origin remote @@ -40,23 +34,21 @@ 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, source_branch, target_branch, - conductor, is_v2_release, labels, conflicted_files): +def open_pr(repo, all_commits, source_branch_short_sha, new_branch_name, conductor): # Sort the commits into the pull requests that introduced them, # and any commits that don't have a pull request pull_requests = [] commits_without_pull_requests = [] for commit in all_commits: - pr = get_pr_for_commit(repo, commit) + pr = get_pr_for_commit(commit) if pr is None: commits_without_pull_requests.append(commit) elif not any(p for p in pull_requests if p.number == pr.number): pull_requests.append(pr) - print('Found ' + str(len(pull_requests)) + ' pull requests') - print('Found ' + str(len(commits_without_pull_requests)) + ' commits not in a pull request') + print(f'Found {len(pull_requests)} pull requests.') + print(f'Found {len(commits_without_pull_requests)} commits not in a pull request.') # Sort PRs and commits by age pull_requests = sorted(pull_requests, key=lambda pr: pr.number) @@ -64,7 +56,7 @@ def open_pr( # Start constructing the body text body = [] - body.append('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}.') @@ -87,50 +79,33 @@ def open_pr( 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(' - [ ] 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_v2_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.') - 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.') - if is_v2_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.') - - title = 'Merge ' + source_branch + ' into ' + target_branch + title = f'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=new_branch_name, base=target_branch, draft=True) - pr.add_to_labels(*labels) - print('Created PR #' + str(pr.number)) + 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}') # Assign the conductor pr.add_to_assignees(conductor) - print('Assigned PR to ' + conductor) + print(f'Assigned PR to {conductor}') # Gets a list of the SHAs of all commits that have happened on the source branch # 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, source_branch, target_branch): +def get_commit_difference(repo): # 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() + 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] @@ -146,13 +121,13 @@ def is_pr_merge_commit(commit): def get_truncated_commit_message(commit): message = commit.commit.message.split('\n')[0] if len(message) > 60: - return message[:57] + '...' + return f'{message[:57]}...' else: return message # Converts a commit into the PR that introduced it to the source branch. # Returns the PR object, or None if no PR could be found. -def get_pr_for_commit(repo, commit): +def get_pr_for_commit(commit): prs = commit.get_pulls() if prs.totalCount > 0: @@ -186,7 +161,7 @@ def update_changelog(version): else: content = EMPTY_CHANGELOG - newContent = content.replace('[UNRELEASED]', version + ' - ' + get_today_string(), 1) + newContent = content.replace('[UNRELEASED]', f'${version} - {get_today_string()}', 1) with open('CHANGELOG.md', 'w') as f: f.write(newContent) @@ -207,16 +182,6 @@ def main(): required=True, help='The nwo of the repository, for example github/codeql-action.' ) - parser.add_argument( - '--mode', - type=str, - required=True, - choices=[V2_MODE, V1_MODE], - help=f"Which release to perform. '{V2_MODE}' uses {SOURCE_BRANCH_FOR_MODE[V2_MODE]} as the source " + - f"branch and {TARGET_BRANCH_FOR_MODE[V2_MODE]} as the target branch. " + - f"'{V1_MODE}' uses {SOURCE_BRANCH_FOR_MODE[V1_MODE]} as the source branch and " + - f"{TARGET_BRANCH_FOR_MODE[V1_MODE]} as the target branch." - ) parser.add_argument( '--conductor', type=str, @@ -226,110 +191,46 @@ def main(): args = parser.parse_args() - source_branch = SOURCE_BRANCH_FOR_MODE[args.mode] - target_branch = TARGET_BRANCH_FOR_MODE[args.mode] - repo = Github(args.github_token).get_repo(args.repository_nwo) version = get_current_version() - if args.mode == V1_MODE: - # 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 ' + source_branch + ' and ' + target_branch) - source_branch_short_sha = run_git('rev-parse', '--short', ORIGIN + '/' + source_branch).strip() - print('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, source_branch=source_branch, target_branch=target_branch) + commits = get_commit_difference(repo=repo) if len(commits) == 0: - print('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 # and the SHA of the branch being merged from. Thus if the branch already # exists we can assume we don't need to recreate it. - new_branch_name = 'update-v' + version + '-' + source_branch_short_sha - print('Branch name is ' + new_branch_name) + new_branch_name = f'update-v{version}-{source_branch_short_sha}' + print(f'Branch name is {new_branch_name}.') # Check if the branch already exists. If so we can abort as this script # has already run on this combination of branches. if branch_exists_on_remote(new_branch_name): - print('Branch ' + new_branch_name + ' already exists. Nothing to do.') + print(f'Branch {new_branch_name} already exists. Nothing to do.') return # Create the new branch and push it to the remote - print('Creating branch ' + new_branch_name) - - # The process of creating the v1 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 args.mode == V1_MODE: - # 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 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. - 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}/{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 v2 version number to a v1 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 - print('Migrating changelog notes from v2 to v1') - subprocess.check_output(['sed', '-i', 's/^## 2\./## 1./g', 'CHANGELOG.md']) - - # Remove changelog notes from v2 that don't apply to v1 - subprocess.check_output(['sed', '-i', '/^- \[v2+ 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(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}') - 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) @@ -339,12 +240,7 @@ def main(): commits, source_branch_short_sha, new_branch_name, - source_branch=source_branch, - target_branch=target_branch, conductor=args.conductor, - is_v2_release=args.mode == V2_MODE, - labels=['Update dependencies'] if args.mode == V1_MODE else [], - conflicted_files=conflicted_files ) if __name__ == '__main__': diff --git a/.github/workflows/post-release-mergeback.yml b/.github/workflows/post-release-mergeback.yml index a2c3204244..453b02c0bd 100644 --- a/.github/workflows/post-release-mergeback.yml +++ b/.github/workflows/post-release-mergeback.yml @@ -1,8 +1,9 @@ -# This workflow runs after a release of the action. For v2 releases, it merges any changes from the -# release back into the main branch. Typically, this is just a single commit that updates the -# changelog. For v2 and v1 releases, it then (a) tags the merge commit on the release branch that -# represents the new release with an `vx.y.z` tag and (b) updates the `vx` tag to refer to this -# commit. +# This workflow runs after a release of the action. It: +# 1. Merges any changes from the release back into the main branch. Typically, this is just a single +# commit that updates the changelog. +# 2. Tags the merge commit on the release branch that represents the new release with an `v2.x.y` +# tag +# 3. Updates the `v2` tag to refer to this merge commit. name: Tag release and merge back on: @@ -15,7 +16,6 @@ on: push: branches: - - releases/v1 - releases/v2 jobs: @@ -105,7 +105,7 @@ jobs: git push origin --atomic --force refs/tags/"${VERSION}" refs/tags/"${major_version_tag}" - name: Create mergeback branch - if: steps.check.outputs.exists != 'true' && contains(github.ref, 'releases/v2') + if: steps.check.outputs.exists != 'true' env: VERSION: "${{ steps.getVersion.outputs.version }}" NEW_BRANCH: "${{ steps.getVersion.outputs.newBranch }}" diff --git a/.github/workflows/update-release-branch.yml b/.github/workflows/update-release-branch.yml index 0907cb3cfa..7c4f1c6e41 100644 --- a/.github/workflows/update-release-branch.yml +++ b/.github/workflows/update-release-branch.yml @@ -4,11 +4,6 @@ on: # 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: - - releases/v2 - jobs: update: timeout-minutes: 45 @@ -43,20 +38,9 @@ jobs: git config --global user.email "github-actions@github.com" git config --global user.name "github-actions[bot]" - - 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 }} \ - --mode v2-release \ - --conductor ${GITHUB_ACTOR} - - - name: Update v1 release branch - if: github.event_name == 'push' + - name: Update release branch run: | python .github/update-release-branch.py \ --github-token ${{ secrets.GITHUB_TOKEN }} \ --repository-nwo ${{ github.repository }} \ - --mode v1-release \ --conductor ${GITHUB_ACTOR}