diff --git a/.claude/skills/deploy/SKILL.md b/.claude/skills/deploy/SKILL.md index 9977d2b..f172c01 100644 --- a/.claude/skills/deploy/SKILL.md +++ b/.claude/skills/deploy/SKILL.md @@ -31,38 +31,32 @@ invoking this skill is the authorization to proceed, but: already up to date and ask whether to restart anyway (they may want a plain restart). If `git fetch` fails (host unreachable, auth), STOP and report. -2. **Pull + hard restart** (single SSH session, halts on any error): +2. **Pull, ensure authlib, then `update-all.sh --hard`** (single SSH session, + halts on any error): ```bash ssh cochi 'set -e cd /home/ubuntu/mc/ulicraft-server-v1 git pull --ff-only - tooling/render-config.sh tooling/fetch-authlib.sh - docker compose down --remove-orphans - docker compose up -d --build' + ./update-all.sh --hard' ``` - `--ff-only` refuses to merge — if the pull is not a fast-forward, STOP and report (local changes on the host need manual resolution). - - `render-config.sh` re-renders drasl/nmsr/packwiz configs from `.env` - (needs `.env` complete: `BASE_DOMAIN`, `RCON_PASSWORD`, - `DISTRIBUTION_WEB_ROOT`, `CADDY_HTTP_*`, `REGISTRATION_MODE`). It halts on - an invalid `REGISTRATION_MODE` (must be `invite` or `open`). - `fetch-authlib.sh` ensures `runtime/authlib-injector.jar` exists (gitignored; - skips if already valid). Missing jar = minecraft crashloops with "Error - opening zip file or JAR manifest missing". - -2b. **Landing rebuild (only if needed).** The static site `www/` is gitignored — - `git pull` does NOT update it. If the pulled commits (from the step-1 - pre-check) touch `landing/`, or `REGISTRATION_MODE` changed, rebuild it; - otherwise skip: - ```bash - ssh cochi 'set -e - cd /home/ubuntu/mc/ulicraft-server-v1 - cd landing && set -a && . ../.env && set +a && pnpm run build' - ``` - Sourcing `.env` bakes `BASE_DOMAIN` + `REGISTRATION_MODE` (invite-field on/off) - into the output. A flag-only change just needs this rebuild + a `drasl` - restart (already covered by step 2's `up`), not the world downtime. + skips if already valid; NOT part of update-all since it rarely changes). + Missing jar = minecraft crashloops with "Error opening zip file or JAR + manifest missing". Run it before `update-all` so the jar is present when + compose brings minecraft up. + - **`update-all.sh` is the single source of truth** for the post-pull build + + restart steps (render drasl/nmsr configs from `.env`, sync server mods, regen + the landing mod list, rebuild `www/`, then apply via docker compose). It is + also runnable on its own for a lighter rolling update (no `--hard` = + change-detected restart, no world downtime). `--hard` here does + `down --remove-orphans && up -d --build` for the deliberate full restart. + `render-config.sh` halts on an invalid `REGISTRATION_MODE` (must be `invite` + or `open`); a missing distribution jar halts `sync-server-mods.sh`. The + landing rebuild is unconditional (the gitignored `www/` is never updated by + `git pull`), so no separate landing step is needed. 3. **Verify.** ```bash diff --git a/update-all.sh b/update-all.sh new file mode 100755 index 0000000..411cda0 --- /dev/null +++ b/update-all.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# update-all.sh — main post-pull orchestrator for the Ulicraft stack on the host. +# +# Run AFTER `git pull` (it does NOT pull or push). Single source of truth for the +# build + restart steps; the `deploy` skill is just `git pull --ff-only` followed +# by `./update-all.sh --hard`. +# +# Steps (always, in order): render configs → sync server mods → regen landing mod +# list → rebuild landing (www/) → apply via docker compose. +# +# Modes: +# (default, rolling) `docker compose up -d --build`, then restart ONLY the +# services whose mounted inputs actually changed — drasl/nmsr on a config +# re-render, minecraft on a mod change. Minimal downtime. NOTE: this path +# canNOT detect changes to caddy's static conf (caddy/conf.d/*.caddy) — it +# has no pre-pull baseline for them; use --hard for those. +# --hard `docker compose down --remove-orphans && up -d --build`. +# Full restart (Minecraft world downtime); picks up everything, caddy +# static conf included. +# +# Cautious by design: strict halt on ANY error (set -euo pipefail) — never a +# partial deploy. Fix the cause and re-run. Uses pnpm/node only (never npm). +set -euo pipefail + +cd "$(dirname "$0")" # repo root (this script lives at the root) + +# ── args ──────────────────────────────────────────────────────────── +HARD=0 +case "${1:-}" in + "") HARD=0 ;; + --hard) HARD=1 ;; + -h|--help) echo "usage: $0 [--hard]"; exit 0 ;; + *) echo "unknown arg: $1" >&2; echo "usage: $0 [--hard]" >&2; exit 2 ;; +esac + +step() { printf '\n\033[1;36m→ %s\033[0m\n' "$*"; } + +cfg_hash() { [ -f "$1" ] && md5sum "$1" | cut -d' ' -f1 || echo absent; } +# Cheap content proxy: jar filenames + sizes (content changes ⇒ size changes). +mods_hash() { + { find server-mods -maxdepth 1 -name '*.jar' -printf '%f %s\n' 2>/dev/null || true; } \ + | sort | md5sum | cut -d' ' -f1 +} + +# ── snapshot inputs (rolling mode only) ───────────────────────────── +if [ "$HARD" -eq 0 ]; then + before_drasl=$(cfg_hash drasl/config/config.toml) + before_nmsr=$(cfg_hash nmsr/config.toml) + before_mods=$(mods_hash) +fi + +# ── 1. render drasl/nmsr configs from .env (halts on bad input) ───── +step "render-config.sh" +tooling/render-config.sh + +# ── 2. mirror the server mod subset into ./server-mods ────────────── +step "sync-server-mods.sh" +tooling/sync-server-mods.sh + +# ── 3. regenerate the landing mod list (mods.json + logos) ────────── +step "build-modlist.py" +tooling/build-modlist.py + +# ── 4. rebuild the landing site into www/ (gitignored — pull never +# updates it, so it must be rebuilt every run) ───────────────── +step "landing build" +( + # Non-interactive shells lack node/pnpm on PATH — source nvm + pnpm. + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + export PNPM_HOME="$HOME/.local/share/pnpm" + export PATH="$PNPM_HOME/bin:$PATH" + command -v pnpm >/dev/null 2>&1 || { + echo "pnpm not on PATH (looked in nvm=$NVM_DIR, pnpm=$PNPM_HOME/bin)." >&2 + echo "Install pnpm + node (never npm) and re-run." >&2 + exit 1 + } + cd landing + set -a; . ../.env; set +a + pnpm run build +) + +# ── 5. apply via docker compose ───────────────────────────────────── +if [ "$HARD" -eq 1 ]; then + step "docker compose down + up (--hard, full restart)" + docker compose down --remove-orphans + docker compose up -d --build +else + step "docker compose up -d --build" + docker compose up -d --build + + # Rolling `up -d` does NOT reload changed bind-mounted configs/mods, so restart + # only the services whose inputs actually changed since the snapshot above. + restart=() + [ "$(cfg_hash drasl/config/config.toml)" != "$before_drasl" ] && restart+=(drasl) + [ "$(cfg_hash nmsr/config.toml)" != "$before_nmsr" ] && restart+=(nmsr) + [ "$(mods_hash)" != "$before_mods" ] && restart+=(minecraft) # mods ⇒ MC restart (downtime, required) + if [ "${#restart[@]}" -gt 0 ]; then + step "restart changed services: ${restart[*]}" + docker compose restart "${restart[@]}" + else + echo " (no config/mod changes — nothing to restart)" + fi +fi + +# ── 6. status summary ─────────────────────────────────────────────── +step "docker compose ps" +docker compose ps + +printf '\n\033[1;32m✓ update-all complete\033[0m\n'