From 19e74af9caaa2aafef19257f75dacfa0f0963c39 Mon Sep 17 00:00:00 2001 From: emi Date: Wed, 22 Jul 2026 18:47:21 +0000 Subject: [PATCH] Add validator CI + CLAUDE.md; fix dashboard-domain typo (#3) --- .forgejo/workflows/validate.yml | 66 +++++++++++++++++++++++++++++++++ CLAUDE.md | 41 ++++++++++++++++++++ docker-compose.yml | 2 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/validate.yml create mode 100644 CLAUDE.md diff --git a/.forgejo/workflows/validate.yml b/.forgejo/workflows/validate.yml new file mode 100644 index 0000000..084a861 --- /dev/null +++ b/.forgejo/workflows/validate.yml @@ -0,0 +1,66 @@ +name: Validate observability stack + +# This repo deploys by hand (`docker compose up -d` on beta + the Alloy agent on +# each node) with no build step, so nothing caught a malformed compose file, a +# broken dashboard JSON, or an unparseable config until it failed live on the +# host. This is that missing guard: it parses every YAML/JSON artifact that ships +# to a node. It does NOT deploy, and deliberately needs no docker CLI (the +# node:20-bookworm job image doesn't ship one) — a YAML parse catches the real +# breakage (bad indent / unparseable file) without it. +# +# Not validated here: the Alloy config (alloy/config.alloy, an HCL-like format, +# needs the `alloy` binary) and docker-compose *schema* (unknown-key checks, which +# would need the docker CLI). Add either if the churn warrants the extra tooling. + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: docker + steps: + - uses: actions/checkout@v4 + + - name: Dashboards are valid JSON + run: | + set -euo pipefail + python3 - <<'PY' + import json, pathlib, sys + bad = False + files = sorted(pathlib.Path("grafana/dashboards").glob("*.json")) + if not files: + print("no dashboards found"); sys.exit(1) + for f in files: + try: + json.loads(f.read_text()); print("OK", f) + except Exception as e: # noqa: BLE001 + print("INVALID JSON", f, "-", e); bad = True + sys.exit(1 if bad else 0) + PY + + - name: YAML artifacts parse (compose + loki + grafana provisioning) + run: | + set -euo pipefail + apt-get update -qq && apt-get install -y -qq python3-yaml >/dev/null + python3 - <<'PY' + import pathlib, sys + import yaml + bad = False + targets = [ + pathlib.Path("docker-compose.yml"), + pathlib.Path("alloy/docker-compose.agent.yml"), + pathlib.Path("loki/config.yml"), + *sorted(pathlib.Path("grafana/provisioning").rglob("*.yml")), + ] + for f in targets: + if not f.exists(): + print("MISSING", f); bad = True; continue + try: + yaml.safe_load(f.read_text()); print("OK", f) + except Exception as e: # noqa: BLE001 + print("INVALID YAML", f, "-", e); bad = True + sys.exit(1 if bad else 0) + PY diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b3e4898 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +# thermograph-observability — agent instructions + +The **logging/metrics stack** for the Thermograph fleet: Loki + Grafana on beta, +with a Grafana Alloy agent on every node (prod, beta, dev) shipping container and +app logs over the WireGuard mesh. Grafana is fronted by beta's Caddy at +`dashboard.thermograph.org` (Google SSO, pre-provisioned users only). + +This is **operational infra config, not application code** — there is no build. +It deploys by hand: `docker compose up -d` on beta for Loki+Grafana, and the +Alloy agent (`alloy/docker-compose.agent.yml`) on each node. See the README for +the full per-node procedure. + +## Layout + +- `docker-compose.yml` — the Loki + Grafana stack (runs on beta). +- `loki/config.yml` — Loki config (mesh-only, filesystem storage). +- `grafana/provisioning/` — datasource (Loki) + dashboard provider, auto-loaded + at startup. `grafana/dashboards/*.json` — the dashboards themselves. +- `alloy/config.alloy` + `alloy/docker-compose.agent.yml` — the per-node log + shipper. The node name comes from `ALLOY_NODE` (set per host). +- `caddy-grafana.conf` — the Caddy vhost for `dashboard.thermograph.org` (lives + in beta's Caddy config, kept here for reference). +- `.env.example` — copy to `.env` on beta before `docker compose up`. `.env` is + gitignored; never commit real OAuth secrets or the admin password. + +## Conventions + +- **Every artifact that ships to a node is CI-validated** (`.forgejo/workflows/validate.yml`): + both compose files parse, all dashboard JSON is valid, and the Loki/provisioning + YAML parses. Keep new dashboards as valid JSON and new config as valid YAML or + CI fails. The Alloy config is not yet CI-validated (needs the `alloy` binary). +- The public hostname is **`dashboard.thermograph.org`** everywhere — not + `grafana.thermograph.org`. Match it in any new comment/config. +- Secrets (OAuth client id/secret, admin password) live only in the host `.env`, + never in the repo. + +## Branching + +Single `main` branch, no protection. Open a PR against `main`; the validator +gates it. Deploying the change to beta / the nodes is a separate manual step +(this repo has no deploy automation — a known gap). diff --git a/docker-compose.yml b/docker-compose.yml index 90f610f..ff9dd6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ # Loki over the WireGuard mesh, so Loki must listen on the mesh interface. # # Deploy (on beta): docker compose up -d -# Grafana is proxied by beta's Caddy at grafana.thermograph.org (see README); +# Grafana is proxied by beta's Caddy at dashboard.thermograph.org (see README); # Loki is NOT public — it binds the mesh IP only, reachable to agents over wg0. services: