1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
#!/usr/bin/env python3
# /// script
# dependencies = [ "towncrier", "packaging" ]
# ///
from __future__ import annotations
import argparse
import re
import subprocess
from functools import cache
from typing import TYPE_CHECKING
from packaging.version import Version
if TYPE_CHECKING:
from collections.abc import Sequence
class BumpVersion(Version):
def __init__(self, version: str) -> None:
super().__init__(version)
if len(self.release) != 3:
msg = f"{version} must contain major, minor, and patch version."
raise argparse.ArgumentTypeError(msg)
base_branch = get_base_branch()
patch_branch_pattern = re.compile(r"\d+\.\d+\.x")
if self.micro != 0 and not patch_branch_pattern.fullmatch(base_branch):
msg = (
f"{version} is a patch release, but "
f"you are trying to release from a non-patch release branch: {base_branch}."
)
raise argparse.ArgumentTypeError(msg)
if self.micro == 0 and base_branch != "main":
msg = (
f"{version} is a minor or major release, "
f"but you are trying to release not from main: {base_branch}."
)
raise argparse.ArgumentTypeError(msg)
class Args(argparse.Namespace):
version: BumpVersion
dry_run: bool
def parse_args(argv: Sequence[str] | None = None) -> Args:
parser = argparse.ArgumentParser(
prog="towncrier-automation",
description=(
"This script runs towncrier for a given version, "
"creates a branch off of the current one, "
"and then creates a PR into the original branch with the changes. "
"The PR will be backported to main if the current branch is not main."
),
)
parser.add_argument(
"version",
type=BumpVersion,
help=(
"The new version for the release must have at least three parts, like `major.minor.patch` and no `major.minor`. "
"It can have a suffix like `major.minor.patch.dev0` or `major.minor.0rc1`."
),
)
parser.add_argument(
"--dry-run",
help="Whether or not to dry-run the actual creation of the pull request",
action="store_true",
)
args = parser.parse_args(argv, Args())
return args
def main(argv: Sequence[str] | None = None) -> None:
args = parse_args(argv)
# Run towncrier
subprocess.run(
["towncrier", "build", f"--version={args.version}", "--yes"], check=True
)
# Check if we are on the main branch to know if we need to backport
base_branch = get_base_branch()
pr_description = "" if base_branch == "main" else "@meeseeksdev backport to main"
branch_name = f"release_notes_{args.version}"
# Create a new branch + commit
subprocess.run(["git", "switch", "-c", branch_name], check=True)
subprocess.run(["git", "add", "docs/release-notes"], check=True)
pr_title = f"(chore): generate {args.version} release notes"
subprocess.run(["git", "commit", "-m", pr_title], check=True)
# push
if not args.dry_run:
subprocess.run(
["git", "push", "--set-upstream", "origin", branch_name], check=True
)
else:
print("Dry run, not pushing")
# Create a PR
subprocess.run(
[
"gh",
"pr",
"create",
f"--base={base_branch}",
f"--title={pr_title}",
f"--body={pr_description}",
"--label=skip-gpu-ci",
*(["--label=no milestone"] if base_branch == "main" else []),
*(["--dry-run"] if args.dry_run else []),
],
check=True,
)
# Enable auto-merge
if not args.dry_run:
subprocess.run(
["gh", "pr", "merge", branch_name, "--auto", "--squash"], check=True
)
else:
print("Dry run, not merging")
@cache
def get_base_branch():
return subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
if __name__ == "__main__":
main()
|