name: Validate observability stack # The observability DOMAIN 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 without it. # # Monorepo port: paths are prefixed observability/, the push trigger is # path-filtered to the domain, and workflow_call lets pr-build.yml reuse this # as the domain's PR check under its single `gate` required status. # # The Alloy config (observability/alloy/config.alloy, an HCL-like format) IS # validated below -- the static `alloy` binary is downloaded straight from its # GitHub release (pinned to the same v1.9.1 the fleet runs; see # observability/alloy/docker-compose.agent.yml), no docker CLI needed. # # Not validated here: docker-compose *schema* (unknown-key checks, which would # need the docker CLI). Add it if the churn warrants the extra tooling. on: workflow_call: {} push: branches: [main, dev] paths: ['observability/**'] 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("observability/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 root = pathlib.Path("observability") targets = [ root / "docker-compose.yml", root / "alloy/docker-compose.agent.yml", root / "loki/config.yml", *sorted((root / "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 - name: Alloy config parses and validates (components, relabel/process wiring) run: | set -euo pipefail apt-get update -qq && apt-get install -y -qq unzip >/dev/null curl -sSL -o /tmp/alloy.zip \ https://github.com/grafana/alloy/releases/download/v1.9.1/alloy-linux-amd64.zip unzip -q /tmp/alloy.zip -d /tmp/alloybin chmod +x /tmp/alloybin/alloy-linux-amd64 # `alloy validate` builds the real component graph (catches bad field # names, dangling forward_to/receiver refs, malformed relabel/process # stages) but doesn't recognize the top-level `livedebugging {}` singleton # block as a component -- it only knows named components, even though # `alloy run` loads that block fine. Known gap in the `validate` # subcommand, not a config error, so strip that one line before checking. grep -v '^livedebugging ' observability/alloy/config.alloy > /tmp/config-for-validate.alloy /tmp/alloybin/alloy-linux-amd64 validate /tmp/config-for-validate.alloy