Skip to content

Commit

Permalink
📦 Update release management script
Browse files Browse the repository at this point in the history
Enhance release.py with improved functionality and styling

- Add semver dependency for version validation
- Implement colorful ASCII art banner
- Improve error handling and user feedback
- Streamline git operations for release process
- Update manifest handling with ordered dict
- Enhance overall script structure and readability
  • Loading branch information
hyperb1iss committed Aug 14, 2024
1 parent 003ffe3 commit e6bb77c
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 105 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
231 changes: 126 additions & 105 deletions scripts/release.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}"
)
Expand All @@ -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__":
Expand Down

0 comments on commit e6bb77c

Please sign in to comment.