#!/usr/bin/env python3 """Make a release. Usage: release.py [] For the release command $FMT_TOKEN should contain a GitHub personal access token obtained from https://github.com/settings/tokens. """ from __future__ import print_function import datetime import errno import fileinput import json import os import re import shutil import sys import urllib.request from subprocess import check_call import docopt class Git: def __init__(self, dir): self.dir = dir def call(self, method, args, **kwargs): return check_call(["git", method] + list(args), **kwargs) def add(self, *args): return self.call("add", args, cwd=self.dir) def checkout(self, *args): return self.call("checkout", args, cwd=self.dir) def clean(self, *args): return self.call("clean", args, cwd=self.dir) def clone(self, *args): return self.call("clone", list(args) + [self.dir]) def commit(self, *args): return self.call("commit", args, cwd=self.dir) def pull(self, *args): return self.call("pull", args, cwd=self.dir) def push(self, *args): return self.call("push", args, cwd=self.dir) def reset(self, *args): return self.call("reset", args, cwd=self.dir) def update(self, *args): clone = not os.path.exists(self.dir) if clone: self.clone(*args) return clone def clean_checkout(repo, branch): repo.clean("-f", "-d") repo.reset("--hard") repo.checkout(branch) class Runner: def __init__(self, cwd): self.cwd = cwd def __call__(self, *args, **kwargs): kwargs["cwd"] = kwargs.get("cwd", self.cwd) check_call(args, **kwargs) def create_build_env(): """Create a build environment.""" class Env: pass env = Env() env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) env.build_dir = "build" env.fmt_repo = Git(os.path.join(env.build_dir, "fmt")) return env if __name__ == "__main__": args = docopt.docopt(__doc__) env = create_build_env() fmt_repo = env.fmt_repo branch = args.get("") if branch is None: branch = "master" if not fmt_repo.update("-b", branch, "git@github.com:fmtlib/fmt"): clean_checkout(fmt_repo, branch) # Update the date in the changelog and extract the version and the first # section content. changelog = "ChangeLog.md" changelog_path = os.path.join(fmt_repo.dir, changelog) is_first_section = True first_section = [] for i, line in enumerate(fileinput.input(changelog_path, inplace=True)): if i == 0: version = re.match(r"# (.*) - TBD", line).group(1) line = "# {} - {}\n".format(version, datetime.date.today().isoformat()) elif not is_first_section: pass elif line.startswith("#"): is_first_section = False else: first_section.append(line) sys.stdout.write(line) if first_section[0] == "\n": first_section.pop(0) ns_version = None base_h_path = os.path.join(fmt_repo.dir, "include", "fmt", "base.h") for line in fileinput.input(base_h_path): m = re.match(r"\s*inline namespace v(.*) .*", line) if m: ns_version = m.group(1) break major_version = version.split(".")[0] if not ns_version or ns_version != major_version: raise Exception(f"Version mismatch {ns_version} != {major_version}") # Workaround GitHub-flavored Markdown treating newlines as
. changes = "" code_block = False stripped = False for line in first_section: if re.match(r"^\s*```", line): code_block = not code_block changes += line stripped = False continue if code_block: changes += line continue if line == "\n" or re.match(r"^\s*\|.*", line): if stripped: changes += "\n" stripped = False changes += line continue if stripped: line = " " + line.lstrip() changes += line.rstrip() stripped = True fmt_repo.checkout("-B", "release") fmt_repo.add(changelog) fmt_repo.commit("-m", "Update version") # Build the docs and package. run = Runner(fmt_repo.dir) run("cmake", ".") run("make", "doc", "package_source") # Create a release on GitHub. fmt_repo.push("origin", "release") auth_headers = {"Authorization": "token " + os.getenv("FMT_TOKEN")} req = urllib.request.Request( "https://api.github.com/repos/fmtlib/fmt/releases", data=json.dumps( { "tag_name": version, "target_commitish": "release", "body": changes, "draft": True, } ).encode("utf-8"), headers=auth_headers, method="POST", ) with urllib.request.urlopen(req) as response: if response.status != 201: raise Exception( f"Failed to create a release " + "{response.status} {response.reason}" ) response_data = json.loads(response.read().decode("utf-8")) id = response_data["id"] # Upload the package. uploads_url = "https://uploads.github.com/repos/fmtlib/fmt/releases" package = "fmt-{}.zip".format(version) req = urllib.request.Request( f"{uploads_url}/{id}/assets?name={package}", headers={"Content-Type": "application/zip"} | auth_headers, data=open("build/fmt/" + package, "rb").read(), method="POST", ) with urllib.request.urlopen(req) as response: if response.status != 201: raise Exception( f"Failed to upload an asset " "{response.status} {response.reason}" ) short_version = ".".join(version.split(".")[:-1]) check_call(["./mkdocs", "deploy", short_version])