From f1a1de22579c11fd1e4301e0c6648ae46a74a1d3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2020 07:50:02 -0300 Subject: [PATCH] Use manual trigger to prepare release PRs (#8150) Co-authored-by: Ran Benita --- .github/workflows/prepare-release-pr.yml | 42 +++++++ scripts/prepare-release-pr.py | 151 +++++++++++++++++++++++ tox.ini | 7 ++ 3 files changed, 200 insertions(+) create mode 100644 .github/workflows/prepare-release-pr.yml create mode 100644 scripts/prepare-release-pr.py diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml new file mode 100644 index 000000000..848dd78a4 --- /dev/null +++ b/.github/workflows/prepare-release-pr.yml @@ -0,0 +1,42 @@ +name: prepare release pr + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to base the release from' + required: true + default: '' + major: + description: 'Major release? (yes/no)' + required: true + default: 'no' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.8" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade setuptools tox + + - name: Prepare release PR (minor/patch release) + if: github.event.inputs.branch.major == 'no' + run: | + tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ secrets.chatops }} + + - name: Prepare release PR (major release) + if: github.event.inputs.branch.major == 'yes' + run: | + tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ secrets.chatops }} --major diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py new file mode 100644 index 000000000..538a5af5a --- /dev/null +++ b/scripts/prepare-release-pr.py @@ -0,0 +1,151 @@ +""" +This script is part of the pytest release process which is triggered manually in the Actions +tab of the repository. + +The user will need to enter the base branch to start the release from (for example +``6.1.x`` or ``master``) and if it should be a major release. + +The appropriate version will be obtained based on the given branch automatically. + +After that, it will create a release using the `release` tox environment, and push a new PR. + +**Secret**: currently the secret is defined in the @pytestbot account, +which the core maintainers have access to. There we created a new secret named `chatops` +with write access to the repository. +""" +import argparse +import re +from pathlib import Path +from subprocess import check_call +from subprocess import check_output +from subprocess import run + +from colorama import Fore +from colorama import init +from github3.repos import Repository + + +class InvalidFeatureRelease(Exception): + pass + + +SLUG = "pytest-dev/pytest" + +PR_BODY = """\ +Created automatically from manual trigger. + +Once all builds pass and it has been **approved** by one or more maintainers, the build +can be released by pushing a tag `{version}` to this repository. +""" + + +def login(token: str) -> Repository: + import github3 + + github = github3.login(token=token) + owner, repo = SLUG.split("/") + return github.repository(owner, repo) + + +def prepare_release_pr(base_branch: str, is_major: bool, token: str) -> None: + print() + print(f"Processing release for branch {Fore.CYAN}{base_branch}") + + check_call(["git", "checkout", f"origin/{base_branch}"]) + + try: + version = find_next_version(base_branch, is_major) + except InvalidFeatureRelease as e: + print(f"{Fore.RED}{e}") + raise SystemExit(1) + + print(f"Version: {Fore.CYAN}{version}") + + release_branch = f"release-{version}" + + run( + ["git", "config", "user.name", "pytest bot"], + text=True, + check=True, + capture_output=True, + ) + run( + ["git", "config", "user.email", "pytestbot@gmail.com"], + text=True, + check=True, + capture_output=True, + ) + + run( + ["git", "checkout", "-b", release_branch, f"origin/{base_branch}"], + text=True, + check=True, + capture_output=True, + ) + + print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.") + + # important to use tox here because we have changed branches, so dependencies + # might have changed as well + cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"] + print("Running", " ".join(cmdline)) + run( + cmdline, text=True, check=True, capture_output=True, + ) + + oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git" + run( + ["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"], + text=True, + check=True, + capture_output=True, + ) + print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.") + + body = PR_BODY.format(version=version) + repo = login(token) + pr = repo.create_pull( + f"Prepare release {version}", base=base_branch, head=release_branch, body=body, + ) + print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.") + + +def find_next_version(base_branch: str, is_major: bool) -> str: + output = check_output(["git", "tag"], encoding="UTF-8") + valid_versions = [] + for v in output.splitlines(): + m = re.match(r"\d.\d.\d+$", v.strip()) + if m: + valid_versions.append(tuple(int(x) for x in v.split("."))) + + valid_versions.sort() + last_version = valid_versions[-1] + + changelog = Path("changelog") + + features = list(changelog.glob("*.feature.rst")) + breaking = list(changelog.glob("*.breaking.rst")) + is_feature_release = features or breaking + + if is_major: + return f"{last_version[0]+1}.0.0" + elif is_feature_release: + return f"{last_version[0]}.{last_version[1] + 1}.0" + else: + return f"{last_version[0]}.{last_version[1]}.{last_version[2] + 1}" + + +def main() -> None: + init(autoreset=True) + parser = argparse.ArgumentParser() + parser.add_argument("base_branch") + parser.add_argument("token") + parser.add_argument("--major", action="store_true", default=False) + options = parser.parse_args() + prepare_release_pr( + base_branch=options.base_branch, is_major=options.major, token=options.token + ) + + +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini index f0cfaa460..43e151c07 100644 --- a/tox.ini +++ b/tox.ini @@ -157,6 +157,13 @@ passenv = {[testenv:release]passenv} deps = {[testenv:release]deps} commands = python scripts/release-on-comment.py {posargs} +[testenv:prepare-release-pr] +decription = prepare a release PR from a manual trigger in GitHub actions +usedevelop = {[testenv:release]usedevelop} +passenv = {[testenv:release]passenv} +deps = {[testenv:release]deps} +commands = python scripts/prepare-release-pr.py {posargs} + [testenv:publish-gh-release-notes] description = create GitHub release after deployment basepython = python3