Files
esh-pfi-infrastructure/scripts/fork-fleet.sh
T
vh e6da607767 chore(task-board): mothball it; superseded by the High Seat and ledger
Operator ruling 2026-09-24. On ana-docker the stack is `docker compose
down`: the container is removed and port 7878 is closed. Kept for revival:
- the data dir /opt/docker/conf/task-board/data (tasks.db, last written
  2026-09-11)
- the task-board:local image
- stacks/task-board/ and the host's compose + .env

The Uptime Kuma monitor (id 3) was deleted before the stop so it could
not page, and its row is removed from monitors.yaml. Homepage drops the
card on its own, since it reads the container's labels.

Hooks: the container log showed no hook POSTs in 30 days. The only
traffic was open browser tabs holding /events, and the plugin was already
uninstalled on nh3-dev. Removed the paragraph that told sessions to call
task_* tools (CLAUDE.md, and the fork-fleet.sh template that seeds new
repos), the vestigial TASK_BOARD_SESSION env in .claude/settings.json, and
the listings in README and FLEETTOOLS.
2026-09-24 09:22:50 -07:00

315 lines
11 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).
## 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"