#!/usr/bin/env python3 """Updates the dates in the changelog to today's date, then launch the `git citool` GUI to create a commit of it, and uses the `gh` tool to file a pull request. To be run after the GA version of emissary has been tagged. change the appropriate files, then launch the `git citool` GUI to create a commit of it. """ from os import getenv import datetime import fileinput import os import re import sys from contextlib import contextmanager from typing import Generator from shutil import which from lib import base_version, build_version, git_add, git_check_clean, re_ga, re_ea, vX, vY, get_gh_repo from lib.uiutil import Checker, CheckResult, run from lib.uiutil import run_txtcapture as run_capture from lib import ansiterm from lib.gitutil import has_open_pr def main(next_ver: str, today: datetime.date, quiet: bool=False, commit: bool = True) -> int: """This edits several files (the Git directory "tree"), then launches the `git citool` GUI to commit them. This _should_ be an utterly trivial and readable list of for line in fileinput.FileInput("FILENAME", inplace=True): # edit 'line' as appropriate sys.stdout.write(line) git_add("FILENAME") blocks. However, the block to edit the releaseNotes.yml file is unfortunately more complex, given the structuring of YAML... maybe line-oriented processing wasn't the best choice for that file, but parsing and reconstructing at as YAML probably isn't great either. Hmmmm. """ if not quiet: print() print(f'Doing basic updates for v{next_ver}...') print() if which("gh") is None: print("gh tool is not installed.") print("Please install the tool and rerun this script:") print("https://github.com/cli/cli#installation") return 1 warning = """ ==> Warning: FIXME: This script does not have the property that if something goes wrong, you can just restart it; put another way: it does not have the property that each step is idempotent. If something does go wrong, then you'll have to address it, then resume where the script left off by going through the checklist manually (or by commenting out the already-completed steps). """ print(f"{ansiterm.sgr.fg_red}{warning}{ansiterm.sgr}") # This context manager and check function are pretty much just to produce # a nice list of steps... checker = Checker() @contextmanager def check(name: str) -> Generator[CheckResult, None, None]: with checker.check(name) as subcheck: yield subcheck if not getenv("GIT_TOKEN"): run(["gh", "auth", "login"]) m = re_ga.match(next_ver) if not m: m = re_ea.match(next_ver) assert m release_branch = f"release/v{m[vX]}.{m[vY]}" with check(f"Checking that {release_branch} exists"): run(["git", "rev-parse", "--verify", release_branch]) if not checker.ok: checker.ok = True with check(f"Creating {release_branch}"): run(["git", "checkout", "-b", release_branch]) run(["git", "push", "-u", "origin", release_branch]) if not checker.ok: print(f"Could not create {release_branch}") print("Please create this branch then rerun this script") return 1 user = os.getenv("USER") if user is None or user == '': user = 'unknownuser' workbranch = f"{user}/v{next_ver}/changelog-updates" with check(f"Creating new branch {workbranch}"): if getenv('CI') == '': run(["git", "fetch", "--tags"]) run(["git", "checkout", f"v{next_ver}", "-b", workbranch]) if not checker.ok: return 1 with check(f"Updating releaseNotes.yml with date for {next_ver}..."): state = 0 for line in fileinput.FileInput("docs/releaseNotes.yml", inplace=True): if state == 0: if line.strip() == f"- version: {next_ver}": state = 1 elif state == 1: if m := re.match(r"(\s+date:)\s+.*$", line): line = f"{m.group(1)} '{today.strftime('%Y-%m-%d')}'\n" state = 2 sys.stdout.write(line) if state != 2: # Something didn't go right. sadness = f""" ==> ERROR: could not find the date for version '{next_ver}' in docs/releaseNotes.yml. Make sure that the block for that release starts with - version: {next_ver} date: .... as the start of the block.""" raise Exception(sadness) # Once this is done, update the CHANGELOG from the release notes. with check(f"Updating CHANGELOG.md from releaseNotes.yml..."): run([ "make", f"{os.getcwd()}/CHANGELOG.md" ]) if checker.ok and commit: with check(f"Committing changes..."): gitdir = run_capture(['git', 'rev-parse', '--git-dir']) with open(os.path.join(gitdir, 'GITGUI_MSG'), 'w') as msgfile: msgfile.write(f"Update for v{next_ver}\n") if getenv("AMBASSADOR_RELENG_NO_GUI"): run(['git', 'commit', '-am', f'Update for v{next_ver}']) else: run(['git', 'citool']) run(["git", "push", "-u", "origin", workbranch]) if not checker.ok: return 1 with check(f"Creating PR for branch {workbranch}"): # TODO: dont hardcode owners pr_body = """Changelog date updates for {next_ver} Reviewers: The only changes in this PR should be updating the changelog entry for {next_ver} to the date it was released. If there are any other changes, the PR creator should note the reason.""" curr_release_branch = f"rel/v{next_ver}" target_branch = release_branch if has_open_pr(get_gh_repo(), release_branch, curr_release_branch): target_branch = curr_release_branch run(["gh", "pr", "create", "--repo", get_gh_repo(), "--base", target_branch, "--title", f"[v{next_ver}] Changelog date update", "--body", pr_body.format(next_ver=next_ver), "--reviewer", "kflynn,rhs,esmet,acookin"]) if checker.ok: if not getenv("AMBASSADOR_RELENG_NO_GUI"): run(["gh", "pr", "view", workbranch, "--repo", get_gh_repo(), "--web"]) return 0 else: return 1 if __name__ == '__main__': args = sys.argv[1:] quiet = False commit = True while args and args[0].startswith("--"): if args[0] == '--quiet': quiet = True args.pop(0) elif args and (args[0] == '--no-commit'): commit = False args.pop(0) if len(args) != 1 or (not re_ga.match(args[0]) and not re_ea.match(args[0])): sys.stderr.write(f"Usage: {os.path.basename(sys.argv[0])} X.Y.Z\n") sys.exit(2) sys.exit(main( next_ver=args[0], today=datetime.date.today(), quiet=quiet, commit=commit, ))