adaptive-lighting/scripts/update-test-matrix.py

147 lines
4.7 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""Update the pytest workflow matrix with latest HA Core versions.
This script fetches the latest Home Assistant Core release versions from GitHub
and updates the pytest workflow matrix to test against them.
Usage:
python scripts/update-test-matrix.py
"""
from __future__ import annotations
import json
import re
import urllib.request
from pathlib import Path
# Minimum HA Core version to include in the test matrix
# This should be the oldest version we want to support
MIN_VERSION = (2024, 12)
def get_ha_core_versions() -> list[str]:
"""Fetch latest stable HA Core versions from GitHub API."""
all_tags = []
page = 1
# Paginate through all tags to ensure we get older versions too
while True:
url = f"https://api.github.com/repos/home-assistant/core/tags?per_page=100&page={page}"
with urllib.request.urlopen(url) as response: # noqa: S310
tags = json.loads(response.read().decode())
if not tags:
break
all_tags.extend(tags)
# Check if we've gone far enough back
# Stop if we've found versions older than our minimum
oldest_in_page = None
for t in tags:
if re.match(r"^\d+\.\d+\.\d+$", t["name"]):
parts = t["name"].split(".")
year, month = int(parts[0]), int(parts[1])
if oldest_in_page is None or (year, month) < oldest_in_page:
oldest_in_page = (year, month)
if oldest_in_page and oldest_in_page < MIN_VERSION:
break
page += 1
if page > 10: # Safety limit
break
# Filter to stable releases only (no beta/rc)
stable_pattern = re.compile(r"^\d+\.\d+\.\d+$")
versions = [t["name"] for t in all_tags if stable_pattern.match(t["name"])]
# Group by minor version and get latest patch for each
latest: dict[str, str] = {}
for version in versions:
parts = version.split(".")
year, month = int(parts[0]), int(parts[1])
# Only include versions >= MIN_VERSION
if (year, month) >= MIN_VERSION:
minor_key = f"{parts[0]}.{parts[1]}"
# Keep the one with highest patch number
if minor_key not in latest:
latest[minor_key] = version
else:
current_patch = int(latest[minor_key].split(".")[2])
new_patch = int(parts[2])
if new_patch > current_patch:
latest[minor_key] = version
# Sort by version
return sorted(latest.values(), key=lambda v: [int(x) for x in v.split(".")])
def get_python_version(ha_version: str) -> str:
"""Determine Python version based on HA Core version."""
parts = ha_version.split(".")
year, month = int(parts[0]), int(parts[1])
# 2024.x and 2025.1 use Python 3.12, 2025.2+ use Python 3.13
if year == 2024 or (year == 2025 and month == 1):
return "3.12"
return "3.13"
def generate_matrix_yaml(versions: list[str]) -> str:
"""Generate the YAML matrix include block."""
lines = []
for version in versions:
python_ver = get_python_version(version)
lines.append(f' - core-version: "{version}"')
lines.append(f' python-version: "{python_ver}"')
# Add dev version
lines.append(' - core-version: "dev"')
lines.append(' python-version: "3.13"')
return "\n".join(lines)
def update_workflow_file(workflow_path: Path, new_matrix: str) -> bool:
"""Update the workflow file with new matrix. Returns True if changed."""
content = workflow_path.read_text()
# Pattern to match the matrix include block
# Matches from "include:" to just before " steps:"
pattern = re.compile(
r"( include:\n)(.*?)( steps:)",
re.DOTALL,
)
def replacer(match: re.Match) -> str:
return f"{match.group(1)}{new_matrix}\n{match.group(3)}"
new_content = pattern.sub(replacer, content)
if new_content == content:
return False
workflow_path.write_text(new_content)
return True
def main() -> None:
"""Main entry point."""
print("Fetching latest HA Core versions...") # noqa: T201
versions = get_ha_core_versions()
print(f"Found {len(versions)} versions: {', '.join(versions)}") # noqa: T201
print("\nGenerating matrix...") # noqa: T201
matrix = generate_matrix_yaml(versions)
print(matrix) # noqa: T201
workflow_path = Path(__file__).parent.parent / ".github/workflows/pytest.yaml"
print(f"\nUpdating {workflow_path}...") # noqa: T201
if update_workflow_file(workflow_path, matrix):
print("Workflow updated successfully!") # noqa: T201
else:
print("No changes needed.") # noqa: T201
if __name__ == "__main__":
main()