From d01a165b917346b69a9960cbd3b237bb67eb8bea Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 11 Mar 2026 18:04:01 -0700 Subject: [PATCH] Add docs-preview task and visual preview step to doc review New `mise run docs-preview ` task builds docs via Dagger and serves them locally in the production quartz container (image parsed from ArgoCD kustomization), opening the browser directly to the specified card. Container auto-cleans after 1 hour. Also updates docs-review checklist and review-documentation how-to to reference the visual preview workflow. Co-Authored-By: Claude Opus 4.6 --- docs/changelog.d/+docs-preview.feature.md | 1 + .../knowledgebase/review-documentation.md | 14 ++ mise-tasks/docs-preview | 133 ++++++++++++++++++ mise-tasks/docs-review | 3 + 4 files changed, 151 insertions(+) create mode 100644 docs/changelog.d/+docs-preview.feature.md create mode 100644 mise-tasks/docs-preview diff --git a/docs/changelog.d/+docs-preview.feature.md b/docs/changelog.d/+docs-preview.feature.md new file mode 100644 index 0000000..f865783 --- /dev/null +++ b/docs/changelog.d/+docs-preview.feature.md @@ -0,0 +1 @@ +Add `docs-preview` mise task: builds docs with Dagger and serves them locally in the production quartz container, opening the browser directly to the specified card. Also adds visual preview hints to the `docs-review` checklist and the review-documentation how-to. diff --git a/docs/how-to/knowledgebase/review-documentation.md b/docs/how-to/knowledgebase/review-documentation.md index d6dc064..fe17449 100644 --- a/docs/how-to/knowledgebase/review-documentation.md +++ b/docs/how-to/knowledgebase/review-documentation.md @@ -92,6 +92,20 @@ mise run dns-preview # DNS (Gandi) If changes are pending, investigate whether docs or infrastructure is stale. +## Visual Preview + +After reviewing and editing a card, visually verify the rendered output. + +**Quick scan (agent):** Have the agent display the card with `bat` for a terminal-based visual check. + +**Full rendered preview:** Build the entire Quartz docs site locally and open directly to the card: + +```bash +mise run docs-preview how-to/knowledgebase/review-documentation +``` + +This builds the docs with Dagger, serves them on `localhost:8484`, and opens the browser to the specified card. Press Ctrl-C to stop. Accepts paths with or without the `.md` suffix. + ## Making Changes If a card needs updates, classify the change (see [[agent-change-process]]): diff --git a/mise-tasks/docs-preview b/mise-tasks/docs-preview new file mode 100644 index 0000000..a968797 --- /dev/null +++ b/mise-tasks/docs-preview @@ -0,0 +1,133 @@ +#!/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="Build docs with Dagger and serve locally, opening to a specific card" +#USAGE arg "" help="Card path relative to docs/, e.g. how-to/knowledgebase/review-documentation" +#USAGE flag "--port " default="8484" help="Port for preview server (default 8484)" +"""Build the full Quartz docs site and serve locally for visual preview. + +Builds the documentation using Dagger's build_docs function, extracts the +result, and serves it in the same quartz container used in production +(image parsed from the ArgoCD kustomization). Opens the browser directly +to the specified card. The container auto-removes after 1 hour. + +Usage: mise run docs-preview how-to/knowledgebase/review-documentation +""" + +import shutil +import subprocess +import tarfile +import tempfile +import webbrowser +from pathlib import Path +from typing import Annotated + +import typer +import yaml +from rich.console import Console + +REPO_ROOT = Path(__file__).parent.parent +CONTAINER_NAME = "docs-preview" + + +def get_quartz_image() -> str: + """Parse the quartz container image from the ArgoCD kustomization.""" + kustomization = REPO_ROOT / "argocd" / "manifests" / "docs" / "kustomization.yaml" + data = yaml.safe_load(kustomization.read_text()) + for img in data.get("images", []): + if img["name"] == "registry.ops.eblu.me/blumeops/quartz": + return f"{img['name']}:{img['newTag']}" + raise RuntimeError("Could not find quartz image in kustomization.yaml") + + +def main( + card: Annotated[str, typer.Argument(help="Card path relative to docs/")], + port: Annotated[int, typer.Option(help="Port for preview server")] = 8484, +) -> None: + console = Console() + + # Normalize: accept with or without .md suffix + card_stem = card.removesuffix(".md") + card_file = REPO_ROOT / "docs" / f"{card_stem}.md" + if not card_file.exists(): + console.print(f"[bold red]Card not found:[/bold red] {card_file}") + raise typer.Exit(code=1) + + url_path = "/" + card_stem + image = get_quartz_image() + console.print(f"[dim]Using image: {image}[/dim]") + + # Clean up any previous preview container and its docroot + subprocess.run( + ["docker", "rm", "-f", CONTAINER_NAME], + capture_output=True, + ) + docroot = Path(tempfile.gettempdir()) / "docs-preview" + if docroot.exists(): + shutil.rmtree(docroot) + docroot.mkdir() + + with tempfile.TemporaryDirectory() as tmpdir: + tarball = Path(tmpdir) / "docs-preview.tar.gz" + + console.print("[bold]Building docs with Dagger...[/bold]") + subprocess.run( + [ + "dagger", + "call", + "build-docs", + "--src=.", + "--version=preview", + "export", + f"--path={tarball}", + ], + cwd=REPO_ROOT, + check=True, + ) + + console.print("[bold]Extracting docs...[/bold]") + with tarfile.open(tarball, "r:gz") as tf: + tf.extractall(docroot) + + console.print("[bold]Starting preview container...[/bold]") + subprocess.run( + [ + "docker", + "run", + "-d", + "--rm", + "--name", CONTAINER_NAME, + "--stop-timeout", "0", + "-p", f"{port}:80", + "-v", f"{docroot}:/usr/share/nginx/html:ro", + "--entrypoint", "nginx", + image, + "-g", "daemon off;", + ], + check=True, + ) + + url = f"http://localhost:{port}{url_path}" + console.print(f"\n[bold green]Preview running at http://localhost:{port}[/bold green]") + console.print(f"[bold cyan]Opening {url}[/bold cyan]\n") + webbrowser.open(url) + + console.print(f"[yellow]Container will auto-stop in 1 hour.[/yellow]") + console.print(f"[yellow]To stop sooner: docker rm -f {CONTAINER_NAME}[/yellow]\n") + + # Schedule auto-cleanup after 1 hour (container + docroot) + subprocess.Popen( + [ + "sh", "-c", + f"sleep 3600 && docker rm -f {CONTAINER_NAME} 2>/dev/null && rm -rf {docroot}", + ], + start_new_session=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + +if __name__ == "__main__": + typer.run(main) diff --git a/mise-tasks/docs-review b/mise-tasks/docs-review index 6d07b4b..e7c7aa2 100755 --- a/mise-tasks/docs-review +++ b/mise-tasks/docs-review @@ -156,6 +156,9 @@ def main( "• If ArgoCD app: is it synced? (argocd app get )\n" "• If Ansible role: does it apply idempotently? (--check --diff)\n" "• If Pulumi: is there drift? (pulumi preview)\n\n" + "[bold]Visual Preview:[/bold]\n\n" + "• Agent: use [cyan]bat[/cyan] to display the reviewed card for user visual scan\n" + "• For full rendered preview: [cyan]mise run docs-preview [/cyan]\n\n" "[bold]After Review:[/bold]\n\n" "• Update the card's frontmatter: [cyan]last-reviewed: " + str(today)