Files
vh 765d89aed3 scripts: add fork-fleet.sh for bootstrapping a new fleet repo
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
2026-04-28 15:58:22 -07:00

333 lines
12 KiB
Bash
Executable File

#!/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 <destination-dir> [<fleet-name>]
#
# 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 <destination-dir> [<fleet-name>]}"
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" <<EOF
# CLAUDE.md
This workspace manages **${FLEET_NAME}** — servers, Docker stacks,
configs. Forked from a shared infra-management template; the
conventions below carry over, the server table + placement rules are
yours to fill in.
**New session starting here?** Once \`docs/orientation.md\` exists
read it first — fleet topology, backup architecture, governing
principles, and any host-specific gotchas.
**For SSH-driven work: use \`scripts/elway\`.** Write a playbook under
\`playbooks/<name>.yaml\` and run
\`scripts/elway <host> --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/<stack>/\` and \`/opt/docker/conf/<stack>/\`.
This workspace mirrors them for version control, editing, and planning.
## Conventions (enforce for every new stack)
- **Compose location on server:** \`/opt/docker/compose/<stack>/compose.yaml\`
- **Config mounts on server:** \`/opt/docker/conf/<stack>/...\`
- **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: \`<stack>_<name>\`)
- **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=<ServiceName>
- homepage.icon=mdi-<icon>
- homepage.description=<short>
- homepage.href=http://<host-ip>:<port>
\`\`\`
- **Healthchecks** on services that expose HTTP
## Servers
| Name | IP | Role | Details |
|------|-----|------|---------|
| _(none yet — \`scripts/add-host.sh <name> <ip>\` 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 <host> # one host
scripts/refresh-server-info.sh --validate-only all
\`\`\`
To register a new server:
\`\`\`bash
scripts/add-host.sh <name> <ip-or-user@ip>
scripts/refresh-server-info.sh <name>
\`\`\`
## Stack tree convention (canonical vs mirror)
| tree | role | git | who writes | who reads |
|---|---|---|---|---|
| \`stacks/<stack>/\` | **canonical / intent** | tracked | you / Claude | \`deploy-stack.sh\` |
| \`stacks-mirror/<host>/<stack>/\` | **snapshot / reality** | gitignored | \`sync-stacks.sh\` | drift inspection |
\`\`\`bash
\$EDITOR stacks/<stack>/compose.yaml
scripts/deploy-stack.sh <host> <stack> # diffs vs live, prompts y/N
scripts/sync-stacks.sh # drift check across all hosts
diff -ru stacks/<stack>/ stacks-mirror/<host>/<stack>/
\`\`\`
## 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" <<EOF
# ${FLEET_NAME}-management
Infrastructure-management workspace for the **${FLEET_NAME}** fleet —
inventory, canonical Docker compose stacks, deploy tooling.
Forked from the shared infra-management template; see
[\`CLAUDE.md\`](./CLAUDE.md) for conventions and tooling reference.
## Quick start
\`\`\`bash
# Register your first host
scripts/add-host.sh <name> <ip-or-user@ip>
# Pull a snapshot of its current state
scripts/refresh-server-info.sh <name>
# Smoke-test the elway runner against it
scripts/elway <name> --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/<name>/ # one dir per registered host
└── stacks/<stack>/ # 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" <<EOF
# Status + Open Issues
Last updated: $(date '+%Y-%m-%d')
Snapshot of fleet state and open work. Refresh when something material
changes (host added, stack deployed, incident closed).
## Fleet summary
_(none yet — populate after first \`scripts/add-host.sh\`)_
## Open work
_(empty)_
## Known issues
_(empty)_
EOF
info "skeleton CLAUDE.md / README.md / STATUS.md written"
# ── 6. Initialize fresh git history ─────────────────────────────────
cd "$DEST"
git init -q -b main
git add .
# Quiet local commit — no remote, no signing fuss. The user's normal
# git config picks up author. If the source repo has a different user
# committing here would inherit it; explicit fallback to env keeps the
# script working in CI/headless contexts where git config isn't set.
if ! git -c user.useConfigOnly=true 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}." 2>/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 <name> <ip-or-user@ip>"
echo " scripts/refresh-server-info.sh <name>"
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"