Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Commit

Permalink
tools: add release script
Browse files Browse the repository at this point in the history
  • Loading branch information
rr- committed Apr 26, 2024
1 parent 487f012 commit dcadf45
Showing 1 changed file with 165 additions and 0 deletions.
165 changes: 165 additions & 0 deletions tools/release
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/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()

0 comments on commit dcadf45

Please sign in to comment.