From d51f0e01d013058256dfdea2343f00eca3cf994a Mon Sep 17 00:00:00 2001 From: galuis116 Date: Wed, 15 Jul 2026 05:36:51 -0400 Subject: [PATCH] docs(selfhost): document ams-observability and backup in the compose PROFILES header (#5812) Both are real, wired --profile flags (ams-reporting-exporter and the backup/backup-exporter services) but were missing from docker-compose.yml's own top-of-file PROFILES banner, the first place an operator looks to discover what --profile flags exist. Realigns the whole table to accommodate ams-observability, the new longest entry. Adds a regression test (test/unit/docker-compose-profiles-header.test.ts) that parses every profiles: [...] a service actually declares and every --profile token in the header banner, and asserts the two sets match -- so a future profile addition/removal that forgets the banner fails CI instead of shipping silent doc drift. --- docker-compose.yml | 35 ++++++----- .../docker-compose-profiles-header.test.ts | 62 +++++++++++++++++++ 2 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 test/unit/docker-compose-profiles-header.test.ts diff --git a/docker-compose.yml b/docker-compose.yml index bd6b781534..d6545550e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,22 +10,25 @@ # # PROFILES — activate optional services by passing --profile (combine freely): # -# (none) SQLite + Redis single-node stack (default — no flags needed) -# --profile postgres pgvector/pg16 shared database (multi-instance capable) -# --profile pgbouncer PgBouncer connection pooler in front of Postgres -# --profile qdrant Qdrant vector database for RAG -# --profile ollama Local Ollama AI backend -# --profile gpu GPU metrics exporter (nvidia-smi -> Prometheus -> Grafana), requires the NVIDIA -# Container Toolkit; combine with --profile ollama on a GPU host -# --profile visual-review Headless Chromium (browserless) for before/after PR screenshot capture -# --profile rees Review-enrichment service (REES) for heavier PR analysis, in-network only -# --profile litestream Continuous SQLite backup to S3/B2/R2 via Litestream -# --profile workflows n8n workflow automation (Slack/Teams/email fan-out, scheduled reports) -# --profile storage MinIO S3-compatible object store (Litestream destination, artifact blobs) -# --profile caddy Caddy HTTPS terminator with auto-TLS (set DOMAIN= in .env) -# --profile observability Prometheus + Alertmanager + Loki + Promtail + Grafana (pre-wired) -# --profile tailscale Tailscale sidecar — access the stack via your tailnet -# --profile runners GitHub Actions self-hosted runner +# (none) SQLite + Redis single-node stack (default — no flags needed) +# --profile postgres pgvector/pg16 shared database (multi-instance capable) +# --profile pgbouncer PgBouncer connection pooler in front of Postgres +# --profile qdrant Qdrant vector database for RAG +# --profile ollama Local Ollama AI backend +# --profile gpu GPU metrics exporter (nvidia-smi -> Prometheus -> Grafana), requires the NVIDIA +# Container Toolkit; combine with --profile ollama on a GPU host +# --profile visual-review Headless Chromium (browserless) for before/after PR screenshot capture +# --profile rees Review-enrichment service (REES) for heavier PR analysis, in-network only +# --profile litestream Continuous SQLite backup to S3/B2/R2 via Litestream +# --profile workflows n8n workflow automation (Slack/Teams/email fan-out, scheduled reports) +# --profile storage MinIO S3-compatible object store (Litestream destination, artifact blobs) +# --profile caddy Caddy HTTPS terminator with auto-TLS (set DOMAIN= in .env) +# --profile observability Prometheus + Alertmanager + Loki + Promtail + Grafana (pre-wired) +# --profile ams-observability Exports AMS (loopover-miner) attempt-log/prediction-ledger data to the +# AMS Attempt Log / Prediction Ledger Grafana datasources +# --profile tailscale Tailscale sidecar — access the stack via your tailnet +# --profile runners GitHub Actions self-hosted runner +# --profile backup Scheduled Postgres/SQLite + Qdrant-snapshot backups with freshness metrics # # Examples: # ./scripts/deploy-selfhost-image.sh # SQLite + Redis, published image diff --git a/test/unit/docker-compose-profiles-header.test.ts b/test/unit/docker-compose-profiles-header.test.ts new file mode 100644 index 0000000000..1aac82f462 --- /dev/null +++ b/test/unit/docker-compose-profiles-header.test.ts @@ -0,0 +1,62 @@ +import { readFileSync } from "node:fs"; +import { parse } from "yaml"; +import { describe, expect, it } from "vitest"; + +// Regression check (#5812): docker-compose.yml's header comment banner is the documented entry point for +// "which --profile flags does this file support", but nothing kept it in sync with the services that +// actually declare a `profiles: [...]` array -- ams-observability and backup silently drifted out of it. +// Scans both sides (the real profiles: declarations, and the --profile tokens in the header banner +// preceding `services:`) and asserts they're the same set, so a future profile addition/removal that +// forgets the banner fails CI instead of shipping silent doc drift. + +interface ComposeService { + profiles?: string[]; +} +interface ComposeDoc { + services: Record; +} + +const HEADER_PROFILE_TOKEN_PATTERN = /--profile ([a-z0-9-]+)/g; + +/** Every profile name any service in `doc` actually declares via `profiles: [...]`. */ +function declaredProfiles(doc: ComposeDoc): Set { + const profiles = new Set(); + for (const service of Object.values(doc.services)) { + for (const profile of service.profiles ?? []) profiles.add(profile); + } + return profiles; +} + +/** Every `--profile ` token mentioned in the header comment block (everything before the first + * `services:` key -- the file's `#`-comment banner, not the machine-readable compose structure). */ +function headerBannerProfiles(fileText: string): Set { + const servicesIndex = fileText.indexOf("\nservices:"); + const header = servicesIndex === -1 ? fileText : fileText.slice(0, servicesIndex); + return new Set([...header.matchAll(HEADER_PROFILE_TOKEN_PATTERN)].map((match) => match[1]!)); +} + +function assertHeaderMatchesDeclared(doc: ComposeDoc, fileText: string): void { + const declared = [...declaredProfiles(doc)].sort(); + const documented = [...headerBannerProfiles(fileText)].sort(); + expect(documented).toEqual(declared); +} + +describe("docker-compose.yml PROFILES header (#5812)", () => { + it("documents exactly the set of profiles services actually declare in the real file", () => { + const fileText = readFileSync("docker-compose.yml", "utf8"); + const doc = parse(fileText) as ComposeDoc; + assertHeaderMatchesDeclared(doc, fileText); + }); + + it("fails when a service declares a profile missing from the header banner", () => { + const doc: ComposeDoc = { services: { foo: { profiles: ["postgres"] }, bar: { profiles: ["undocumented-profile"] } } }; + const fileText = "# --profile postgres a database\nservices:\n foo:\n"; + expect(() => assertHeaderMatchesDeclared(doc, fileText)).toThrow(); + }); + + it("fails when the header banner mentions a profile no service declares", () => { + const doc: ComposeDoc = { services: { foo: { profiles: ["postgres"] } } }; + const fileText = "# --profile postgres a database\n# --profile phantom never declared\nservices:\n foo:\n"; + expect(() => assertHeaderMatchesDeclared(doc, fileText)).toThrow(); + }); +});