From 765d89aed36b6172c427b22519f6e9c967d621ca Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Tue, 28 Apr 2026 15:58:22 -0700 Subject: [PATCH] scripts: add fork-fleet.sh for bootstrapping a new fleet repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the reusable tooling (scripts/, generic playbook templates, .gitignore, conventions section of CLAUDE.md) into a new directory and strips everything fleet-specific: servers/, stacks/, configs/, fleet-named playbooks (deploy-*, decouple-*), runbooks, status, host-pinned scripts. Output is an empty conventionally-organized workspace with fresh git history, ready to populate with a different fleet. Skeleton CLAUDE.md / README.md / STATUS.md are written with the new fleet name baked in but no server table or placement rules pre-populated. Deliberately does NOT create a remote or push — the user picks the namespace + name explicitly via tea / git remote add. scripts/fork-fleet.sh ~/development/acme-prod-management scripts/fork-fleet.sh /tmp/test-fork test-fleet --- scripts/fork-fleet.sh | 332 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100755 scripts/fork-fleet.sh diff --git a/scripts/fork-fleet.sh b/scripts/fork-fleet.sh new file mode 100755 index 0000000..827bc8e --- /dev/null +++ b/scripts/fork-fleet.sh @@ -0,0 +1,332 @@ +#!/usr/bin/env bash +# fork-fleet.sh — bootstrap a fresh fleet-management repo from this one. +# +# Mirrors the reusable tooling (scripts/, generic playbook templates, +# conventions section of CLAUDE.md, .gitignore) into a new directory +# and strips everything fleet-specific (servers/, stacks/, configs/, +# fleet-named playbooks, runbooks, status). The output is an empty +# but conventionally-organized workspace that's ready to populate +# with a different fleet. +# +# Deliberately: +# * does NOT create a remote / push — that's a manual step so the +# name + namespace are explicit +# * does NOT carry git history — the new fleet starts with a clean +# "Initial commit" so the source repo's commit log doesn't bleed +# across +# * refuses to overwrite an existing directory +# +# Usage: +# scripts/fork-fleet.sh [] +# +# Examples: +# scripts/fork-fleet.sh ~/development/acme-prod-management +# scripts/fork-fleet.sh /tmp/test-fork test-fleet +# +# Default fleet-name is the destination dir's basename. + +set -euo pipefail + +DEST="${1:?usage: $0 []}" +FLEET_NAME="${2:-$(basename "$DEST" | sed 's/-management$//')}" +SRC="$(cd "$(dirname "$0")/.." && pwd)" + +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +info() { printf ' %s\n' "$*"; } + +if [ -e "$DEST" ]; then + echo "FATAL: $DEST already exists — refusing to overwrite" >&2 + exit 1 +fi + +bold "→ forking $SRC" +info " destination : $DEST" +info " fleet name : $FLEET_NAME" +echo + +# ── 1. Mirror the reusable tooling ────────────────────────────────── +# Excludes are explicit so adding a new fleet-specific dir to the +# source doesn't accidentally leak into forks. +mkdir -p "$DEST" +rsync -a \ + --exclude='.git/' \ + --exclude='.claude/' \ + --exclude='stacks-mirror/' \ + --exclude='servers/*' \ + --exclude='stacks/*' \ + --exclude='configs/' \ + --exclude='docs/orientation.md' \ + --exclude='docs/runbooks/' \ + --exclude='docs/pfi/' \ + --exclude='MEMORY.md' \ + --exclude='STATUS.md' \ + --exclude='README.md' \ + --exclude='CLAUDE.md' \ + --exclude='playbooks/deploy-*' \ + --exclude='playbooks/decouple-*' \ + --exclude='scripts/upgrade-irv-ml1-cuda.sh' \ + "$SRC/" "$DEST/" + +info "tooling copied" + +# ── 2. Replace gitkeeps in the now-empty fleet dirs ───────────────── +mkdir -p "$DEST/servers" "$DEST/stacks" "$DEST/stacks-mirror" \ + "$DEST/configs" "$DEST/docs/runbooks" +touch "$DEST/servers/.gitkeep" "$DEST/stacks/.gitkeep" \ + "$DEST/configs/.gitkeep" "$DEST/docs/runbooks/.gitkeep" +# stacks-mirror is gitignored — no .gitkeep needed. + +# ── 3. Skeleton CLAUDE.md ─────────────────────────────────────────── +cat > "$DEST/CLAUDE.md" <.yaml\` and run +\`scripts/elway --playbook ...\` instead of chaining +\`ssh -t host 'sudo …'\` commands — handles sudo once lazily, +structured pass/change/fail reporting, idempotency via +\`creates:\` / \`when:\` / \`changed_when:\`. Template: +[\`playbooks/elway-smoke.yaml\`](playbooks/elway-smoke.yaml). + +**Task visibility via task-board.** If the Claude Code session has the +\`task-board\` plugin enabled (installed from +\`git@gitea.phasefinal.com:vh/task-board.git\`), a card tracks work +in progress. Hooks flip the card on turn boundaries automatically; +call \`task_start\` / \`task_update\` / \`task_wait\` / +\`task_complete\` MCP tools to set the activity subheader and post +meaningful log entries. + +When you launch a \`Bash\` tool with \`run_in_background: true\` (or +any long-running shell / monitor / poll loop), call +\`task_set_shells\` with one short description per active background +shell — and call it again with the updated list (or \`[]\`) when one +completes. The board flips a waiting card to **orange** while the +list is non-empty so the user can tell at a glance the session is +parked on background work, not stalled on them. Hooks have no way +to enumerate the bg-task list externally, so this is on the +assistant. + +## Purpose + +- Inventory of servers and their state +- Canonical copies of Docker Compose stacks deployed on those servers +- Scripts for inspecting and managing the infrastructure +- Conventions so all stacks look the same + +This is a **reference workspace** — the authoritative copies of +compose files and configs live **on the servers** under +\`/opt/docker/compose//\` and \`/opt/docker/conf//\`. +This workspace mirrors them for version control, editing, and planning. + +## Conventions (enforce for every new stack) + +- **Compose location on server:** \`/opt/docker/compose//compose.yaml\` +- **Config mounts on server:** \`/opt/docker/conf//...\` +- **Networks:** external \`traefik-net\`, aliased as \`tnet\` in compose + + \`\`\`yaml + networks: + tnet: + name: traefik-net + external: true + \`\`\` + +- **GPU reservation:** prefer \`deploy.resources.reservations.devices\` + with explicit \`device_ids\` for pinning + + \`\`\`yaml + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ["1"] + capabilities: [gpu] + \`\`\` + +- **Tunables:** \`.env\` in the same directory as \`compose.yaml\` — + keep the compose file constant, edit the \`.env\` +- **Named volumes** for service state (pattern: \`_\`) +- **Bind mounts** only for: model files, config files + (\`/opt/docker/conf/...\`), docker socket where required +- **Restart policy:** \`restart: unless-stopped\` for daemons +- **Homepage labels** on user-facing services: + + \`\`\`yaml + labels: + - homepage.group=AI Systems + - homepage.name= + - homepage.icon=mdi- + - homepage.description= + - homepage.href=http://: + \`\`\` + +- **Healthchecks** on services that expose HTTP + +## Servers + +| Name | IP | Role | Details | +|------|-----|------|---------| +| _(none yet — \`scripts/add-host.sh \` to register)_ | | | | + +## How to refresh a server's state + +\`\`\`bash +scripts/refresh-server-info.sh # show help +scripts/refresh-server-info.sh all # every host +scripts/refresh-server-info.sh # one host +scripts/refresh-server-info.sh --validate-only all +\`\`\` + +To register a new server: + +\`\`\`bash +scripts/add-host.sh +scripts/refresh-server-info.sh +\`\`\` + +## Stack tree convention (canonical vs mirror) + +| tree | role | git | who writes | who reads | +|---|---|---|---|---| +| \`stacks//\` | **canonical / intent** | tracked | you / Claude | \`deploy-stack.sh\` | +| \`stacks-mirror///\` | **snapshot / reality** | gitignored | \`sync-stacks.sh\` | drift inspection | + +\`\`\`bash +\$EDITOR stacks//compose.yaml +scripts/deploy-stack.sh # diffs vs live, prompts y/N +scripts/sync-stacks.sh # drift check across all hosts +diff -ru stacks// stacks-mirror/// +\`\`\` + +## Working rules + +- **Copies, not symlinks.** Files here reflect what's on the server + at the time of the last sync. Editing here doesn't change the + server until you deploy. +- **Never commit secrets.** Use \`.env.example\` templates; real + \`.env\` files live on the server (gitignored). +- **Surgical edits.** When fixing one stack, don't touch unrelated ones. +- **Sanity-check before deploying.** Run \`docker compose config\` + (dry parse) before \`docker compose up -d\` on the server. +EOF + +# ── 4. Skeleton README.md ─────────────────────────────────────────── +cat > "$DEST/README.md" < + +# Pull a snapshot of its current state +scripts/refresh-server-info.sh + +# Smoke-test the elway runner against it +scripts/elway --playbook playbooks/elway-smoke.yaml +\`\`\` + +## Layout + +\`\`\` +${FLEET_NAME}-management/ +├── CLAUDE.md # workspace conventions + assistant guidance +├── README.md # this file +├── scripts/ # reusable tooling +│ ├── elway # SSH-driven playbook runner (lazy sudo) +│ ├── server_inspect.sh +│ ├── refresh-server-info.sh +│ ├── add-host.sh +│ ├── deploy-stack.sh +│ ├── sync-stacks.sh +│ ├── add-digest-user.sh +│ └── fork-fleet.sh # bootstrap another fleet from this one +├── playbooks/ +│ └── elway-smoke.yaml # template playbook +├── servers// # one dir per registered host +└── stacks// # one dir per canonical stack +\`\`\` + +## Onboarding the assistant + +This workspace is intended to be opened in a Claude Code session +named after the fleet (e.g. \`${FLEET_NAME}-infra\`). The +\`CLAUDE.md\` file is auto-loaded and gives the assistant fleet +conventions and tool wiring. +EOF + +# ── 5. Skeleton STATUS.md (optional rolling-status doc) ───────────── +cat > "$DEST/STATUS.md" </dev/null; then + git -c user.email=fork@local -c user.name=fork-fleet \ + commit -q -m "Initial commit: forked from infra-management template + +Stripped fleet-specific content; kept the reusable tooling +(scripts/, elway-smoke.yaml template, conventions section of CLAUDE.md, +.gitignore). Fleet name: ${FLEET_NAME}." +fi + +bold "" +bold "✓ forked into $DEST" +echo +echo "next steps:" +echo " cd $DEST" +echo " # 1. Edit CLAUDE.md — fill in the Servers table + any placement rules" +echo " # 2. Add your first host" +echo " scripts/add-host.sh " +echo " scripts/refresh-server-info.sh " +echo " # 3. Wire up a remote when ready" +echo " tea repo create vh/${FLEET_NAME}-management # or whatever namespace" +echo " git remote add origin git@gitea.phasefinal.com:vh/${FLEET_NAME}-management.git" +echo " git push -u origin main"