diff --git a/pyproject.toml b/pyproject.toml index 32b9b09..498372a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ mypy = "^1.11" pylint = "^3.2" pre-commit = "^3.7.0" ruff = "^0.5" +semver = "^3.0.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/scripts/release.py b/scripts/release.py index d82a3c7..ba1aed7 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +"""Release management script for SignalRGB Home Assistant Integration.""" + +# pylint: disable=line-too-long,broad-exception-caught import argparse import json @@ -8,151 +11,153 @@ import sys from collections import OrderedDict -import colorama -from colorama import Fore, Style +import semver +from colorama import Fore, Style, init # Initialize colorama for cross-platform colored output -colorama.init(autoreset=True) +init(autoreset=True) # Constants PROJECT_NAME = "SignalRGB Home Assistant Integration" -PROJECT_LINK = "https://github.com/hyperb1iss/signalrgb-homeassistant" +REPO_NAME = "hyperb1iss/signalrgb-homeassistant" +PROJECT_LINK = f"https://github.com/{REPO_NAME}" ISSUE_TRACKER = f"{PROJECT_LINK}/issues" HASS_CONFIG_DIR = os.getenv( "HASS_CONFIG_DIR", os.path.expanduser("~/dev/ha_core/config") ) CUSTOM_COMPONENTS_DIR = os.path.join(HASS_CONFIG_DIR, "custom_components") - -def print_banner(version: str) -> None: - """Print a beautiful banner for the release script.""" - banner = f""" -{Fore.MAGENTA}โ•”{'โ•' * 60}โ•— -โ•‘ {Fore.CYAN}๐Ÿš€ {PROJECT_NAME} Release Manager {Fore.MAGENTA}โ•‘ -โ•‘ {Fore.YELLOW}Version: {version}{' ' * (49 - len(version))} {Fore.MAGENTA}โ•‘ -โ•š{'โ•' * 60}โ•{Style.RESET_ALL} +# Colorful ASCII Art Banner +LOGO = f""" +{Fore.CYAN} ๏ฝฅ ๏ฝก โ˜† โˆด๏ฝกใ€€ใ€€๏ฝฅ๏พŸ*๏ฝกโ˜…๏ฝฅ +{Fore.YELLOW} โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +{Fore.MAGENTA} โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ”‚ +{Fore.MAGENTA} โ”‚ โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•— โ”‚ +{Fore.MAGENTA} โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ”‚ +{Fore.MAGENTA} โ”‚ โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•— โ”‚ +{Fore.MAGENTA} โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ”‚ +{Fore.MAGENTA} โ”‚ โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ”‚ +{Fore.CYAN} โ”‚ Home Assistant Integration โ”‚ +{Fore.YELLOW} โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ +{Fore.CYAN} โˆด๏ฝกใ€€ใ€€๏ฝฅ๏พŸ*๏ฝกโ˜† Release Manager โ˜†๏ฝก*๏พŸ๏ฝฅใ€€ ๏ฝกโˆด +{Fore.YELLOW} ๏ฝฅ ๏ฝก โ˜† โˆด๏ฝกใ€€ใ€€๏ฝฅ๏พŸ*๏ฝกโ˜…๏ฝฅ """ - print(banner) -def print_message(message: str, color: str = Fore.GREEN) -> None: - """Print a colored message.""" - print(f"{color}{message}{Style.RESET_ALL}") +def print_logo(): + """Print the colorful ASCII art banner.""" + print(LOGO) + + +def print_step(step): + """Print a step message in blue.""" + print(Fore.BLUE + f"\nโœจ {step}" + Style.RESET_ALL) + + +def print_error(message): + """Print an error message in red.""" + print(Fore.RED + f"โŒ Error: {message}" + Style.RESET_ALL) -def check_tool_installed(tool_name: str) -> None: +def print_success(message): + """Print a success message in green.""" + print(Fore.GREEN + f"โœ… {message}" + Style.RESET_ALL) + + +def check_tool_installed(tool_name): """Check if a tool is installed.""" if shutil.which(tool_name) is None: - print_message( - f"โŒ {tool_name} is not installed. Please install it and try again.", - Fore.RED, - ) + print_error(f"{tool_name} is not installed. Please install it and try again.") sys.exit(1) -def copy_integration(src_path: str, dest_path: str, verbose: bool = False) -> None: - """Copy the integration files to the destination.""" +def get_current_version(): + """Get the current version from the manifest file.""" try: - if os.path.exists(dest_path): - shutil.rmtree(dest_path) - if verbose: - print_message(f"๐Ÿ—‘๏ธ Removed existing directory at {dest_path}", Fore.BLUE) - shutil.copytree(src_path, dest_path) - print_message(f"โœ… Copied integration from {src_path} to {dest_path}") - except OSError as e: - print_message(f"โŒ Error copying integration: {e}", Fore.RED) + manifest_path = os.path.join("custom_components", "signalrgb", "manifest.json") + with open(manifest_path, "r", encoding="utf-8") as file: + manifest = json.load(file) + return manifest.get("version") + except FileNotFoundError: + print_error("manifest.json not found.") + sys.exit(1) + except json.JSONDecodeError: + print_error("Invalid JSON in manifest.json.") sys.exit(1) -def update_manifest( - manifest_path: str, new_version: str, verbose: bool = False -) -> None: - """Update the manifest.json file with the new version and reorder entries.""" +def update_manifest(new_version): + """Update the version in the manifest file.""" + manifest_path = os.path.join("custom_components", "signalrgb", "manifest.json") try: - with open(manifest_path, "r", encoding="utf-8") as f: - manifest = json.load(f) + with open(manifest_path, "r", encoding="utf-8") as file: + manifest = json.load(file) manifest["version"] = new_version manifest["documentation"] = PROJECT_LINK manifest["issue_tracker"] = ISSUE_TRACKER ordered_manifest = OrderedDict( - [ - ("domain", manifest["domain"]), - ("name", manifest["name"]), - ] + [("domain", manifest["domain"]), ("name", manifest["name"])] + sorted( - [(k, v) for k, v in manifest.items() if k not in ["domain", "name"]], - key=lambda x: x[0], + [(k, v) for k, v in manifest.items() if k not in ["domain", "name"]] ) ) - with open(manifest_path, "w", encoding="utf-8") as f: - json.dump(ordered_manifest, f, indent=2) - f.write("\n") + with open(manifest_path, "w", encoding="utf-8") as file: + json.dump(ordered_manifest, file, indent=2) + file.write("\n") # Add a newline at the end of the file + print_success(f"Updated version in manifest.json to {new_version}") + except Exception as e: + print_error(f"Failed to update manifest: {str(e)}") + sys.exit(1) - print_message( - f"โœ… Updated manifest version to {new_version} and reordered entries" - ) - if verbose: - print_message( - f"๐Ÿ“„ New manifest: {json.dumps(ordered_manifest, indent=2)}", Fore.BLUE - ) - except (OSError, json.JSONDecodeError) as e: - print_message(f"โŒ Error updating manifest: {e}", Fore.RED) + +def copy_integration(src_path, dest_path): + """Copy the integration from source to destination.""" + try: + if os.path.exists(dest_path): + shutil.rmtree(dest_path) + shutil.copytree(src_path, dest_path) + print_success(f"Copied integration from {src_path} to {dest_path}") + except Exception as e: + print_error(f"Failed to copy integration: {str(e)}") sys.exit(1) -def git_commit_and_tag(version: str, verbose: bool = False) -> None: - """Commit changes and create a new tag.""" +def commit_and_push(version): + """Commit and push changes to the repository.""" + print_step("Committing and pushing changes") try: - commit_message = f"๐Ÿš€ Release version {version}" subprocess.run(["git", "add", "custom_components"], check=True) - subprocess.run(["git", "commit", "-m", commit_message], check=True) subprocess.run( - ["git", "tag", "-a", f"v{version}", "-m", f"Version {version}"], check=True + ["git", "commit", "-m", f":rocket: Release version {version}"], check=True ) - print_message(f"โœ… Changes committed and tagged as v{version}") - if verbose: - print_message(f"๐Ÿ”– Created tag: v{version}", Fore.BLUE) + subprocess.run(["git", "push"], check=True) + subprocess.run(["git", "tag", f"v{version}"], check=True) + subprocess.run(["git", "push", "--tags"], check=True) + print_success(f"Changes committed and pushed for version {version}") except subprocess.CalledProcessError as e: - print_message(f"โŒ Error committing/tagging: {e}", Fore.RED) + print_error(f"Git operations failed: {str(e)}") sys.exit(1) -def update_hass(src_path: str, verbose: bool = False) -> None: - """Update Home Assistant integration.""" +def update_hass(): + """Update the Home Assistant integration.""" + print_step("Updating Home Assistant integration") src_path = os.path.join(os.getcwd(), "custom_components", "signalrgb") dest_path = os.path.join(CUSTOM_COMPONENTS_DIR, "signalrgb") - - print_message( - f"๐Ÿ”„ Updating Home Assistant integration from {src_path} to {dest_path}", - Fore.BLUE, - ) - copy_integration(src_path, dest_path, verbose) - print_message( - "โš ๏ธ Remember to reload Home Assistant to apply the changes.", Fore.YELLOW - ) - - -def do_release(src_path: str, version: str, verbose: bool = False) -> None: - """Perform the release process.""" - print_banner(version) - manifest_path = os.path.join(src_path, "manifest.json") - - update_manifest(manifest_path, version, verbose) - git_commit_and_tag(version, verbose) - - print_message( - f"\n๐ŸŽ‰ Release [{version}] process completed successfully!", Fore.CYAN - ) - print_message( - "โš ๏ธ Don't forget to push the changes and the new tag to GitHub.", Fore.YELLOW + copy_integration(src_path, dest_path) + print_success("Home Assistant integration updated") + print( + Fore.YELLOW + + "โš ๏ธ Remember to reload Home Assistant to apply the changes." + + Style.RESET_ALL ) -def main() -> None: - """Main function to handle argument parsing and command execution.""" +def main(): + """Main function to handle command-line arguments and execute the appropriate commands.""" parser = argparse.ArgumentParser( description=f"Release management for {PROJECT_NAME}" ) @@ -162,29 +167,45 @@ def main() -> None: parser.add_argument( "version", nargs="?", - help="Version number for the release (required for release command)", - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="Enable verbose output" + help="Version number for release (required for release command)", ) - args = parser.parse_args() - # Paths - src_path = os.path.join(os.getcwd(), "custom_components", "signalrgb") + print_logo() + print_step(f"Starting {args.command} process") - # Check for necessary tools check_tool_installed("git") if args.command == "update-hass": - update_hass(src_path, args.verbose) + update_hass() elif args.command == "release": if not args.version: - print_message( - "โŒ Version number is required for release command.", Fore.RED - ) + print_error("Version number is required for the release command.") + sys.exit(1) + + try: + semver.parse(args.version) + except ValueError: + print_error("Invalid semantic version.") sys.exit(1) - do_release(src_path, args.version, args.verbose) + + current_version = get_current_version() + print(Fore.CYAN + f"Current version: {current_version}" + Style.RESET_ALL) + print(Fore.MAGENTA + f"New version: {args.version}" + Style.RESET_ALL) + + update_manifest(args.version) + commit_and_push(args.version) + + print( + Fore.GREEN + + f"\n๐ŸŽ‰โœจ {PROJECT_NAME} v{args.version} has been successfully prepared for release! โœจ๐ŸŽ‰" + + Style.RESET_ALL + ) + print( + Fore.YELLOW + + "Note: The GitHub release will be created by CI." + + Style.RESET_ALL + ) if __name__ == "__main__":