|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Tag a reviewed master commit; GitHub Actions builds and publishes it.""" |
| 3 | + |
| 4 | +from __future__ import annotations |
| 5 | + |
| 6 | +import argparse |
| 7 | +import json |
| 8 | +import subprocess |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +from package import ROOT, version |
| 12 | + |
| 13 | + |
| 14 | +def run(*args: str) -> str: |
| 15 | + result = subprocess.run(args, cwd=ROOT, check=True, text=True, capture_output=True) |
| 16 | + return result.stdout.strip() |
| 17 | + |
| 18 | + |
| 19 | +def check_ready(tag: str) -> None: |
| 20 | + if Path(run("git", "rev-parse", "--show-toplevel")) != ROOT: |
| 21 | + raise ValueError("run from this repository") |
| 22 | + if run("git", "branch", "--show-current") != "master": |
| 23 | + raise ValueError("releases must come from master") |
| 24 | + if run("git", "status", "--porcelain"): |
| 25 | + raise ValueError("commit working-tree changes first") |
| 26 | + |
| 27 | + run("git", "fetch", "origin", "master", "--tags") |
| 28 | + head = run("git", "rev-parse", "HEAD") |
| 29 | + if head != run("git", "rev-parse", "origin/master"): |
| 30 | + raise ValueError("local master is not synchronized with origin/master") |
| 31 | + if run("git", "tag", "--list", tag): |
| 32 | + raise ValueError(f"tag already exists: {tag}") |
| 33 | + |
| 34 | + builds = json.loads(run( |
| 35 | + "gh", "run", "list", "--workflow", "build.yml", "--commit", head, |
| 36 | + "--json", "conclusion,status", "--limit", "20", |
| 37 | + )) |
| 38 | + if not any(build["status"] == "completed" and build["conclusion"] == "success" for build in builds): |
| 39 | + raise ValueError("wait for a successful Windows build of this commit before releasing") |
| 40 | + |
| 41 | + |
| 42 | +def main() -> None: |
| 43 | + parser = argparse.ArgumentParser(description=__doc__) |
| 44 | + parser.add_argument("--current", action="store_true", required=True) |
| 45 | + parser.add_argument("--check", action="store_true", help="validate without tagging") |
| 46 | + parser.add_argument("--yes", action="store_true", help="skip the confirmation prompt") |
| 47 | + args = parser.parse_args() |
| 48 | + |
| 49 | + tag = f"v{version()}" |
| 50 | + check_ready(tag) |
| 51 | + if args.check: |
| 52 | + print(f"Ready to publish {tag} from {run('git', 'rev-parse', '--short', 'HEAD')}") |
| 53 | + return |
| 54 | + if not args.yes and input(f"Create and push {tag}? [y/N] ").lower() not in ("y", "yes"): |
| 55 | + return |
| 56 | + |
| 57 | + run("git", "tag", "-a", tag, "-m", tag) |
| 58 | + run("git", "push", "origin", tag) |
| 59 | + print(f"Pushed {tag}. Check the release workflow before sharing its downloads.") |
| 60 | + |
| 61 | + |
| 62 | +if __name__ == "__main__": |
| 63 | + main() |
0 commit comments