Skip to content

Commit

Permalink
🎨 Full RGB release management script
Browse files Browse the repository at this point in the history
Improve the release management script with better styling and functionality:

- Add colorful ASCII art banner with gradient
- Implement more robust error handling
- Enhance user feedback with colored output
- Improve code structure and readability
- Update manifest handling with ordered dict
- Add semver dependency for version validation
  • Loading branch information
hyperb1iss committed Aug 15, 2024
1 parent f5ef452 commit 4c54025
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 52 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pylint = "^3.2"
pre-commit = "^3.7.0"
ruff = "^0.5"
semver = "^3.0.2"
wcwidth = "^0.2.13"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
226 changes: 174 additions & 52 deletions scripts/release.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
#!/usr/bin/env python3
"""Release management script for SignalRGB Home Assistant Integration."""

# pylint: disable=line-too-long,broad-exception-caught
# ruff: noqa: E501
# pylint: disable=broad-exception-caught, line-too-long

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from collections import OrderedDict
from typing import List, Tuple

import semver
from colorama import Fore, Style, init
from colorama import Style, init
from wcwidth import wcswidth

# Initialize colorama for cross-platform colored output
init(autoreset=True)
Expand All @@ -27,57 +31,164 @@
)
CUSTOM_COMPONENTS_DIR = os.path.join(HASS_CONFIG_DIR, "custom_components")

# 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} ・ 。 ☆ ∴。  ・゚*。★・
"""
# ANSI Color Constants
COLOR_RESET = Style.RESET_ALL
COLOR_BORDER = "\033[38;2;75;0;130m"
COLOR_STAR = "\033[38;2;255;255;0m"
COLOR_ERROR = "\033[38;2;255;0;0m"
COLOR_SUCCESS = "\033[38;2;50;205;50m"
COLOR_BUILD_SUCCESS = "\033[38;2;255;215;0m"
COLOR_VERSION_PROMPT = "\033[38;2;147;112;219m"
COLOR_STEP = "\033[38;2;255;0;130m"
COLOR_WARNING = "\033[38;2;255;165;0m"

# Gradient colors for the banner
GRADIENT_COLORS = [
(255, 0, 0),
(0, 0, 255),
(0, 255, 0),
(0, 0, 255),
(255, 0, 0),
]

def print_logo():
"""Print the colorful ASCII art banner."""
print(LOGO)

def print_colored(message: str, color: str) -> None:
"""Print a message with a specific color."""
print(f"{color}{message}{COLOR_RESET}")

def print_step(step):
"""Print a step message in blue."""
print(Fore.BLUE + f"\n{step}" + Style.RESET_ALL)

def print_step(step: str) -> None:
"""Print a step in the process with a specific color."""
print_colored(f"\n{step}", COLOR_STEP)

def print_error(message):
"""Print an error message in red."""
print(Fore.RED + f"❌ Error: {message}" + Style.RESET_ALL)

def print_error(message: str) -> None:
"""Print an error message with a specific color."""
print_colored(f"❌ Error: {message}", COLOR_ERROR)

def print_success(message):
"""Print a success message in green."""
print(Fore.GREEN + f"✅ {message}" + Style.RESET_ALL)

def print_success(message: str) -> None:
"""Print a success message with a specific color."""
print_colored(f"✅ {message}", COLOR_SUCCESS)

def check_tool_installed(tool_name):

def print_warning(message: str) -> None:
"""Print a warning message with a specific color."""
print_colored(f"⚠️ {message}", COLOR_WARNING)


def generate_gradient(colors: List[Tuple[int, int, int]], steps: int) -> List[str]:
"""Generate a list of color codes for a smooth multi-color gradient."""
gradient = []
segments = len(colors) - 1
steps_per_segment = max(1, steps // segments)

for i in range(segments):
start_color = colors[i]
end_color = colors[i + 1]
for j in range(steps_per_segment):
t = j / steps_per_segment
r = int(start_color[0] * (1 - t) + end_color[0] * t)
g = int(start_color[1] * (1 - t) + end_color[1] * t)
b = int(start_color[2] * (1 - t) + end_color[2] * t)
gradient.append(f"\033[38;2;{r};{g};{b}m")

return gradient


def strip_ansi(text: str) -> str:
"""Remove ANSI color codes from a string."""
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
return ansi_escape.sub("", text)


def apply_gradient(text: str, gradient: List[str], line_number: int) -> str:
"""Apply gradient colors diagonally to text."""
return "".join(
f"{gradient[(i + line_number) % len(gradient)]}{char}"
for i, char in enumerate(text)
)


def center_text(text: str, width: int) -> str:
"""Center text, accounting for ANSI color codes and Unicode widths."""
visible_length = wcswidth(strip_ansi(text))
padding = (width - visible_length) // 2
return f"{' ' * padding}{text}{' ' * (width - padding - visible_length)}"


def center_block(block: List[str], width: int) -> List[str]:
"""Center a block of text within a given width."""
return [center_text(line, width) for line in block]


def create_banner() -> str:
"""Create a FULL RGB banner with diagonal gradient."""
banner_width = 80
content_width = banner_width - 4 # Accounting for border characters
cosmic_gradient = generate_gradient(GRADIENT_COLORS, banner_width)

logo = [
"███████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██╗ ██████╗ ██████╗ ██████╗ ",
"██╔════╝██║██╔════╝ ████╗ ██║██╔══██╗██║ ██╔══██╗██╔════╝ ██╔══██╗",
"███████╗██║██║ ███╗██╔██╗ ██║███████║██║ ██████╔╝██║ ███╗██████╔╝",
"╚════██║██║██║ ██║██║╚██╗██║██╔══██║██║ ██╔══██╗██║ ██║██╔══██╗",
"███████║██║╚██████╔╝██║ ╚████║██║ ██║███████╗██║ ██║╚██████╔╝██████╔╝",
"╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ",
center_text("🏠 Home Assistant Integration 🏠", content_width),
]

centered_logo = center_block(logo, content_width)

banner = [
center_text(f"{COLOR_STAR}・ 。 ☆ ∴。  ・゚*。★・ ∴。  ・゚*。☆ ・ 。 ☆ ∴。", banner_width),
f"{COLOR_BORDER}{'─' * (banner_width - 2)}╮",
]

for line_number, line in enumerate(centered_logo):
gradient_line = apply_gradient(line, cosmic_gradient, line_number)
banner.append(f"{COLOR_BORDER}{gradient_line} {COLOR_BORDER}│")

release_manager_text = COLOR_STEP + "Release Manager"

banner.extend(
[
f"{COLOR_BORDER}{'─' * (banner_width - 2)}╯",
center_text(
f"{COLOR_STAR}∴。  ・゚*。☆ {release_manager_text}{COLOR_STAR} ☆。*゚・  。∴",
banner_width,
),
center_text(
f"{COLOR_STAR}・ 。 ☆ ∴。  ・゚*。★・ ∴。  ・゚*。☆ ・ 。 ☆ ∴。", banner_width
),
]
)

return "\n".join(banner)


def print_logo() -> None:
"""Print the banner/logo for the release manager."""
print(create_banner())


def check_tool_installed(tool_name: str) -> None:
"""Check if a tool is installed."""
if shutil.which(tool_name) is None:
print_error(f"{tool_name} is not installed. Please install it and try again.")
sys.exit(1)


def get_current_version():
def get_current_version() -> str:
"""Get the current version from the manifest file."""
try:
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")
version = manifest.get("version")
if version is None or not isinstance(version, str):
raise ValueError("Version not found or invalid in manifest.json")
return str(version)
except FileNotFoundError:
print_error("manifest.json not found.")
sys.exit(1)
Expand All @@ -86,7 +197,7 @@ def get_current_version():
sys.exit(1)


def update_manifest(new_version):
def update_manifest(new_version: str) -> None:
"""Update the version in the manifest file."""
manifest_path = os.path.join("custom_components", "signalrgb", "manifest.json")
try:
Expand All @@ -113,7 +224,7 @@ def update_manifest(new_version):
sys.exit(1)


def update_pyproject_toml(new_version):
def update_pyproject_toml(new_version: str) -> None:
"""Update the version in pyproject.toml."""
pyproject_path = "pyproject.toml"
try:
Expand All @@ -136,7 +247,7 @@ def update_pyproject_toml(new_version):
sys.exit(1)


def copy_integration(src_path, dest_path):
def copy_integration(src_path: str, dest_path: str) -> None:
"""Copy the integration from source to destination."""
try:
if os.path.exists(dest_path):
Expand All @@ -148,7 +259,7 @@ def copy_integration(src_path, dest_path):
sys.exit(1)


def commit_and_push(version):
def commit_and_push(version: str) -> None:
"""Commit and push changes to the repository."""
print_step("Committing and pushing changes")
try:
Expand All @@ -167,21 +278,32 @@ def commit_and_push(version):
sys.exit(1)


def update_hass():
def update_hass() -> None:
"""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")
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
print_colored(
"⚠️ Remember to reload Home Assistant to apply the changes.",
COLOR_BUILD_SUCCESS,
)


def main():
def confirm_release(new_version: str) -> bool:
"""Prompt the user to confirm the release."""
print_warning(f"You are about to release version {new_version} of {PROJECT_NAME}.")
print_warning(
"This action will update the manifest, commit changes, and push to the repository."
)
confirmation = input(
f"{COLOR_VERSION_PROMPT}Are you sure you want to proceed? (y/N): {COLOR_RESET}"
).lower()
return confirmation == "y"


def main() -> None:
"""Main function to handle command-line arguments and execute the appropriate commands."""
parser = argparse.ArgumentParser(
description=f"Release management for {PROJECT_NAME}"
Expand Down Expand Up @@ -215,22 +337,22 @@ def main():
sys.exit(1)

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)
print_colored(f"Current version: {current_version}", COLOR_STEP)
print_colored(f"New version: {args.version}", COLOR_VERSION_PROMPT)

if not confirm_release(args.version):
print_warning("Release process aborted.")
sys.exit(0)

update_manifest(args.version)
update_pyproject_toml(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_success(
f"\n🎉✨ {PROJECT_NAME} v{args.version} has been successfully prepared for release! ✨🎉"
)
print(
Fore.YELLOW
+ "Note: The GitHub release will be created by CI."
+ Style.RESET_ALL
print_colored(
"Note: The GitHub release will be created by CI.", COLOR_BUILD_SUCCESS
)


Expand Down

0 comments on commit 4c54025

Please sign in to comment.