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:
parent
994bed0693
commit
faf9682b55
8 changed files with 536 additions and 0 deletions
1
docs/changelog.d/feature-service-review.feature.md
Normal file
1
docs/changelog.d/feature-service-review.feature.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add service version review system with `mise run service-review` task, tracking file, and how-to guide.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
84
docs/how-to/knowledgebase/review-services.md
Normal file
84
docs/how-to/knowledgebase/review-services.md
Normal 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
|
||||
|
|
@ -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
205
mise-tasks/service-review
Executable 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
234
service-versions.yaml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue