diff --git a/CLAUDE.md b/CLAUDE.md index ca7d6af..bdbec28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,23 +140,36 @@ scripts/refresh-server-info.sh --validate-only all scripts/refresh-server-info.sh --validate-only ``` -## Stack mirror (pull / push) +## Stack tree convention (canonical vs mirror) -Compose and config trees are mirrored into `stacks-mirror///` so they can be diffed and version-controlled. Pull is fleet-wide and safe; push is one stack at a time with a diff + prompt. +Two trees, distinct roles. **They are NOT interchangeable.** + +| tree | role | git | who writes | who reads | +|---|---|---|---|---| +| `stacks//` | **canonical / intent** — source of truth for what we want deployed | tracked | you / Claude | `deploy-stack.sh` | +| `stacks-mirror///` | **snapshot / reality** — what's currently on each host | gitignored | `sync-stacks.sh` | drift inspection | + +**Why two:** keeps "intent" (committed, reviewable, deployed) cleanly separate from "reality on the server right now" (often drifts, useful to compare, not durable). Editing the mirror does NOT affect what gets deployed. ```bash -# Pull compose + conf from every host into stacks-mirror/ -scripts/sync-stacks.sh -scripts/sync-stacks.sh --dry-run # see what would change -scripts/sync-stacks.sh ana-docker # one host +# Edit the canonical, then push it to the host: +# stacks// → /opt/docker/compose// +# stacks//conf/ → /opt/docker/conf// +$EDITOR stacks//compose.yaml +scripts/deploy-stack.sh # diffs vs live, prompts y/N +scripts/deploy-stack.sh --compose # skip conf side +scripts/deploy-stack.sh --conf # skip compose side -# Push a local stack back to the server (diffs each file, prompts y/N) -scripts/deploy-stack.sh -scripts/deploy-stack.sh --compose # skip conf -scripts/deploy-stack.sh --conf # skip compose +# Pull current host state into the gitignored snapshot tree (drift check): +scripts/sync-stacks.sh # every host +scripts/sync-stacks.sh ana-docker # one host +scripts/sync-stacks.sh --dry-run # see what would change + +# Compare canonical (intent) vs mirror (reality) for one stack: +diff -ru stacks// stacks-mirror/// ``` -**Opt-out per stack:** create `stacks-mirror///.no-sync` (skip both sides) or `stacks-mirror///conf/.no-sync` (skip conf only). +**Opt-out per stack** (mirror only — sync-stacks.sh skip): create `stacks-mirror///.no-sync` (skip both sides) or `stacks-mirror///conf/.no-sync` (skip conf only). **Always excluded in both directions** (secrets / runtime state): `.env`, `.env.*`, `acme.json`, `client_secrets.json`, `*.pem`, `*.key`, `*.crt`, `*.pfx`, `*.sqlite`, `*.sqlite3`, `*.db`, `*.log`, `*.log.*`, `*.pid`, `hub/`, `logs/`. @@ -174,11 +187,14 @@ eshpfi-management/ │ └── / │ ├── README.md │ └── system-details.txt # latest server_inspect output -├── stacks/ +├── stacks/ # canonical/intent — git-tracked source of truth │ └── / │ ├── compose.yaml # deployed to /opt/docker/compose// +│ ├── conf/ # deployed to /opt/docker/conf// │ ├── .env.example # template; real .env lives on server │ └── README.md # what this stack does, how to deploy +├── stacks-mirror/ # gitignored snapshot of live host state (drift detection) +│ └── // # populated by sync-stacks.sh, NOT a deploy source └── docs/ └── pfi/ # general PFI infrastructure reference ``` diff --git a/scripts/deploy-stack.sh b/scripts/deploy-stack.sh index 432bf38..bc5200e 100755 --- a/scripts/deploy-stack.sh +++ b/scripts/deploy-stack.sh @@ -1,10 +1,15 @@ #!/usr/bin/env bash -# deploy-stack.sh — push a local stacks-mirror dir to a server, with -# per-file diff and confirmation prompt. +# deploy-stack.sh — push a canonical stack from `stacks/` to a server, +# with per-file diff and confirmation prompt. +# +# Source of truth is `stacks//` (the canonical, git-tracked tree). +# `stacks-mirror/` is a separate, gitignored snapshot of what's currently +# on each host (pulled by sync-stacks.sh) — it's used for drift detection, +# NOT as the deploy source. See CLAUDE.md "Stack tree convention". # # Layout assumed: -# stacks-mirror/// → :/opt/docker/compose// -# stacks-mirror///conf/ → :/opt/docker/conf// +# stacks// → :/opt/docker/compose// +# stacks//conf/ → :/opt/docker/conf// # # Secrets / runtime state are never pushed (same exclude list as # sync-stacks.sh): .env*, acme.json, *.key/crt/pem/pfx, *.sqlite*, *.db, @@ -32,7 +37,7 @@ fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SERVERS_DIR="$REPO_ROOT/servers" -MIRROR_DIR="$REPO_ROOT/stacks-mirror" +STACKS_DIR="$REPO_ROOT/stacks" EXCLUDES=( # Include .env.example / *.env.example templates before the broader @@ -100,9 +105,9 @@ resolve_target() { } TARGET=$(resolve_target "$HOST") -STACK_DIR="$MIRROR_DIR/$HOST/$STACK" +STACK_DIR="$STACKS_DIR/$STACK" -[ -d "$STACK_DIR" ] || { echo "error: $STACK_DIR not found (pull with sync-stacks.sh first)" >&2; exit 2; } +[ -d "$STACK_DIR" ] || { echo "error: $STACK_DIR not found — author the canonical stack first (see stacks// for examples)" >&2; exit 2; } # Collect the two src/dest pairs we need to consider. PAIRS=() # each entry: "||" diff --git a/scripts/sync-stacks.sh b/scripts/sync-stacks.sh index 038f54a..b3dea93 100755 --- a/scripts/sync-stacks.sh +++ b/scripts/sync-stacks.sh @@ -1,6 +1,13 @@ #!/usr/bin/env bash # sync-stacks.sh — pull /opt/docker/{compose,conf}// from every -# server into version-controlled `stacks-mirror///`. +# server into the gitignored `stacks-mirror///` snapshot tree. +# +# Role: drift detection. The mirror reflects what's actually running on +# each host RIGHT NOW. The canonical source of truth lives at +# `stacks//` (git-tracked), and `deploy-stack.sh` pushes from +# there. Use the mirror to compare what we have to what's deployed: +# diff -ru stacks// stacks-mirror/// +# See CLAUDE.md "Stack tree convention" for the full split. # # Layout (flat per stack): # stacks-mirror/// <- mirrors /opt/docker/compose// diff --git a/stacks/llama-swap/config.yaml b/stacks/llama-swap/conf/config.yaml similarity index 100% rename from stacks/llama-swap/config.yaml rename to stacks/llama-swap/conf/config.yaml