clyde 0.7.0

A cross-platform package manager for prebuilt applications
"""
SPDX-FileCopyrightText: 2022 Aurélien Gâteau <mail@agateau.com>

SPDX-License-Identifier: GPL-3.0-or-later

A set of tasks to simplify the release process. See docs/release-check-list.md
for details.
"""

import os
import re
import shutil
import sys

from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import List

from invoke import task, run


ARTIFACTS_DIR = Path("artifacts")

MAIN_BRANCH = "main"

def get_version():
    return os.environ["VERSION"]


def erun(*args, **kwargs):
    """Like run, but with echo on"""
    kwargs["echo"] = True
    return run(*args, **kwargs)


def cerun(c, *args, **kwargs):
    """Like Context.run, but with echo on"""
    kwargs["echo"] = True
    return c.run(*args, **kwargs)


def ask(msg: str) -> str:
    """Show a message, wait for input and returns it"""
    print(msg, end=" ")
    return input()


def is_ok(msg: str) -> bool:
    """Show a message, append (y/n) and return True if user select y or Y"""
    answer = ask(f"{msg} (y/n)").lower()
    return answer == "y"


@task(help={"skip_changelog": "Add skip-changelog label"})
def create_pr(c, skip_changelog=False):
    """Create a pull-request and mark it as auto-mergeable"""
    cmd = "gh pr create --fill"
    if skip_changelog:
        cmd += " --label skip-changelog"
    result = cerun(c, cmd, warn=True)
    if not result:
        sys.exit(1)
    cerun(c, "gh pr merge --auto -dm")


@task
def update_version(c):
    version = get_version()
    path = Path("Cargo.toml")
    text = path.read_text()
    text, count = re.subn(r"^version = .*", f"version = \"{version}\"", text,
                          flags=re.MULTILINE)
    assert count == 0 or count == 1
    path.write_text(text)


@task
def prepare_release(c):
    version = get_version()
    run(f"gh issue list -m {version}", pty=True)
    run("gh pr list", pty=True)
    if not is_ok("Continue?"):
        sys.exit(1)

    erun(f"git checkout {MAIN_BRANCH}")
    erun("git pull")
    erun("git status -s")
    if not is_ok("Continue?"):
        sys.exit(1)

    prepare_release2(c)


@task
def prepare_release2(c):
    version = get_version()
    erun("git checkout -b prep-release")

    update_version(c)

    erun(f"changie batch {version}")
    print(f"Review/edit changelog (.changes/{version}.md)")
    if not is_ok("Looks good?"):
        sys.exit(1)
    erun("changie merge")
    print("Review CHANGELOG.md")

    if not is_ok("Looks good?"):
        sys.exit(1)

    prepare_release3(c)


@task
def prepare_release3(c):
    version = get_version()
    # Rebuild to ensure Cargo.lock is updated
    erun("cargo build")
    erun("git add Cargo.toml Cargo.lock CHANGELOG.md .changes")
    erun(f"git commit -m 'Prepare {version}'")

    prepare_release4(c)


@task
def prepare_release4(c):
    erun("cargo publish --dry-run")
    erun("git push -u origin prep-release")
    create_pr(c)


@task
def tag(c):
    version = get_version()
    erun(f"git checkout {MAIN_BRANCH}")
    erun("git pull")
    changes_file = Path(".changes") / f"{version}.md"
    if not changes_file.exists():
        print(f"{changes_file} does not exist, check previous PR has been merged")
        sys.exit(1)
    if not is_ok("Create tag?"):
        sys.exit(1)

    erun(f"git tag -a {version} -m 'Releasing version {version}'")

    erun("git push")
    erun("git push --tags")


def get_artifact_list() -> List[Path]:
    assert ARTIFACTS_DIR.exists()
    return list(ARTIFACTS_DIR.glob("*.tar.gz")) + list(ARTIFACTS_DIR.glob("*.zip"))


@task
def download_artifacts(c):
    if ARTIFACTS_DIR.exists():
        shutil.rmtree(ARTIFACTS_DIR)
    ARTIFACTS_DIR.mkdir()
    erun(f"gh run download --dir {ARTIFACTS_DIR}", pty=True)


def prepare_release_notes(version_md: Path) -> str:
    """
    Take the content of $VERSION.md and return it ready to use as GitHub release notes:
    - Remove header
    - Turn all h3 into h2
    """
    content = re.sub("^## .*", "", version_md.read_text())
    content = re.sub("^### ", "## ", content, flags=re.MULTILINE)
    return content.strip() + "\n"


@task
def publish(c):
    version = get_version()
    files_str = " ".join(str(x) for x in get_artifact_list())
    with NamedTemporaryFile() as tmp_file:
        content = prepare_release_notes(Path(".changes") / f"{version}.md")
        tmp_file.write(content.encode("utf-8"))
        tmp_file.flush()
        erun(f"gh release create {version} -F{tmp_file.name} {files_str}")
    erun("cargo publish")


@task
def update_store(c):
    version = get_version()
    with c.cd("../clyde-store"):
        cerun(c, "git checkout main")
        cerun(c, "git pull")
        cerun(c, "git checkout -b update-clyde")
        cerun(c, "clydetools fetch clyde.yaml", pty=True)
        cerun(c, "git add clyde.yaml")
        cerun(c, f"git commit -m 'Update clyde to {version}'")
        cerun(c, "git push -u origin update-clyde")
        create_pr(c)