Dankclaw's endpoints are re-split. frank-large is the halogen-flash-server quadlet again -- the 2026-09-13 unit file was never touched by the two-day ik_llama.cpp interlude, so the runtime was still in the repo and its image is still in local storage -- retuned to the shape asked for: HALOGEN_CTX 200192 with a 400,384-position pool and two slots, exactly two 200k conversations. frank-small is new: mainline llama.cpp on the RTX 3090 serving ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF at IQ3_S, using the -mtp build (it carries blk.64.nextn.*, which is what lets --spec-type draft-mtp run on the main model's own weights instead of a second 12 GiB copy) plus the BF16 vision projector, as one 131,072-token slot with f16 KV on port 7771. The host is shared now, and that is what the halogen retune pays for: HALOGEN_MAX_TOK 24576 (arena 20.6 -> 16.4 GiB, ~4-5% prefill) and the vision tower dropped (1.97 GiB) hand ~6 GiB back to the host, and the reserve rises to 20 to keep the fit check honest. Vision moved wholesale to frank-small. Retired to scripts/.AGENTTRASH/: the ik trio (env, service, serve script), the Qwen-Sharp template only that runtime read, and four benchmark harnesses bound to retired runtimes. Because retiring a file makes any test that greps it pass by absence, the seams assert the archive exists AND the live path does not.
263 lines
8.6 KiB
Bash
Executable File
263 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy.sh — push main, pull one target, and deploy only selected services.
|
|
#
|
|
# Planning is path-aware and happens before the push. The target-side runner
|
|
# then applies only those services, with per-service deployment revisions kept
|
|
# outside the Git checkout. A docs-only commit is a no-op. An LLM service is
|
|
# restarted only when its own files (or the shared chat template) are selected.
|
|
#
|
|
# Usage:
|
|
# ./scripts/deploy.sh <dankclaw|dankserver|frankbook-pro> [--service NAME]... [--dry-run]
|
|
#
|
|
# Without --service, the planner selects services changed since origin/main and
|
|
# the remote runner also applies pending service changes from its state store.
|
|
# --service intentionally restricts the run to the named services; it is useful
|
|
# for a safe, explicit retry after another service failed.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
PLANNER="$SCRIPT_DIR/deploy-plan.py"
|
|
REMOTE_USER="dank"
|
|
REMOTE_DIR="/home/dank/serverAppConfigs"
|
|
REMOTE_SHELL="bash"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: deploy.sh <dankclaw|dankserver|frankbook-pro> [options]
|
|
|
|
Options:
|
|
--service NAME deploy only NAME (repeatable; bypasses automatic planning)
|
|
--dry-run show the plan without pushing or connecting to a target
|
|
--skip-e2e skip the mandatory Playwright live-fleet gate (emergency only)
|
|
-h, --help show this help
|
|
|
|
Examples:
|
|
./scripts/deploy.sh dankclaw
|
|
./scripts/deploy.sh dankclaw --service searxng
|
|
./scripts/deploy.sh dankserver --service frank-medium
|
|
./scripts/deploy.sh dankclaw --dry-run
|
|
EOF
|
|
}
|
|
|
|
TARGET="${1:-}"
|
|
if [[ "$TARGET" == "-h" || "$TARGET" == "--help" ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
if [[ -z "$TARGET" ]]; then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
shift
|
|
|
|
case "$TARGET" in
|
|
dankclaw)
|
|
REMOTE_HOST="dankclaw"
|
|
;;
|
|
dankserver)
|
|
REMOTE_HOST="dankserver"
|
|
;;
|
|
frankbook-pro)
|
|
REMOTE_HOST="frankbook-pro"
|
|
REMOTE_USER="dank"
|
|
REMOTE_DIR="/Users/dank/Projects/serverAppConfigs"
|
|
# macOS ships bash 3.2; the runner needs bash >= 4 (associative
|
|
# arrays). /usr/local/bin/bash is the Intel-homebrew interpreter,
|
|
# provisioned with the Mac checkout.
|
|
REMOTE_SHELL="/usr/local/bin/bash"
|
|
;;
|
|
*)
|
|
echo "Unknown target: $TARGET" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
DRY_RUN=0
|
|
SKIP_E2E=0
|
|
EXPLICIT_SERVICES=()
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
shift
|
|
;;
|
|
--skip-e2e)
|
|
SKIP_E2E=1
|
|
shift
|
|
;;
|
|
--service)
|
|
[[ "$#" -ge 2 ]] || { echo "--service requires a name" >&2; exit 2; }
|
|
EXPLICIT_SERVICES+=("$2")
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
cd "$REPO_ROOT"
|
|
if [[ ! -x "$PLANNER" ]]; then
|
|
echo "FAIL: missing executable planner: $PLANNER" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# A deployment is a release operation, not a way to publish an arbitrary
|
|
# worktree. Include untracked files in this guard; the old diff-only check let
|
|
# an untracked secret or half-written config pass the pre-flight.
|
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
echo "FAIL: Local working tree has uncommitted changes." >&2
|
|
echo "Commit or remove them before deploying." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$(git branch --show-current)" != "main" ]]; then
|
|
echo "FAIL: deploy.sh must run from the main branch." >&2
|
|
exit 1
|
|
fi
|
|
|
|
HEAD_REVISION="$(git rev-parse HEAD)"
|
|
if BASE_REVISION="$(git rev-parse origin/main 2>/dev/null)"; then
|
|
:
|
|
else
|
|
BASE_REVISION="$(git rev-parse HEAD^ 2>/dev/null || git rev-parse HEAD)"
|
|
fi
|
|
|
|
valid_service() {
|
|
local service="$1"
|
|
case "$TARGET:$service" in
|
|
dankclaw:caddy|dankclaw:gluetun|dankclaw:searxng-network|dankclaw:searxng-valkey|\
|
|
dankclaw:searxng|dankclaw:marreta|dankclaw:pihole|dankclaw:open-webui|dankclaw:gitea-postgres|\
|
|
dankclaw:gitea-server|dankclaw:homepage|\
|
|
dankclaw:comfyui|dankclaw:hermes|dankclaw:hermes-dashboard|\
|
|
dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\
|
|
dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
|
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
|
|
dankclaw:frank-large|dankclaw:frank-small|\
|
|
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
|
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\
|
|
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
|
|
dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|\
|
|
dankserver:frank-medium|\
|
|
frankbook-pro:gluetun|frankbook-pro:qbittorrent|frankbook-pro:prowlarr|\
|
|
frankbook-pro:radarr|frankbook-pro:sonarr|frankbook-pro:lidarr|\
|
|
frankbook-pro:readarr)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
for service in "${EXPLICIT_SERVICES[@]}"; do
|
|
if ! valid_service "$service"; then
|
|
echo "Service '$service' is not deployed on $TARGET." >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
PLAN=()
|
|
if [[ "${#EXPLICIT_SERVICES[@]}" -gt 0 ]]; then
|
|
PLAN=("${EXPLICIT_SERVICES[@]}")
|
|
else
|
|
mapfile -t PLAN < <(
|
|
python3 "$PLANNER" --target "$TARGET" --repo "$REPO_ROOT" \
|
|
--base "$BASE_REVISION" --head "$HEAD_REVISION"
|
|
)
|
|
fi
|
|
|
|
# Preserve planner order while removing duplicate explicit service names.
|
|
UNIQUE_PLAN=()
|
|
declare -A SEEN=()
|
|
for service in "${PLAN[@]}"; do
|
|
[[ -n "$service" ]] || continue
|
|
if [[ -z "${SEEN[$service]:-}" ]]; then
|
|
UNIQUE_PLAN+=("$service")
|
|
SEEN[$service]=1
|
|
fi
|
|
done
|
|
PLAN=("${UNIQUE_PLAN[@]}")
|
|
|
|
printf '=== Deployment plan: %s ===\n' "$TARGET"
|
|
printf ' checkout: %s\n' "$HEAD_REVISION"
|
|
if [[ "${#PLAN[@]}" -eq 0 ]]; then
|
|
echo " changed services: (none; the remote will check pending service state)"
|
|
else
|
|
printf ' changed services:\n'
|
|
printf ' - %s\n' "${PLAN[@]}"
|
|
fi
|
|
|
|
REMOTE="${REMOTE_USER}@${REMOTE_HOST}"
|
|
REMOTE_ARGS=("$TARGET" "$HEAD_REVISION" "$REMOTE_DIR" "$REMOTE_SHELL")
|
|
if [[ "${#EXPLICIT_SERVICES[@]}" -eq 0 ]]; then
|
|
REMOTE_ARGS+=(--pending)
|
|
fi
|
|
REMOTE_ARGS+=("${PLAN[@]}")
|
|
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
echo "[dry-run] Would push main to origin."
|
|
echo "[dry-run] Would SSH to $REMOTE, pull with --ff-only, and run the service-scoped runner."
|
|
printf '[dry-run] Runner arguments:'
|
|
printf ' %q' "${REMOTE_ARGS[@]}"
|
|
printf '\n'
|
|
exit 0
|
|
fi
|
|
|
|
echo "[1/2] Pushing main to origin..."
|
|
git push origin main
|
|
|
|
echo "[2/2] Pulling and deploying on $REMOTE_HOST..."
|
|
# The target runner computes any pending revisions from its own state after
|
|
# the pull. This keeps a target that missed an earlier service deployment
|
|
# recoverable without restarting services that are already current.
|
|
ssh "$REMOTE" "$REMOTE_SHELL" -s -- "${REMOTE_ARGS[@]}" <<'REMOTE_EOF'
|
|
set -euo pipefail
|
|
TARGET="$1"
|
|
EXPECTED_REVISION="$2"
|
|
REPO_DIR="$3"
|
|
REMOTE_SHELL="$4"
|
|
shift 4
|
|
export REMOTE_DIR="$REPO_DIR"
|
|
cd "$REPO_DIR"
|
|
OLD_REVISION="$(git rev-parse HEAD)"
|
|
echo " pulling origin/main..."
|
|
git pull --ff-only origin main
|
|
NEW_REVISION="$(git rev-parse HEAD)"
|
|
if [[ "$NEW_REVISION" != "$EXPECTED_REVISION" ]]; then
|
|
echo "FAIL: target checkout is $NEW_REVISION, expected $EXPECTED_REVISION" >&2
|
|
exit 1
|
|
fi
|
|
exec "$REMOTE_SHELL" ./scripts/deploy-remote.sh "$TARGET" "$NEW_REVISION" "$@" \
|
|
--from-revision "$OLD_REVISION"
|
|
REMOTE_EOF
|
|
|
|
# --- Playwright e2e gate (MANDATORY after behavior-affecting deploys) ---
|
|
# The unit seams in tests/run.sh cannot see the live guard chain, LAN DNS, or
|
|
# the rendered homepage — exactly the layers that broke on 2026-09-12. This gate
|
|
# drives the LIVE fleet (Anubis -> Authentik outpost -> apps) and fails the
|
|
# deploy on any regression. It needs tests/e2e/.env.local with the qa-e2e IdP
|
|
# credentials (see .env.local.example). Use --skip-e2e for an emergency only.
|
|
if [[ "$SKIP_E2E" -eq 1 ]]; then
|
|
echo "[3/3] e2e gate SKIPPED (--skip-e2e): live fleet NOT verified." >&2
|
|
else
|
|
echo "[3/3] Running Playwright e2e gate against the live fleet..."
|
|
if bash "$REPO_ROOT/tests/e2e/run.sh"; then
|
|
echo "e2e gate PASSED."
|
|
else
|
|
echo "=== DEPLOY INCOMPLETE: e2e gate FAILED on $TARGET ===" >&2
|
|
echo "The live-fleet Playwright suite did not pass. Do NOT treat this" >&2
|
|
echo "deploy as good. Re-run manually: tests/e2e/run.sh" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo "=== Deploy complete: $TARGET ==="
|