TR2X/tools/release
2024-04-26 23:17:37 +02:00

166 lines
4.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import argparse
import re
import sys
from datetime import datetime
from pathlib import Path
from subprocess import check_output, run
from shared.common import REPO_DIR
from shared.versioning import get_branch_version
HEADER = "## [Unreleased](https://github.com/LostArtefacts/TR2X/compare/stable...develop) - ××××-××-××"
CHANGELOG_PATH = REPO_DIR / "CHANGELOG.md"
def update_changelog(
changelog: str, old_version: str, new_version: str
) -> str:
if f"[{new_version}]" in changelog:
return changelog
changelog = re.sub("Unreleased", new_version, changelog, count=1)
changelog = re.sub("stable", old_version, changelog, count=1)
changelog = re.sub("develop", new_version, changelog, count=1)
changelog = re.sub(
"××××-××-××", datetime.now().strftime("%Y-%m-%d"), changelog
)
changelog = HEADER + "\n\n" + changelog
return changelog
class Git:
def checkout_branch(self, branch_name: str) -> None:
if check_output(["git", "diff", "--cached", "--name-only"]):
raise RuntimeError("Staged files")
check_output(["git", "checkout", branch_name])
def reset(self, target: str, hard: bool = False) -> None:
check_output(
["git", "reset", "develop", *(["--hard"] if hard else [])]
)
def delete_tag(self, tag_name: str) -> None:
run(["git", "tag", "-d", tag_name])
def create_tag(self, tag_name: str) -> None:
check_output(["git", "tag", tag_name])
def add(self, target: str) -> None:
check_output(["git", "add", target])
def commit(self, message: str) -> None:
check_output(["git", "commit", "-m", message])
def push(
self, upstream: str, targets: list[str], force: bool = False
) -> None:
check_output(
[
"git",
"push",
upstream,
*targets,
*(["--force-with-lease"] if force else []),
]
)
class BaseCommand:
name: str = NotImplemented
help: str = NotImplemented
def __init__(self, git: Git) -> None:
self.git = git
def decorate_parser(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument("version")
def run(self, args: argparse.Namespace) -> None:
raise NotImplementedError("not implemented")
class CommitCommand(BaseCommand):
name = "commit"
help = "Create and tag a commit with the release information"
def decorate_parser(self, parser: argparse.ArgumentParser) -> None:
super().decorate_parser(parser)
parser.add_argument(
"-d",
"--dry-run",
action='store_true',
help="only output the changelog to stdout, do not commit anything",
)
def run(self, args: argparse.Namespace) -> None:
self.git.checkout_branch("develop")
old_version = get_branch_version("origin/stable")
new_version = args.version
old_changelog = CHANGELOG_PATH.read_text()
new_changelog = update_changelog(
old_changelog, old_version, args.version
)
if old_changelog == new_changelog:
return
if args.dry_run:
print(new_changelog)
return
CHANGELOG_PATH.write_text(new_changelog)
self.git.add(CHANGELOG_PATH)
self.git.commit(f"docs: release {args.version}")
self.git.delete_tag(args.version)
self.git.create_tag(args.version)
class BranchCommand(BaseCommand):
name = "branch"
help = "Merge branch to the specified tag"
def run(self, args: argparse.Namespace) -> None:
self.git.checkout_branch("stable")
self.git.reset(args.version, hard=True)
self.git.checkout_branch("develop")
class PushCommand(BaseCommand):
name = "push"
help = (
"Push the develop and stable branches, and the version tag to GitHub"
)
def run(self, args) -> None:
self.git.push(
"origin", ["develop", "stable", args.version], force=True
)
def parse_args(commands: list[BaseCommand]) -> None:
parser = argparse.ArgumentParser(
description="Argument parser with subcommands"
)
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
for command in commands:
subparser = subparsers.add_parser(command.name, help=command.help)
command.decorate_parser(subparser)
subparser.set_defaults(command=command)
result = parser.parse_args()
if not hasattr(result, "command"):
parser.error("missing command")
return result
def main() -> None:
git = Git()
commands = [
command_cls(git) for command_cls in BaseCommand.__subclasses__()
]
args = parse_args(commands)
args.command.run(args)
main()