Add service version review system (#196)

## Summary

- Add `service-versions.yaml` tracking file with 33 services and upstream release URLs
- Add `mise run service-review` task (Python uv script) mirroring the docs-review UX
- Add `review-services` how-to article covering the review process by service type
- Add `[[review-services]]` link to the how-to index Knowledge Base table

## Deployment and Testing

- [x] `mise run service-review` displays 33 services, all "never reviewed"
- [x] `mise run service-review -- --type ansible` filters to 7 Ansible services
- [x] `mise run service-review -- --limit 5` shows 5 rows
- [x] `mise run docs-check-links` — no broken wiki-links
- [x] `mise run docs-check-frontmatter` — new doc passes validation
- [x] All pre-commit hooks pass

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/196
This commit is contained in:
Erich Blume 2026-02-16 17:02:56 -08:00
commit faf9682b55
8 changed files with 536 additions and 0 deletions

View file

@ -0,0 +1 @@
Add service version review system with `mise run service-review` task, tracking file, and how-to guide.

View file

@ -136,8 +136,17 @@ For metrics collection, create a companion `<role>_metrics` role that:
See [[alloy]] for how metrics are collected from textfiles.
## Checklist
- [ ] Role created in `ansible/roles/<role>/`
- [ ] Role added to `ansible/playbooks/indri.yml` with tag
- [ ] Secrets wired via pre_tasks (if needed)
- [ ] Dry run passes: `mise run provision-indri -- --tags <role> --check --diff`
- [ ] Service added to `service-versions.yaml` for version tracking
## Related
- [[ansible]] - Available roles reference
- [[indri]] - Target host
- [[observability]] - Metrics collection
- [[review-services]] - Periodic service version review

View file

@ -131,6 +131,7 @@ argocd app sync <service>
- [ ] Tested on feature branch
- [ ] PR reviewed and merged
- [ ] Reset to main branch
- [ ] Service added to `service-versions.yaml` for version tracking
## Related

View file

@ -38,6 +38,7 @@ Task-oriented instructions for common BlumeOps operations. These guides assume y
| Guide | Description |
|-------|-------------|
| [[review-documentation]] | Periodically review and maintain documentation |
| [[review-services]] | Periodically review services for version freshness |
## Database

View file

@ -0,0 +1,84 @@
---
title: Review Services
modified: 2026-02-16
tags:
- how-to
- maintenance
- services
---
# Review Services
How to periodically review BlumeOps services for version freshness and upgrade opportunities.
## Review by Staleness
Show services sorted by when they were last reviewed (most stale first):
```bash
mise run service-review
```
This reads the tracking file at `service-versions.yaml` (repo root) and sorts by the `last-reviewed` field. Services without a review date float to the top. The script shows a staleness table and then displays the most stale service with a review checklist.
To show more entries in the table:
```bash
mise run service-review -- --limit 30
```
To filter by service type:
```bash
mise run service-review -- --type argocd
mise run service-review -- --type ansible
mise run service-review -- --type hybrid
```
## Review Process by Service Type
### ArgoCD Services
1. Check the upstream releases page for new versions
2. Compare to the image tag or Helm chart version in `argocd/manifests/<service>/`
3. Review the upstream changelog for breaking changes
4. If upgrading, update the manifest and follow [[deploy-k8s-service]]
### Helm Chart Services
Same as ArgoCD, but also check for new chart versions in the mirrored chart repos under `argocd/manifests/<service>/charts/`.
### Hybrid Services (Custom Container + ArgoCD)
1. Check the upstream project for new releases
2. Check the base image for security updates
3. If rebuilding, follow [[build-container-image]] to tag and release
4. Update the ArgoCD manifest with the new image tag
### Ansible Services
1. Check the upstream releases page for new versions
2. Review the role's vars/defaults for version pins in `ansible/roles/<service>/`
3. If upgrading, update the version and dry-run: `mise run provision-indri -- --tags <service> --check --diff`
4. Follow [[add-ansible-role]] patterns for role changes
## Marking a Service as Reviewed
After reviewing, edit `service-versions.yaml` (repo root) and update the service entry:
```yaml
- name: prometheus
type: argocd
last-reviewed: 2026-02-16
current-version: "v3.9.1"
upstream-source: https://github.com/prometheus/prometheus/releases
```
Commit this change alongside any upgrades you make during the review.
## Related
- [[review-documentation]] - Periodically review documentation cards
- [[deploy-k8s-service]] - Deploy changes to Kubernetes services
- [[build-container-image]] - Build and release custom container images
- [[add-ansible-role]] - Add or modify Ansible roles

View file

@ -246,6 +246,7 @@ See [[grafana]] for dashboard provisioning details.
- [ ] Metrics/dashboard configured (if applicable)
- [ ] PR created and reviewed
- [ ] Reset to main after merge
- [ ] Service added to `service-versions.yaml` for version tracking
## Related

205
mise-tasks/service-review Executable file
View file

@ -0,0 +1,205 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["pyyaml>=6.0", "rich>=13.0.0", "typer>=0.9.0"]
# ///
#MISE description="Review the most stale service for version freshness"
"""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, hybrid)")] = 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 == "hybrid":
checklist_parts += [
"\n[bold]Custom Container (hybrid):[/bold]\n",
"• Check base image for updates\n",
"• Rebuild container if needed: mise run container-tag-and-release\n",
"• Update ArgoCD manifest with new image tag\n",
]
elif 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",
]
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)

234
service-versions.yaml Normal file
View file

@ -0,0 +1,234 @@
# Service Version Tracking
#
# Tracks when each BlumeOps service was last reviewed for version freshness.
# Used by `mise run service-review` to surface stale services.
#
# Fields:
# name - kebab-case service identifier
# type - argocd | ansible | hybrid (custom container + ArgoCD)
# last-reviewed - date (YYYY-MM-DD) or null
# current-version - deployed version string or null
# upstream-source - URL to upstream releases/changelog
# notes - optional context
services:
# --- ArgoCD plain manifests ---
- name: prometheus
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/prometheus/prometheus/releases
- name: loki
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/grafana/loki/releases
- name: kube-state-metrics
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/kubernetes/kube-state-metrics/releases
- name: mosquitto
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/eclipse/mosquitto/releases
- name: ntfy
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/binwiederhier/ntfy/releases
- name: homepage
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/gethomepage/homepage/releases
notes: Deployed via Helm chart
- name: frigate
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/blakeblackshear/frigate/releases
- name: frigate-notify
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/0x2142/frigate-notify/releases
- name: alloy-k8s
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/grafana/alloy/releases
- name: tailscale-operator
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/tailscale/tailscale/releases
# --- ArgoCD Helm charts ---
- name: grafana
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/grafana/grafana/releases
notes: Deployed via Helm chart
- name: cloudnative-pg
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/cloudnative-pg/cloudnative-pg/releases
notes: Deployed via Helm chart
- name: immich
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/immich-app/immich/releases
notes: Deployed via Helm chart
- name: external-secrets
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/external-secrets/external-secrets/releases
notes: Deployed via Helm chart
- name: 1password-connect
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/1Password/connect/releases
notes: Deployed via Helm chart
# --- ArgoCD infra ---
- name: argocd
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/argoproj/argo-cd/releases
- name: blumeops-pg
type: argocd
last-reviewed: null
current-version: null
upstream-source: https://github.com/cloudnative-pg/cloudnative-pg/releases
notes: CloudNativePG Cluster resource
# --- Hybrid (custom container + ArgoCD) ---
- name: navidrome
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/navidrome/navidrome/releases
- name: miniflux
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/miniflux/v2/releases
- name: teslamate
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/teslamate-org/teslamate/releases
- name: transmission
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/transmission/transmission/releases
- name: kiwix
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/kiwix/kiwix-tools/releases
- name: devpi
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/devpi/devpi/releases
- name: cv
type: hybrid
last-reviewed: null
current-version: null
upstream-source: null
notes: Personal static site, no upstream
- name: docs
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://github.com/jackyzha0/quartz/releases
notes: Quartz static site generator
- name: forgejo-runner
type: hybrid
last-reviewed: null
current-version: null
upstream-source: https://code.forgejo.org/forgejo/runner/releases
# --- Ansible native ---
- name: forgejo
type: ansible
last-reviewed: null
current-version: null
upstream-source: https://codeberg.org/forgejo/forgejo/releases
- name: alloy
type: ansible
last-reviewed: null
current-version: null
upstream-source: https://github.com/grafana/alloy/releases
notes: Built from source on indri
- name: zot
type: ansible
last-reviewed: null
current-version: null
upstream-source: https://github.com/project-zot/zot/releases
notes: Built from source on indri
- name: caddy
type: ansible
last-reviewed: null
current-version: null
upstream-source: https://github.com/caddyserver/caddy/releases
notes: Built from source with Gandi DNS plugin
- name: borgmatic
type: ansible
last-reviewed: null
current-version: null
upstream-source: https://github.com/borgmatic-collective/borgmatic/releases
- name: jellyfin
type: ansible
last-reviewed: null
current-version: null
upstream-source: https://github.com/jellyfin/jellyfin/releases
- name: automounter
type: ansible
last-reviewed: null
current-version: null
upstream-source: null
notes: Custom systemd service, no upstream