blumeops/mise-tasks/service-review
Erich Blume cb9a06bb75
All checks were successful
Deploy Fly.io Proxy / deploy (push) Successful in 1m30s
Update tooling dependencies (Feb 2026 cycle) (#254)
## Summary

Monthly tooling dependency update cycle:

- **Pre-commit hooks**: trufflehog v3.92.5→v3.93.4, ruff v0.14.13→v0.15.2, shellcheck v0.10.0.1→v0.11.0.1, prettier v3.8.0→v3.8.1, actionlint v1.7.10→v1.7.11
- **Fly.io Dockerfile**: pin nginx to 1.28.2-alpine (was unpinned), bump alloy v1.5.1→v1.13.1
- **Mise tasks**: normalize httpx lower bound to >=0.28.0 and typer to >=0.15.0 across all scripts
- **Forgejo workflows**: actions/checkout@v4 is current, no changes needed
- **New how-to doc**: [[update-tooling-dependencies]] documenting this monthly cycle

## No changes needed

- pre-commit-hooks v6.0.0, yamllint v1.38.0, shfmt v3.12.0-2, taplo v0.9.3, ansible-lint 26.1.1 — all already at latest

## Test plan

- [x] `uvx pre-commit run --all-files` — all 24 hooks pass
- [ ] Fly.io deploy (triggered automatically on merge to main via deploy-fly workflow)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/254
2026-02-23 13:08:41 -08:00

207 lines
7.1 KiB
Text
Executable file

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["pyyaml>=6.0", "rich>=13.0.0", "typer>=0.15.0"]
# ///
#MISE description="Review the most stale service for version freshness"
#USAGE flag "--limit <limit>" default="15" help="Number of services to show in the table"
#USAGE flag "--type <type>" help="Filter by service type (argocd, ansible, nixos)"
"""Review the most stale service for version freshness.
Reads ``docs/reference/services/service-versions.yaml`` and sorts services
by the ``last-reviewed`` field. Services without the field (or null) are
treated as never-reviewed and float to the top. Displays a staleness table
and then shows the most stale service with a review checklist.
After reviewing, update the service entry in the YAML file:
last-reviewed: YYYY-MM-DD
current-version: "x.y.z"
Usage: mise run service-review [-- --limit 15] [-- --type argocd]
"""
import sys
from datetime import date
from pathlib import Path
from typing import Annotated
import typer
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
VERSIONS_FILE = Path(__file__).parent.parent / "service-versions.yaml"
def load_services(path: Path) -> list[dict]:
"""Load services from the YAML tracking file."""
data = yaml.safe_load(path.read_text())
return data.get("services", [])
def parse_date(raw) -> date | None:
"""Parse a date value from YAML."""
if raw is None:
return None
if isinstance(raw, date):
return raw
try:
return date.fromisoformat(str(raw))
except ValueError:
return None
def main(
limit: Annotated[int, typer.Option(help="Number of services to show in the table")] = 15,
type: Annotated[str | None, typer.Option(help="Filter by service type (argocd, ansible, nixos)")] = None,
) -> None:
console = Console()
today = date.today()
if not VERSIONS_FILE.exists():
console.print(f"[bold red]Tracking file not found:[/bold red] {VERSIONS_FILE}")
raise typer.Exit(code=1)
services = load_services(VERSIONS_FILE)
if type:
services = [s for s in services if s.get("type") == type]
if not services:
console.print(f"[bold red]No services found with type '{type}'[/bold red]")
raise typer.Exit(code=1)
# Parse dates and build sortable entries
entries: list[tuple[dict, date | None]] = []
for svc in services:
reviewed = parse_date(svc.get("last-reviewed"))
entries.append((svc, reviewed))
# Sort: never-reviewed first (None), then oldest reviewed
entries.sort(key=lambda e: (e[1] is not None, e[1] or date.min))
never_reviewed = sum(1 for _, r in entries if r is None)
type_label = f" ({type})" if type else ""
# --- Summary panel ---
console.print()
console.print(Panel(
f"[bold]{len(entries)}[/bold] total services{type_label}, "
f"[bold red]{never_reviewed}[/bold red] never reviewed",
title="[bold]Service Review Queue[/bold]",
border_style="cyan",
))
console.print()
# --- Staleness table ---
table = Table(show_header=True, header_style="bold")
table.add_column("#", justify="right")
table.add_column("Service")
table.add_column("Type", justify="center")
table.add_column("Version")
table.add_column("Last Reviewed", justify="right")
table.add_column("Age (days)", justify="right")
for i, (svc, reviewed) in enumerate(entries[:limit], 1):
name = svc["name"]
svc_type = svc.get("type", "?")
version = svc.get("current-version") or "—"
if reviewed is None:
table.add_row(
str(i),
f"[red]{name}[/red]",
svc_type,
f"[dim]{version}[/dim]",
"[red]never[/red]",
"[red]—[/red]",
)
else:
age = (today - reviewed).days
style = "yellow" if age > 90 else ""
name_str = f"[{style}]{name}[/{style}]" if style else name
date_str = f"[{style}]{reviewed}[/{style}]" if style else str(reviewed)
age_str = f"[{style}]{age}[/{style}]" if style else str(age)
table.add_row(str(i), name_str, svc_type, version, date_str, age_str)
remaining = len(entries) - limit
if remaining > 0:
table.add_row("", f"[dim]… {remaining} more[/dim]", "", "", "", "")
console.print(table)
console.print()
# --- Show the most stale service ---
if not entries:
console.print("[bold red]No services found![/bold red]")
raise typer.Exit(code=1)
top_svc, top_reviewed = entries[0]
upstream = top_svc.get("upstream-source") or "N/A"
notes = top_svc.get("notes") or ""
detail_lines = [
f"[bold cyan]{top_svc['name']}[/bold cyan] [dim]({top_svc.get('type', '?')})[/dim]",
f"[dim]Last reviewed: {top_reviewed or 'never'}[/dim]",
f"[dim]Current version: {top_svc.get('current-version') or 'unknown'}[/dim]",
f"[dim]Upstream: {upstream}[/dim]",
]
if notes:
detail_lines.append(f"[dim]Notes: {notes}[/dim]")
console.print(Panel(
"\n".join(detail_lines),
title="[bold]Up For Review[/bold]",
border_style="green",
))
console.print()
# --- Review checklist ---
checklist_parts = [
"[bold]Version Check:[/bold]\n",
f"• Check upstream releases: {upstream}\n",
"• Compare upstream latest to current deployed version\n",
"• Review changelog for breaking changes or security fixes\n",
]
svc_type = top_svc.get("type", "")
if svc_type == "argocd":
checklist_parts += [
"\n[bold]ArgoCD Deployment:[/bold]\n",
"• Update image tag or Helm chart version in argocd/manifests/\n",
f"• Verify sync status: argocd app get {top_svc['name']}\n",
]
elif svc_type == "ansible":
checklist_parts += [
"\n[bold]Ansible Deployment:[/bold]\n",
f"• Check role vars for version pins: ansible/roles/{top_svc['name']}/\n",
f"• Dry run: mise run provision-indri -- --tags {top_svc['name']} --check --diff\n",
]
elif svc_type == "nixos":
checklist_parts += [
"\n[bold]NixOS Deployment:[/bold]\n",
"• Version tracks nixpkgs via flake.lock\n",
"• Update: dagger call flake-update --src=. export --path=nixos/ringtail/flake.lock\n",
"• Deploy: mise run provision-ringtail\n",
]
checklist_parts += [
"\n[bold]Health Check:[/bold]\n",
"• Verify the service is running and healthy\n",
"• Check logs for errors or warnings\n",
"\n[bold]After Review:[/bold]\n",
"• Update the tracking file: [cyan]docs/reference/services/service-versions.yaml[/cyan]\n",
f"• Set [cyan]last-reviewed: {today}[/cyan] and [cyan]current-version[/cyan]\n",
"• Commit the change (along with any upgrades)",
]
console.print(Panel(
"".join(checklist_parts),
title="[bold yellow]Review Guidance[/bold yellow]",
border_style="yellow",
))
if __name__ == "__main__":
typer.run(main)