diff --git a/docs/changelog.d/feature-service-review.feature.md b/docs/changelog.d/feature-service-review.feature.md new file mode 100644 index 0000000..8bf5396 --- /dev/null +++ b/docs/changelog.d/feature-service-review.feature.md @@ -0,0 +1 @@ +Add service version review system with `mise run service-review` task, tracking file, and how-to guide. diff --git a/docs/how-to/add-ansible-role.md b/docs/how-to/add-ansible-role.md index a7c2ded..5c51a79 100644 --- a/docs/how-to/add-ansible-role.md +++ b/docs/how-to/add-ansible-role.md @@ -136,8 +136,17 @@ For metrics collection, create a companion `_metrics` role that: See [[alloy]] for how metrics are collected from textfiles. +## Checklist + +- [ ] Role created in `ansible/roles//` +- [ ] Role added to `ansible/playbooks/indri.yml` with tag +- [ ] Secrets wired via pre_tasks (if needed) +- [ ] Dry run passes: `mise run provision-indri -- --tags --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 diff --git a/docs/how-to/deploy-k8s-service.md b/docs/how-to/deploy-k8s-service.md index e54fd7a..6005d6f 100644 --- a/docs/how-to/deploy-k8s-service.md +++ b/docs/how-to/deploy-k8s-service.md @@ -131,6 +131,7 @@ argocd app sync - [ ] Tested on feature branch - [ ] PR reviewed and merged - [ ] Reset to main branch +- [ ] Service added to `service-versions.yaml` for version tracking ## Related diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index 7d8a524..81f887a 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -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 diff --git a/docs/how-to/knowledgebase/review-services.md b/docs/how-to/knowledgebase/review-services.md new file mode 100644 index 0000000..4716bbf --- /dev/null +++ b/docs/how-to/knowledgebase/review-services.md @@ -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//` +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//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//` +3. If upgrading, update the version and dry-run: `mise run provision-indri -- --tags --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 diff --git a/docs/tutorials/adding-a-service.md b/docs/tutorials/adding-a-service.md index 3f9b7a4..5fdce11 100644 --- a/docs/tutorials/adding-a-service.md +++ b/docs/tutorials/adding-a-service.md @@ -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 diff --git a/mise-tasks/service-review b/mise-tasks/service-review new file mode 100755 index 0000000..03b9b4a --- /dev/null +++ b/mise-tasks/service-review @@ -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) diff --git a/service-versions.yaml b/service-versions.yaml new file mode 100644 index 0000000..981365c --- /dev/null +++ b/service-versions.yaml @@ -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