From e41c28ed905c1aedbf7283b1fdeb50d0aef3ba21 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 22 Feb 2026 10:20:11 -0800 Subject: [PATCH] Replace indri-runner-logs with general-purpose runner-logs Typer CLI (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replace bash `indri-runner-logs` with a Python Typer CLI `runner-logs` that supports filtering by runner host (`indri`, `ringtail`, or `all`) with rich table output - Add missing `#USAGE` declarations to `docs-review`, `docs-review-stale`, and `service-review` so flags work without the `--` separator - Update docs references in `review-documentation.md` and `review-services.md` to use the new flag syntax ## Test plan - [x] `mise run runner-logs all` lists runs from both runners - [x] `mise run runner-logs ringtail` filters to ringtail-only runs - [x] `mise run docs-review-stale --threshold 90` works without `--` - [x] `mise run docs-review --limit 5` works without `--` - [x] `mise run service-review --limit 3` works without `--` - [x] Pre-commit hooks pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/244 --- .../knowledgebase/review-documentation.md | 2 +- docs/how-to/knowledgebase/review-services.md | 8 +- docs/tutorials/ai-assistance-guide.md | 2 +- mise-tasks/docs-review | 1 + mise-tasks/docs-review-stale | 1 + mise-tasks/indri-runner-logs | 38 ----- mise-tasks/runner-logs | 131 ++++++++++++++++++ mise-tasks/service-review | 2 + 8 files changed, 141 insertions(+), 44 deletions(-) delete mode 100755 mise-tasks/indri-runner-logs create mode 100755 mise-tasks/runner-logs diff --git a/docs/how-to/knowledgebase/review-documentation.md b/docs/how-to/knowledgebase/review-documentation.md index 57a0111..9d031d3 100644 --- a/docs/how-to/knowledgebase/review-documentation.md +++ b/docs/how-to/knowledgebase/review-documentation.md @@ -24,7 +24,7 @@ This reads the `last-reviewed` frontmatter field from each card. Cards without t To show more entries in the table: ```bash -mise run docs-review -- --limit 30 +mise run docs-review --limit 30 ``` ### Marking a Card as Reviewed diff --git a/docs/how-to/knowledgebase/review-services.md b/docs/how-to/knowledgebase/review-services.md index f07a017..516bcef 100644 --- a/docs/how-to/knowledgebase/review-services.md +++ b/docs/how-to/knowledgebase/review-services.md @@ -24,15 +24,15 @@ This reads the tracking file at `service-versions.yaml` (repo root) and sorts by To show more entries in the table: ```bash -mise run service-review -- --limit 30 +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 +mise run service-review --type argocd +mise run service-review --type ansible +mise run service-review --type hybrid ``` ## Review Process by Service Type diff --git a/docs/tutorials/ai-assistance-guide.md b/docs/tutorials/ai-assistance-guide.md index 3123e07..a98fbbd 100644 --- a/docs/tutorials/ai-assistance-guide.md +++ b/docs/tutorials/ai-assistance-guide.md @@ -102,7 +102,7 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail | `docs-review-stale` | Report docs by last-modified date, highlight stale ones | | `docs-review-tags` | Print frontmatter tag inventory across all docs | | `docs-review` | Review the most stale doc by last-reviewed date | -| `indri-runner-logs` | View Forgejo workflow logs from local runner | +| `runner-logs` | View Forgejo workflow logs (indri or ringtail runner) | For ArgoCD operations, use the `argocd` CLI directly: - `argocd app diff ` - Preview changes diff --git a/mise-tasks/docs-review b/mise-tasks/docs-review index cb94a14..5429bd0 100755 --- a/mise-tasks/docs-review +++ b/mise-tasks/docs-review @@ -4,6 +4,7 @@ # dependencies = ["pyyaml>=6.0", "rich>=13.0.0", "typer>=0.9.0"] # /// #MISE description="Review the most stale documentation card by last-reviewed date" +#USAGE flag "--limit " default="15" help="Number of docs to show in the table" """Review the most stale documentation card by last-reviewed date. Scans all markdown files in docs/ (excluding changelog.d/) and sorts them diff --git a/mise-tasks/docs-review-stale b/mise-tasks/docs-review-stale index 819660f..6fe4982 100755 --- a/mise-tasks/docs-review-stale +++ b/mise-tasks/docs-review-stale @@ -4,6 +4,7 @@ # dependencies = ["rich>=13.0.0", "typer>=0.9.0"] # /// #MISE description="Report docs by git-last-modified date, highlighting stale ones" +#USAGE flag "--threshold " default="180" help="Days before a doc is considered stale" """Report documentation files sorted by git-last-modified date. Scans all markdown files in docs/ (excluding changelog.d/) and shows diff --git a/mise-tasks/indri-runner-logs b/mise-tasks/indri-runner-logs deleted file mode 100755 index a61454a..0000000 --- a/mise-tasks/indri-runner-logs +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Get logs for a workflow run from indri (local runner only)" - -set -euo pipefail - -RUN_ID="${1:-}" - -if [[ -z "$RUN_ID" ]]; then - echo "Usage: mise run indri-runner-logs " - echo "" - echo "Fetches logs for a Forgejo Actions run from indri's local storage." - echo "Only works for runs executed by the indri-host-runner." - echo "" - echo "Recent runs:" - curl -sf "https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/actions/tasks" | \ - jq -r '.workflow_runs[:10] | .[] | " \(.id)\t\(.status)\t\(.workflow_id)\t\(.display_title | .[0:50])"' - exit 1 -fi - -# Logs are stored as: actions_log////.log.zst -# The hex subdir is the last 2 hex chars of the run_id -ACTIONS_LOG_DIR="/opt/homebrew/var/forgejo/data/actions_log/eblume/blumeops" - -# Find the log file - hex subdir is computed from run_id -HEX_SUBDIR=$(printf '%02x' "$RUN_ID") -LOG_FILE="${ACTIONS_LOG_DIR}/${HEX_SUBDIR}/${RUN_ID}.log.zst" - -# Check if log exists and decompress -if ssh indri "test -f '$LOG_FILE'"; then - ssh indri "zstd -d -c '$LOG_FILE'" -else - echo "Error: Log file not found for run $RUN_ID" - echo "Expected path: $LOG_FILE" - echo "" - echo "Available logs:" - ssh indri "find '$ACTIONS_LOG_DIR' -name '*.log.zst' -exec basename {} .log.zst \; | sort -n | tail -10" - exit 1 -fi diff --git a/mise-tasks/runner-logs b/mise-tasks/runner-logs new file mode 100755 index 0000000..536f1fe --- /dev/null +++ b/mise-tasks/runner-logs @@ -0,0 +1,131 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["httpx>=0.28.0", "rich>=13.0.0", "typer>=0.15.0"] +# /// +#MISE description="Get logs for a Forgejo Actions workflow run (indri or ringtail runner)" +#USAGE arg "" help="Runner filter: indri, ringtail, or all" +#USAGE arg "[run_id]" help="Run ID to fetch logs for (omit to list recent runs)" +"""Fetch Forgejo Actions workflow logs from indri's log storage. + +Both the indri k8s runner and ringtail nix-container-builder runner report +logs back to the Forgejo server on indri. This tool lists recent runs +(optionally filtered by runner) and fetches compressed logs by run ID. + +Usage: + mise run runner-logs all # list recent runs from all runners + mise run runner-logs ringtail # list recent ringtail runs + mise run runner-logs all 337 # fetch logs for run 337 +""" + +import subprocess +import sys +from typing import Annotated + +import httpx +import typer +from rich.console import Console +from rich.table import Table + +FORGE_API = "https://forge.ops.eblu.me/api/v1" +REPO = "eblume/blumeops" +ACTIONS_LOG_DIR = "/opt/homebrew/var/forgejo/data/actions_log/eblume/blumeops" + +# Workflows using the ringtail nix-container-builder runner; everything else +# runs on the indri k8s runner. +RINGTAIL_WORKFLOWS = {"build-container-nix.yaml"} + +app = typer.Typer(add_completion=False) + + +def runner_for_workflow(workflow_id: str) -> str: + return "ringtail" if workflow_id in RINGTAIL_WORKFLOWS else "indri" + + +def list_runs(runner: str, console: Console) -> None: + resp = httpx.get( + f"{FORGE_API}/repos/{REPO}/actions/tasks", + timeout=15, + ) + resp.raise_for_status() + runs = resp.json().get("workflow_runs", []) + + table = Table(title=f"Recent runs (filter: {runner})") + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Status") + table.add_column("Runner") + table.add_column("Name") + table.add_column("Title") + + for run in runs[:20]: + host = runner_for_workflow(run.get("workflow_id", "")) + if runner != "all" and host != runner: + continue + status = run.get("status", "") + style = "green" if status == "success" else "red" if status == "failure" else "yellow" + table.add_row( + str(run["id"]), + f"[{style}]{status}[/{style}]", + host, + (run.get("name") or "")[:40], + (run.get("display_title") or "")[:30], + ) + + console.print(table) + + +def fetch_log(run_id: int) -> None: + hex_subdir = f"{run_id:02x}" + log_file = f"{ACTIONS_LOG_DIR}/{hex_subdir}/{run_id}.log.zst" + + # All logs live on indri (the Forgejo server) regardless of runner + result = subprocess.run( + ["ssh", "indri", f"test -f '{log_file}' && zstd -d -c '{log_file}'"], + capture_output=True, + text=True, + ) + + if result.returncode == 0: + sys.stdout.write(result.stdout) + else: + typer.echo(f"Error: Log file not found for run {run_id}", err=True) + typer.echo(f"Expected path: {log_file}", err=True) + typer.echo("", err=True) + typer.echo("Available logs:", err=True) + avail = subprocess.run( + [ + "ssh", + "indri", + f"find '{ACTIONS_LOG_DIR}' -name '*.log.zst' -exec basename {{}} .log.zst \\; | sort -n | tail -10", + ], + capture_output=True, + text=True, + ) + typer.echo(avail.stdout, err=True) + raise typer.Exit(1) + + +@app.command() +def main( + runner: Annotated[ + str, + typer.Argument(help="Runner filter: indri, ringtail, or all"), + ], + run_id: Annotated[ + int | None, + typer.Argument(help="Run ID to fetch logs for (omit to list recent runs)"), + ] = None, +) -> None: + """Get logs for a Forgejo Actions workflow run.""" + if runner not in ("indri", "ringtail", "all"): + typer.echo(f"Error: runner must be 'indri', 'ringtail', or 'all', got '{runner}'") + raise typer.Exit(1) + + if run_id is None: + list_runs(runner, Console()) + else: + fetch_log(run_id) + + +if __name__ == "__main__": + app() diff --git a/mise-tasks/service-review b/mise-tasks/service-review index 0c3070c..aaaf016 100755 --- a/mise-tasks/service-review +++ b/mise-tasks/service-review @@ -4,6 +4,8 @@ # dependencies = ["pyyaml>=6.0", "rich>=13.0.0", "typer>=0.9.0"] # /// #MISE description="Review the most stale service for version freshness" +#USAGE flag "--limit " default="15" help="Number of services to show in the table" +#USAGE flag "--type " help="Filter by service type (argocd, ansible, hybrid)" """Review the most stale service for version freshness. Reads ``docs/reference/services/service-versions.yaml`` and sorts services