blumeops/mise-tasks/docs-preview

140 lines
4.6 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
C1: SHA-pin tooling dependencies (2026-04 cycle) (#344) ## Summary Monthly tooling dependency refresh, with a one-time conversion from version-tag pins (`rev = "vX.Y.Z"`, `image:tag`, `>=`) to SHA / digest pins everywhere. ## Changes - **prek hooks**: all `rev = "vX.Y.Z"` → commit SHA + `# vX.Y.Z` comment. Bumped trufflehog (3.94.0→3.95.2), kingfisher (1.91.0→1.97.0), ruff (0.15.7→0.15.12), shfmt (3.13.0→3.13.1), prettier (3.8.1→3.8.3), actionlint (1.7.11→1.7.12). - **fly/Dockerfile**: tag pins → `image@sha256:...` digest pins. Bumped nginx (1.29.6→1.30.0-alpine), tailscale (v1.94.1→v1.94.2 — still inside the safe pre-1.96.5 range), alloy (v1.14.1→v1.16.0). - **mise-tasks**: PEP 723 inline deps converted from `>=` to `==` (PEP 508 doesn't support hashes inline). All scripts pinned to current latest: rich 15.0.0, typer 0.25.0, pyyaml 6.0.3, httpx 0.28.1. - **prek `additional_dependencies`**: ansible-lint==26.4.0, ansible-core==2.20.5. - **taplo-lint**: pass `--no-schema`. Upstream's `--default-schema-catalogs` returns a format taplo v0.9.3 can't parse — we don't validate against TOML schemas anyway, so this turns off the broken catalog fetch. - **docs/update-tooling-dependencies**: documents the SHA-pin convention, `docker buildx imagetools inspect` for digest lookup, and `prek clean` before re-verifying (cache grows to several GiB). Forgejo workflow `actions/checkout@v6.0.2` was already at the latest SHA — no change. ## Test plan - [x] `prek run --all-files` passes after `prek clean` - [x] `deploy-fly` workflow builds and deploys the new fly image on merge - [x] `fly status -a blumeops-proxy` healthy after deploy - [x] Spot-check a few mise tasks (`mise run blumeops-tasks`, `mise run docs-check-links`) to confirm pinned deps resolve cleanly Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/344
2026-04-30 16:51:43 -07:00
# dependencies = ["pyyaml==6.0.3", "rich==15.0.0", "typer==0.25.0"]
# ///
#MISE description="Build docs with Dagger and serve locally, opening to a specific card"
#USAGE arg "<card>" help="Card path relative to docs/, e.g. how-to/knowledgebase/review-documentation"
#USAGE flag "--port <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")
# Try exact path first (e.g. "docs/how-to/..."), then inside docs/
exact_file = REPO_ROOT / f"{card_stem}.md"
docs_file = REPO_ROOT / "docs" / f"{card_stem}.md"
if exact_file.exists() and card_stem.startswith("docs/"):
card_stem = card_stem.removeprefix("docs/")
card_file = exact_file
elif docs_file.exists():
card_file = docs_file
else:
console.print(f"[bold red]Card not found:[/bold red] {docs_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, filter="data")
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)