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

148 lines
4.8 KiB
Python
Executable file

#!/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
# Keep the latest stable release month and the preceding 12 monthly release lines.
# Update this explicit floor when dropping another supported release month.
MIN_VERSION = (2025, 9)
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:
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])
# 2025.9 through 2026.2 use Python 3.13.
# 2026.3+ uses Python 3.14.
if year > 2026 or (year == 2026 and month >= 3):
return "3.14.2"
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.14.2"')
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()