#!/usr/bin/env python3 """Helper script to bump the current version.""" import argparse from pathlib import Path import re import subprocess from packaging.version import Version from homeassistant import const from homeassistant.util import dt as dt_util def _bump_release(release, bump_type): """Bump a release tuple consisting of 3 numbers.""" major, minor, patch = release if bump_type == "patch": patch += 1 elif bump_type == "minor": minor += 1 patch = 0 return major, minor, patch def bump_version( version: Version, bump_type: str, *, nightly_version: str | None = None ) -> Version: """Return a new version given a current version and action.""" to_change = {} if bump_type == "minor": # Convert 0.67.3 to 0.68.0 # Convert 0.67.3.b5 to 0.68.0 # Convert 0.67.3.dev0 to 0.68.0 # Convert 0.67.0.b5 to 0.67.0 # Convert 0.67.0.dev0 to 0.67.0 to_change["dev"] = None to_change["pre"] = None if not version.is_prerelease or version.release[2] != 0: to_change["release"] = _bump_release(version.release, "minor") elif bump_type == "patch": # Convert 0.67.3 to 0.67.4 # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 to_change["dev"] = None to_change["pre"] = None if not version.is_prerelease: to_change["release"] = _bump_release(version.release, "patch") elif bump_type == "dev": # Convert 0.67.3 to 0.67.4.dev0 # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 if version.is_devrelease: to_change["dev"] = ("dev", version.dev + 1) else: to_change["pre"] = ("dev", 0) to_change["release"] = _bump_release(version.release, "minor") elif bump_type == "beta": # Convert 0.67.5 to 0.67.6b0 # Convert 0.67.0.dev0 to 0.67.0b0 # Convert 0.67.5.b4 to 0.67.5b5 if version.is_devrelease: to_change["dev"] = None to_change["pre"] = ("b", 0) elif version.is_prerelease: if version.pre[0] == "a": to_change["pre"] = ("b", 0) if version.pre[0] == "b": to_change["pre"] = ("b", version.pre[1] + 1) else: to_change["pre"] = ("b", 0) to_change["release"] = _bump_release(version.release, "patch") else: to_change["release"] = _bump_release(version.release, "patch") to_change["pre"] = ("b", 0) elif bump_type == "nightly": # Convert 0.70.0d0 to 0.70.0d201904241254, fails when run on non dev release if not version.is_devrelease: raise ValueError("Can only be run on dev release") new_dev = dt_util.utcnow().strftime("%Y%m%d%H%M") if nightly_version: new_version = Version(nightly_version) if new_version.release != version.release: raise ValueError("Nightly version must have the same release version") if not new_version.is_devrelease: raise ValueError("Nightly version must be a dev version") new_dev = new_version.dev to_change["dev"] = ("dev", new_dev) else: raise ValueError(f"Unsupported type: {bump_type}") temp = Version("0") temp._version = version._version._replace(**to_change) # noqa: SLF001 return Version(str(temp)) def write_version(version): """Update Home Assistant constant file with new version.""" content = Path("homeassistant/const.py").read_text() major, minor, patch = str(version).split(".", 2) content = re.sub( "MAJOR_VERSION: Final = .*\n", f"MAJOR_VERSION: Final = {major}\n", content ) content = re.sub( "MINOR_VERSION: Final = .*\n", f"MINOR_VERSION: Final = {minor}\n", content ) content = re.sub( "PATCH_VERSION: Final = .*\n", f'PATCH_VERSION: Final = "{patch}"\n', content ) Path("homeassistant/const.py").write_text(content) def write_version_metadata(version: Version) -> None: """Update pyproject.toml file with new version.""" content = Path("pyproject.toml").read_text(encoding="utf8") content = re.sub(r"(version\W+=\W).+\n", f'\\g<1>"{version}"\n', content, count=1) Path("pyproject.toml").write_text(content, encoding="utf8") def write_ci_workflow(version: Version) -> None: """Update ci workflow with new version.""" content = Path(".github/workflows/ci.yaml").read_text() short_version = ".".join(str(version).split(".", maxsplit=2)[:2]) content = re.sub( r"(\n\W+HA_SHORT_VERSION: )\"\d{4}\.\d{1,2}\"\n", f'\\g<1>"{short_version}"\n', content, count=1, ) Path(".github/workflows/ci.yaml").write_text(content) def main() -> None: """Execute script.""" parser = argparse.ArgumentParser(description="Bump version of Home Assistant") parser.add_argument( "type", help="The type of the bump the version to.", choices=["beta", "dev", "patch", "minor", "nightly"], ) parser.add_argument( "--commit", action="store_true", help="Create a version bump commit." ) parser.add_argument( "--set-nightly-version", help="Set the nightly version to", type=str ) arguments = parser.parse_args() if arguments.set_nightly_version and arguments.type != "nightly": parser.error("--set-nightly-version requires type set to nightly.") if ( arguments.commit and subprocess.run(["git", "diff", "--quiet"], check=False).returncode == 1 ): print("Cannot use --commit because git is dirty.") return current = Version(const.__version__) bumped = bump_version( current, arguments.type, nightly_version=arguments.set_nightly_version ) assert bumped > current, "BUG! New version is not newer than old version" write_version(bumped) write_version_metadata(bumped) write_ci_workflow(bumped) print(bumped) if not arguments.commit: return subprocess.run(["git", "commit", "-nam", f"Bump version to {bumped}"], check=True) def test_bump_version() -> None: """Make sure it all works.""" import pytest assert bump_version(Version("0.56.0"), "beta") == Version("0.56.1b0") assert bump_version(Version("0.56.0b3"), "beta") == Version("0.56.0b4") assert bump_version(Version("0.56.0.dev0"), "beta") == Version("0.56.0b0") assert bump_version(Version("0.56.3"), "dev") == Version("0.57.0.dev0") assert bump_version(Version("0.56.0b3"), "dev") == Version("0.57.0.dev0") assert bump_version(Version("0.56.0.dev0"), "dev") == Version("0.56.0.dev1") assert bump_version(Version("0.56.3"), "patch") == Version("0.56.4") assert bump_version(Version("0.56.3.b3"), "patch") == Version("0.56.3") assert bump_version(Version("0.56.0.dev0"), "patch") == Version("0.56.0") assert bump_version(Version("0.56.0"), "minor") == Version("0.57.0") assert bump_version(Version("0.56.3"), "minor") == Version("0.57.0") assert bump_version(Version("0.56.0.b3"), "minor") == Version("0.56.0") assert bump_version(Version("0.56.3.b3"), "minor") == Version("0.57.0") assert bump_version(Version("0.56.0.dev0"), "minor") == Version("0.56.0") assert bump_version(Version("0.56.2.dev0"), "minor") == Version("0.57.0") now = dt_util.utcnow().strftime("%Y%m%d%H%M") assert bump_version(Version("0.56.0.dev0"), "nightly") == Version( f"0.56.0.dev{now}" ) assert bump_version( Version("2024.4.0.dev20240327"), "nightly", nightly_version="2024.4.0.dev202403271315", ) == Version("2024.4.0.dev202403271315") with pytest.raises(ValueError, match="Can only be run on dev release"): bump_version(Version("0.56.0"), "nightly") with pytest.raises( ValueError, match="Nightly version must have the same release version" ): bump_version( Version("0.56.0.dev0"), "nightly", nightly_version="2024.4.0.dev202403271315", ) with pytest.raises(ValueError, match="Nightly version must be a dev version"): bump_version(Version("0.56.0.dev0"), "nightly", nightly_version="0.56.0") if __name__ == "__main__": main()