diff --git a/python/CHANGES.md b/python/CHANGES.md index 64ae502..0c03c93 100644 --- a/python/CHANGES.md +++ b/python/CHANGES.md @@ -1,5 +1,9 @@ # insta-science +## 0.3.1 + +Fix the semi-automated release process. + ## 0.3.0 Add support for Python 3.8. diff --git a/python/RELEASE.md b/python/RELEASE.md index 80b4749..3d55b36 100644 --- a/python/RELEASE.md +++ b/python/RELEASE.md @@ -7,7 +7,9 @@ 1. Bump the version in [`insta_science/version.py`](insta_science/version.py). 2. Run `uv run dev-cmd` as a sanity check on the state of the project. 3. Update [`CHANGES.md`](CHANGES.md) with any changes that are likely to be useful to consumers. -4. Open a PR with these changes and land it on https://github.com/a-scie/science-installers main. +4. Run `uv run dev-cmd -q release -- --dry-run` as a sanity check the release will work once the + current changes are commited to main. +5. Open a PR with these changes and land it on https://github.com/a-scie/science-installers main. ## Release diff --git a/python/insta_science/version.py b/python/insta_science/version.py index 996f427..4f5b0d7 100644 --- a/python/insta_science/version.py +++ b/python/insta_science/version.py @@ -1,4 +1,4 @@ # Copyright 2024 Science project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/python/pyproject.toml b/python/pyproject.toml index 755ea2d..942454c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -132,7 +132,9 @@ type-check-313 = [ "mypy", "--python-version", "3.13", "insta_science", "scripts", "test-support", "tests" ] -release = ["python", "scripts/release.py"] +[tool.dev-cmd.commands.release] +args = ["python", "scripts/release.py"] +accepts-extra-args = true [tool.dev-cmd.commands.test] env = {"PYTHONPATH" = "../test-support"} diff --git a/python/scripts/release.py b/python/scripts/release.py index 9a12945..59231ec 100644 --- a/python/scripts/release.py +++ b/python/scripts/release.py @@ -5,11 +5,12 @@ import subprocess import sys +from argparse import ArgumentParser from dataclasses import dataclass from pathlib import Path from subprocess import CalledProcessError from textwrap import dedent -from typing import Any, Iterable, TextIO +from typing import Any, Iterable, NewType, TextIO import colors import marko @@ -31,6 +32,9 @@ RELEASE_HEADING_LEVEL = 2 +ReleaseError = NewType("ReleaseError", str) + + @dataclass(frozen=True) class Release: version: Version @@ -105,60 +109,80 @@ def tag_and_push_release() -> None: subprocess.run(args=["git", "push", "--tags", REMOTE, "HEAD:main"], check=True) -def main() -> Any: +def check_branch() -> ReleaseError | None: if (current_branch := branch()) != RELEASE_BRANCH: - return colors.yellow( - f"Aborted release since the current branch is {current_branch} and releases must be " - f"done from {RELEASE_BRANCH}." + return ReleaseError( + colors.yellow( + f"Aborted release since the current branch is {current_branch} and releases must " + f"be done from {RELEASE_BRANCH}." + ) ) if status := branch_status(): print(status) print("---") - return colors.yellow( - "Aborted release since the current branch is dirty with the status shown above." + return ReleaseError( + colors.yellow( + "Aborted release since the current branch is dirty with the status shown above." + ) ) + return None + + +def check_version_and_changelog() -> Release | ReleaseError: if existing_tag := release_tag_exists(): print(existing_tag) print("---") - return colors.yellow( - f"Aborted release since there is already a release tag for {__version__} shown above." + return ReleaseError( + colors.yellow( + f"Aborted release since there is already a release tag for {__version__} shown " + f"above." + ) ) release = parse_latest_release(CHANGELOG.read_text(), level=RELEASE_HEADING_LEVEL) if release is None or VERSION > release.version: heading = colors.red(f"There are no release notes for {__version__} in {CHANGELOG}!") - return dedent( - f"""\ - {heading} - - You need to add a level {RELEASE_HEADING_LEVEL} heading with the release version number - followed by the release notes for that version. - - For example: - ------------ - - {'#' * RELEASE_HEADING_LEVEL} {__version__} - - These are the {__version__} release notes... - """ + return ReleaseError( + dedent( + f"""\ + {heading} + + You need to add a level {RELEASE_HEADING_LEVEL} heading with the release version + number followed by the release notes for that version. + + For example: + ------------ + + {'#' * RELEASE_HEADING_LEVEL} {__version__} + + These are the {__version__} release notes... + """ + ) ) - elif VERSION < release.version: + + if VERSION < release.version: release.render(sys.stdout) print("---") - return colors.red( - f"The current version is {VERSION} which is older than the latest release of " - f"{release.version} recorded in {CHANGELOG} and shown above." + return ReleaseError( + colors.red( + f"The current version is {VERSION} which is older than the latest release of " + f"{release.version} recorded in {CHANGELOG} and shown above." + ) ) + return release + + +def finalize_release(release: Release) -> ReleaseError | None: release.render(sys.stdout) print("---") if ( "y" != input("Do you want to proceed with releasing the changes above? [y|N] ").strip().lower() ): - return colors.yellow("Aborted release at user request.") + return ReleaseError(colors.yellow("Aborted release at user request.")) print() tag_and_push_release() @@ -167,6 +191,33 @@ def main() -> Any: print() print("You can view release progress by visiting the latest job here:") print(" https://github.com/a-scie/science-installers/actions/workflows/python-release.yml") + return None + + +def main() -> Any: + parser = ArgumentParser() + parser.add_argument( + "-n", + "--dry-run", + action="store_true", + help="Instead of attempting a release, just check that the codebase is ready to release.", + ) + options = parser.parse_args() + + if not options.dry_run: + if branch_error := check_branch(): + return branch_error + + release_or_error = check_version_and_changelog() + if not isinstance(release_or_error, Release): + return release_or_error + + if not options.dry_run: + return finalize_release(release_or_error) + + release_or_error.render(sys.stdout) + print("---") + print(f"The changes above are releasable from a clean {RELEASE_BRANCH} branch.") if __name__ == "__main__": diff --git a/python/uv.lock b/python/uv.lock index 2cd7624..11325a5 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -88,7 +88,7 @@ wheels = [ [[package]] name = "dev-cmd" -version = "0.9.1" +version = "0.9.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aioconsole" }, @@ -96,9 +96,9 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/94/11e26c96f776be2213ab5cebebfca7cf4fc9e30c5947e736312a047a9450/dev_cmd-0.9.1.tar.gz", hash = "sha256:7ca9d015dd0341037843e87d8047b2b469cb3c07e959c4c1232b6124d32b22ae", size = 34902 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/13/0714020c249b8ce590ba74fe842a985e35ef1442d1fdc62bd93aa451b22f/dev_cmd-0.9.3.tar.gz", hash = "sha256:a658f24b4d1bd5d3e246892f21b9a17c6a9e2ac1377e0fc4bb5d49f651dbea50", size = 35081 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/ae/1f0d7fcdcca289b0aac80a45178142bc49cd00b928afea1c342e3a9f153a/dev_cmd-0.9.1-py3-none-any.whl", hash = "sha256:52c6bcbdc754af9ad9a8ec74a73f7285f6c885ddfbda0341a2f3705f46954297", size = 23661 }, + { url = "https://files.pythonhosted.org/packages/cd/72/01ee54c61056645d4177aade1242872262e2826bd7ebebbcbdea74c87d95/dev_cmd-0.9.3-py3-none-any.whl", hash = "sha256:ee025b8cf27781d10a8a662b53d3369dee08f97a259e7f833fd53b34ad75c8e3", size = 23883 }, ] [[package]] @@ -186,7 +186,7 @@ wheels = [ [[package]] name = "insta-science" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "ansicolors" },