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
This commit is contained in:
Executable
+332
@@ -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 <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"
|
||||
Reference in New Issue
Block a user