765d89aed3
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
333 lines
12 KiB
Bash
Executable File
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"
|