diff --git a/deploy.sh b/deploy.sh index c1896d9..559e7c7 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,7 +1,124 @@ -./lint.sh && \ -./test.sh && \ -python3 -m pip install --upgrade build && \ -python3 -m pip install --upgrade twine && \ -rm -rf dist && \ -python3 -m build && \ -python3 -m twine upload dist/* +#!/bin/bash +set -euo pipefail + +PYTHON="${PYTHON:-python}" + +usage() { + echo "Usage: $0 [version]" >&2 + echo "Example: $0 3.13.5" >&2 +} + +die() { + echo "deploy.sh: $*" >&2 + exit 1 +} + +current_version() { + "$PYTHON" - <<'PY' +import re +from pathlib import Path + +text = Path("mhctools/__init__.py").read_text() +match = re.search(r'^__version__ = "([^"]+)"$', text, re.MULTILINE) +if not match: + raise SystemExit("Unable to find mhctools.__version__") +print(match.group(1)) +PY +} + +bump_version() { + local version="$1" + "$PYTHON" - "$version" <<'PY' +import re +import sys +from pathlib import Path + +version = sys.argv[1] +if not re.fullmatch(r"[0-9]+[.][0-9]+[.][0-9]+", version): + raise SystemExit( + "Version must be X.Y.Z without a leading 'v', got %r" % version) + +path = Path("mhctools/__init__.py") +text = path.read_text() +new_text, count = re.subn( + r'^__version__ = "[^"]+"$', + '__version__ = "%s"' % version, + text, + count=1, + flags=re.MULTILINE) +if count != 1: + raise SystemExit("Unable to update mhctools.__version__") +path.write_text(new_text) +PY +} + +require_clean_tree() { + git update-index -q --refresh + git diff --quiet || die "working tree has unstaged changes" + git diff --cached --quiet || die "working tree has staged changes" +} + +require_release_branch() { + local branch + branch="$(git rev-parse --abbrev-ref HEAD)" + if [[ "$branch" != "main" && "$branch" != "master" ]]; then + die "must deploy from main or master, currently on $branch" + fi + echo "$branch" +} + +require_tooling() { + "$PYTHON" -m build --version >/dev/null || + die "python module 'build' is unavailable; install the dev extra" + "$PYTHON" -m twine --version >/dev/null || + die "python module 'twine' is unavailable; install the dev extra" +} + +if [[ "$#" -gt 1 ]]; then + usage + exit 2 +fi + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +branch="$(require_release_branch)" +require_clean_tree +require_tooling + +requested_version="${1:-}" +if [[ -n "$requested_version" ]]; then + requested_version="${requested_version#v}" + old_version="$(current_version)" + if [[ "$requested_version" != "$old_version" ]]; then + bump_version "$requested_version" + ./lint.sh + ./test.sh + git add mhctools/__init__.py + git commit -m "Bump to $requested_version" + git push origin "$branch" + else + ./lint.sh + ./test.sh + fi +else + ./lint.sh + ./test.sh +fi + +release_version="$(current_version)" +tag="v$release_version" + +git fetch --tags origin +if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + die "tag $tag already exists" +fi + +rm -rf dist +"$PYTHON" -m build +"$PYTHON" -m twine upload dist/* + +git tag -a "$tag" -m "Release $tag" +git push origin "$tag" diff --git a/mhctools/__init__.py b/mhctools/__init__.py index c4063ec..cda4714 100644 --- a/mhctools/__init__.py +++ b/mhctools/__init__.py @@ -63,7 +63,7 @@ def __getattr__(name): raise AttributeError( "module %r has no attribute %r" % (__name__, name)) -__version__ = "3.13.4" +__version__ = "3.13.5" __all__ = [ "Prediction", diff --git a/tests/test_deploy_script.py b/tests/test_deploy_script.py new file mode 100644 index 0000000..0944a10 --- /dev/null +++ b/tests/test_deploy_script.py @@ -0,0 +1,31 @@ +import os +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DEPLOY_SH = ROOT / "deploy.sh" + + +def test_deploy_script_is_valid_bash(): + subprocess.run( + ["bash", "-n", str(DEPLOY_SH)], + check=True, + cwd=ROOT) + + +def test_deploy_script_matches_documented_release_guards(): + text = DEPLOY_SH.read_text() + + assert "git rev-parse --abbrev-ref HEAD" in text + assert '"main" && "$branch" != "master"' in text + assert "git diff --quiet" in text + assert "git diff --cached --quiet" in text + assert "python3 -m pip install" not in text + assert 'PYTHON="${PYTHON:-python}"' in text + assert "git tag -a" in text + assert "git push origin \"$tag\"" in text + + +def test_deploy_script_is_executable(): + assert os.access(DEPLOY_SH, os.X_OK)