Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@
#
# PROFILES — activate optional services by passing --profile <name> (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
Expand Down
62 changes: 62 additions & 0 deletions test/unit/docker-compose-profiles-header.test.ts
Original file line number Diff line number Diff line change
@@ -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 <name> 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<string, ComposeService>;
}

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<string> {
const profiles = new Set<string>();
for (const service of Object.values(doc.services)) {
for (const profile of service.profiles ?? []) profiles.add(profile);
}
return profiles;
}

/** Every `--profile <name>` 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<string> {
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();
});
});
Loading