tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

prepare-release-pr.py (5296B)


      1 # mypy: disallow-untyped-defs
      2 """
      3 This script is part of the pytest release process which is triggered manually in the Actions
      4 tab of the repository.
      5 
      6 The user will need to enter the base branch to start the release from (for example
      7 ``6.1.x`` or ``main``) and if it should be a major release.
      8 
      9 The appropriate version will be obtained based on the given branch automatically.
     10 
     11 After that, it will create a release using the `release` tox environment, and push a new PR.
     12 
     13 **Token**: currently the token from the GitHub Actions is used, pushed with
     14 `pytest bot <pytestbot@gmail.com>` commit author.
     15 """
     16 
     17 import argparse
     18 from pathlib import Path
     19 import re
     20 from subprocess import check_call
     21 from subprocess import check_output
     22 from subprocess import run
     23 
     24 from colorama import Fore
     25 from colorama import init
     26 from github3.repos import Repository
     27 
     28 
     29 class InvalidFeatureRelease(Exception):
     30    pass
     31 
     32 
     33 SLUG = "pytest-dev/pytest"
     34 
     35 PR_BODY = """\
     36 Created by the [prepare release pr]\
     37 (https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml) workflow.
     38 
     39 Once all builds pass and it has been **approved** by one or more maintainers, start the \
     40 [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
     41 
     42 * `Use workflow from`: `release-{version}`.
     43 * `Release version`: `{version}`.
     44 
     45 Or execute on the command line:
     46 
     47 ```console
     48 gh workflow run deploy.yml -r release-{version} -f version={version}
     49 ```
     50 
     51 After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
     52 """
     53 
     54 
     55 def login(token: str) -> Repository:
     56    import github3
     57 
     58    github = github3.login(token=token)
     59    owner, repo = SLUG.split("/")
     60    return github.repository(owner, repo)
     61 
     62 
     63 def prepare_release_pr(
     64    base_branch: str, is_major: bool, token: str, prerelease: str
     65 ) -> None:
     66    print()
     67    print(f"Processing release for branch {Fore.CYAN}{base_branch}")
     68 
     69    check_call(["git", "checkout", f"origin/{base_branch}"])
     70 
     71    changelog = Path("changelog")
     72 
     73    features = list(changelog.glob("*.feature.rst"))
     74    breaking = list(changelog.glob("*.breaking.rst"))
     75    is_feature_release = bool(features or breaking)
     76 
     77    try:
     78        version = find_next_version(
     79            base_branch, is_major, is_feature_release, prerelease
     80        )
     81    except InvalidFeatureRelease as e:
     82        print(f"{Fore.RED}{e}")
     83        raise SystemExit(1) from None
     84 
     85    print(f"Version: {Fore.CYAN}{version}")
     86 
     87    release_branch = f"release-{version}"
     88 
     89    run(
     90        ["git", "config", "user.name", "pytest bot"],
     91        check=True,
     92    )
     93    run(
     94        ["git", "config", "user.email", "pytestbot@gmail.com"],
     95        check=True,
     96    )
     97 
     98    run(
     99        ["git", "checkout", "-b", release_branch, f"origin/{base_branch}"],
    100        check=True,
    101    )
    102 
    103    print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
    104 
    105    if is_major:
    106        template_name = "release.major.rst"
    107    elif prerelease:
    108        template_name = "release.pre.rst"
    109    elif is_feature_release:
    110        template_name = "release.minor.rst"
    111    else:
    112        template_name = "release.patch.rst"
    113 
    114    # important to use tox here because we have changed branches, so dependencies
    115    # might have changed as well
    116    cmdline = [
    117        "tox",
    118        "-e",
    119        "release",
    120        "--",
    121        version,
    122        template_name,
    123        release_branch,  # doc_version
    124        "--skip-check-links",
    125    ]
    126    print("Running", " ".join(cmdline))
    127    run(
    128        cmdline,
    129        check=True,
    130    )
    131 
    132    oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
    133    run(
    134        ["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"],
    135        check=True,
    136    )
    137    print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.")
    138 
    139    body = PR_BODY.format(version=version)
    140    repo = login(token)
    141    pr = repo.create_pull(
    142        f"Prepare release {version}",
    143        base=base_branch,
    144        head=release_branch,
    145        body=body,
    146    )
    147    print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.")
    148 
    149 
    150 def find_next_version(
    151    base_branch: str, is_major: bool, is_feature_release: bool, prerelease: str
    152 ) -> str:
    153    output = check_output(["git", "tag"], encoding="UTF-8")
    154    valid_versions = []
    155    for v in output.splitlines():
    156        m = re.match(r"\d.\d.\d+$", v.strip())
    157        if m:
    158            valid_versions.append(tuple(int(x) for x in v.split(".")))
    159 
    160    valid_versions.sort()
    161    last_version = valid_versions[-1]
    162 
    163    if is_major:
    164        return f"{last_version[0]+1}.0.0{prerelease}"
    165    elif is_feature_release:
    166        return f"{last_version[0]}.{last_version[1] + 1}.0{prerelease}"
    167    else:
    168        return f"{last_version[0]}.{last_version[1]}.{last_version[2] + 1}{prerelease}"
    169 
    170 
    171 def main() -> None:
    172    init(autoreset=True)
    173    parser = argparse.ArgumentParser()
    174    parser.add_argument("base_branch")
    175    parser.add_argument("token")
    176    parser.add_argument("--major", action="store_true", default=False)
    177    parser.add_argument("--prerelease", default="")
    178    options = parser.parse_args()
    179    prepare_release_pr(
    180        base_branch=options.base_branch,
    181        is_major=options.major,
    182        token=options.token,
    183        prerelease=options.prerelease,
    184    )
    185 
    186 
    187 if __name__ == "__main__":
    188    main()