feat: detect repo drift in preflight + add scripts/update.sh

Two-part addition for the most common stale-setup pattern: user cloned
weeks ago, master has moved (Genesis pin bumps, compose changes, vendored
patch updates), they re-run their compose, hit a stale config, and file
an issue we already solved on master. Wispborne's _register_op_once and
GuiPerPT's pre-pull boot OOM both surfaced through this loop.

scripts/preflight.sh — preflight_repo_drift:
  - Skips silently if not a git repo, on a non-master branch, or if
    PREFLIGHT_NO_FETCH=1 (offline rigs / CI / forks tracking elsewhere).
  - Verifies origin remote is noonghunna/club-3090 (avoids false positives
    on forks pointing elsewhere).
  - timeout 5 git fetch --quiet origin master — bounded so flaky networks
    don't block boot.
  - On behind > 0: WARN with commit count, last-fetch age (h/d), and the
    one-line fix command. Soft-warning, never blocks. Tells user about
    PREFLIGHT_NO_FETCH=1 for opting out.

Wired into both launch.sh and switch.sh, alongside the existing
preflight_genesis_pin so users get one consolidated stale-setup signal.

scripts/update.sh — the easy upgrade path:
  - Refuses on dirty tree (git status --porcelain) — surfaces the local
    edits and tells the user to commit or stash first. We don't clobber
    the rare user who's been editing a compose locally.
  - Refuses on non-master branch — feature branches and fork-trackers
    should pull manually; this script is the master-from-origin path.
  - git pull --ff-only — no merge commits, no rebase ambiguity. Diverged
    branches get an explicit error pointing at git pull --rebase.
  - Re-runs setup.sh (idempotent — re-pins Genesis, re-vendors Marlin).
  - Tells the user to restart their variant via switch.sh — doesn't auto-
    restart, so they can A/B old-vs-new if they want.
  - --dry-run shows the plan without changing anything.
  - --force re-runs setup.sh even when up-to-date (for "I edited Genesis
    by hand and want it re-pinned" cases).

Why detection-then-explicit-command instead of "press y to auto-update":
the user will rarely have local commits (they're consumers of the recipes,
not vLLM contributors), but we still want consent — they should see what
they're committing to. The dirty-tree guard handles the rare custom-edit
case without nagging the common path.
This commit is contained in:
noonghunna
2026-05-03 12:17:04 +00:00
parent 673fa5bbcc
commit 43fe2a4e20
4 changed files with 219 additions and 3 deletions
+1
View File
@@ -55,6 +55,7 @@ if [[ $SKIP_PREFLIGHT -eq 0 ]]; then
preflight_gpu_idle
preflight_running
preflight_genesis_pin "${ROOT_DIR}"
preflight_repo_drift "${ROOT_DIR}"
echo "[preflight] ok."
echo ""
fi
+70
View File
@@ -10,6 +10,8 @@
# preflight_disk <path> <gb>— free space at path covers <gb> gigabytes
# preflight_gpu_idle — warn if GPUs have significant VRAM already in use
# preflight_running — warn if a club-3090 container is already up
# preflight_genesis_pin — warn if on-disk Genesis tree differs from setup.sh's pin
# preflight_repo_drift — warn if local HEAD is behind origin/master
#
# Style: each function prints one or more "[preflight] ..." lines.
# Hard failures get a one-line ERROR + a "Fix:" hint.
@@ -174,3 +176,71 @@ preflight_genesis_pin() {
fi
return 0
}
# preflight_repo_drift — warn if local HEAD is behind origin/master.
# Catches the most common stale-setup pattern: user cloned weeks ago, master
# has moved (Genesis pin bumps, compose changes, vendored patch updates),
# they re-run their compose, hit a stale config, and file an issue we
# already solved on master.
#
# Behavior:
# - Skips silently if not in a git repo, on a non-master branch, or if
# PREFLIGHT_NO_FETCH=1 (offline rigs / CI / forks tracking elsewhere).
# - Runs 'git fetch --quiet origin master' (~1-2s online).
# - Compares local HEAD vs origin/master. Behind > 0 → WARN with the
# count + last-fetch age + the one-line fix command.
# - Returns 0 always; soft-warning only.
preflight_repo_drift() {
local repo_root="${1:-${ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}"
# Fast bail-outs — silent.
[[ "${PREFLIGHT_NO_FETCH:-0}" == "1" ]] && return 0
[[ -d "${repo_root}/.git" ]] || return 0
command -v git >/dev/null 2>&1 || return 0
# Only check on master — on a feature branch, "behind master" is expected
# state, not drift. Forks / contributors live there.
local current_branch
current_branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null)
[[ "$current_branch" == "master" ]] || return 0
# Verify origin remote points at noonghunna/club-3090. If they've forked
# and re-pointed origin elsewhere, we don't know what's "behind."
local origin_url
origin_url=$(git -C "$repo_root" config --get remote.origin.url 2>/dev/null)
[[ "$origin_url" == *"noonghunna/club-3090"* ]] || return 0
# Fetch silently. 5s timeout so we don't hang on flaky networks.
if ! timeout 5 git -C "$repo_root" fetch --quiet origin master 2>/dev/null; then
# Network failure / timeout — don't make this fatal or even noisy.
return 0
fi
local behind
behind=$(git -C "$repo_root" rev-list --count HEAD..origin/master 2>/dev/null)
[[ -z "$behind" || "$behind" == "0" ]] && return 0
# Last-fetch age. FETCH_HEAD's mtime is the cleanest proxy.
local fetch_head="${repo_root}/.git/FETCH_HEAD"
local age_str=""
if [[ -f "$fetch_head" ]]; then
local now mtime age_sec
now=$(date +%s)
mtime=$(stat -c %Y "$fetch_head" 2>/dev/null || stat -f %m "$fetch_head" 2>/dev/null)
if [[ -n "$mtime" ]]; then
age_sec=$(( now - mtime ))
if (( age_sec < 60 )); then age_str="just now"
elif (( age_sec < 3600 )); then age_str="${age_sec}s ago" # < 1h, surface seconds
elif (( age_sec < 86400 )); then age_str="$(( age_sec / 3600 ))h ago"
else age_str="$(( age_sec / 86400 ))d ago"; fi
fi
fi
echo "[preflight] WARN: Your club-3090 checkout is ${behind} commit(s) behind origin/master." >&2
[[ -n "$age_str" ]] && echo "[preflight] (last origin fetch: ${age_str})" >&2
echo "[preflight] Master may have new configs, patches, or Genesis pin bumps." >&2
echo "[preflight] Easy upgrade: bash scripts/update.sh" >&2
echo "[preflight] (Will refuse if you have local edits — commit or stash first.)" >&2
echo "[preflight] Skip this check: PREFLIGHT_NO_FETCH=1 bash scripts/launch.sh" >&2
return 0
}
+5 -3
View File
@@ -144,13 +144,15 @@ up_variant() {
fi
# Pre-up sanity: warn if the on-disk Genesis tree is out of sync with
# the GENESIS_PIN declared in setup.sh. Catches "user pulled latest but
# didn't re-run setup.sh" failure mode (see club-3090#32 for context).
# Soft-warns via stderr; does not block boot.
# the GENESIS_PIN declared in setup.sh (catches "user pulled latest but
# didn't re-run setup.sh") AND if the repo itself is behind origin/master
# (catches "user cloned weeks ago, never pulled"). Both soft-warn via
# stderr and don't block boot. See club-3090#32 for the original case.
if [[ -f "${ROOT_DIR}/scripts/preflight.sh" ]]; then
# shellcheck source=preflight.sh
source "${ROOT_DIR}/scripts/preflight.sh"
preflight_genesis_pin "${ROOT_DIR}" || true
preflight_repo_drift "${ROOT_DIR}" || true
fi
echo "[switch] bringing up: ${v} (${dir}/${file})"
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
#
# Pull the latest club-3090 stack and re-run setup. The one-shot upgrade path.
#
# What this does (in order):
# 1. Refuses to run on a dirty tree — commit or stash your local edits first.
# 2. Bails on a non-master branch (forks / contributors should pull manually).
# 3. git pull --ff-only origin master (no merge commits, no rebase ambiguity)
# 4. bash scripts/setup.sh <model> (re-pins Genesis, re-vendors Marlin —
# idempotent, fast on second run)
# 5. Tells you to restart whatever variant you had running.
#
# What this does NOT do:
# - Pull the vLLM image (the SHA is pinned in compose; setup.sh's
# instructions cover the docker pull when needed).
# - Restart your container — that's a deliberate user action, in case you
# want to A/B old vs new before bringing the new variant up.
#
# Usage:
# bash scripts/update.sh # uses default model (qwen3.6-27b)
# bash scripts/update.sh qwen3.6-27b # explicit
# bash scripts/update.sh --dry-run # show what would happen, change nothing
# bash scripts/update.sh --force # skip the "behind origin" check (re-runs setup anyway)
#
# Exit codes:
# 0 — already up-to-date or successfully updated
# 1 — dirty tree / wrong branch / missing dep / git pull failed / setup.sh failed
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
MODEL="${MODEL:-qwen3.6-27b}"
DRY_RUN=0
FORCE=0
# --- arg parsing ---
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--force) FORCE=1; shift ;;
-h|--help)
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;;
-*) echo "Unknown flag: $1"; exit 1 ;;
*) MODEL="$1"; shift ;;
esac
done
cd "$ROOT_DIR"
run() {
# Echo + execute (or just echo, in dry-run).
echo "[update] \$ $*"
if [[ $DRY_RUN -eq 0 ]]; then
"$@"
fi
}
# --- 1. dep checks ---
command -v git >/dev/null 2>&1 || { echo "[update] ERROR: 'git' not found in PATH." >&2; exit 1; }
[[ -d "${ROOT_DIR}/.git" ]] || { echo "[update] ERROR: ${ROOT_DIR} is not a git repo." >&2; exit 1; }
# --- 2. branch check ---
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ "$current_branch" != "master" ]]; then
echo "[update] ERROR: not on master (current: ${current_branch})." >&2
echo " This script only updates the master branch from origin." >&2
echo " If you're on a feature branch, switch first: git checkout master" >&2
echo " If you've forked, pull manually from your fork's upstream." >&2
exit 1
fi
# --- 3. dirty-tree check ---
# git status --porcelain prints one line per modified / untracked file.
# Empty output = clean tree. We refuse to operate on dirty trees so we
# don't clobber the rare user who's edited a compose locally.
dirty=$(git status --porcelain 2>/dev/null)
if [[ -n "$dirty" ]]; then
echo "[update] ERROR: working tree has local changes — refusing to update." >&2
echo "" >&2
echo "$dirty" | sed 's/^/ /' >&2
echo "" >&2
echo " Commit or stash your changes first, then re-run:" >&2
echo " git stash && bash scripts/update.sh && git stash pop" >&2
echo " Or if these were experiments you don't need:" >&2
echo " git restore . && git clean -fd && bash scripts/update.sh" >&2
exit 1
fi
# --- 4. fetch + behind check ---
echo "[update] fetching origin/master..."
if [[ $DRY_RUN -eq 1 ]]; then
echo "[update] (dry-run) git fetch --quiet origin master"
else
git fetch --quiet origin master || { echo "[update] ERROR: git fetch failed." >&2; exit 1; }
fi
behind=$(git rev-list --count HEAD..origin/master 2>/dev/null || echo 0)
ahead=$(git rev-list --count origin/master..HEAD 2>/dev/null || echo 0)
if [[ "$behind" == "0" && "$ahead" == "0" ]]; then
echo "[update] up-to-date with origin/master."
if [[ $FORCE -eq 0 ]]; then
echo "[update] (use --force to re-run setup.sh anyway, e.g. after a manual edit)"
exit 0
fi
echo "[update] --force passed; re-running setup.sh anyway."
elif [[ "$ahead" != "0" && "$behind" == "0" ]]; then
echo "[update] you are ${ahead} commit(s) ahead of origin/master (no remote changes)."
echo " Nothing to pull. If you want to push: git push origin master"
exit 0
elif [[ "$ahead" != "0" && "$behind" != "0" ]]; then
echo "[update] ERROR: branch has diverged (${ahead} ahead, ${behind} behind)." >&2
echo " Resolve manually: git pull --rebase (or) git pull origin master" >&2
exit 1
else
echo "[update] ${behind} commit(s) behind origin/master — pulling..."
fi
# --- 5. pull (only if behind) ---
if [[ "$behind" != "0" ]]; then
run git pull --ff-only origin master
fi
# --- 6. re-run setup.sh ---
echo ""
echo "[update] re-running setup.sh ${MODEL} (re-pins Genesis, re-vendors Marlin)..."
echo ""
run bash "${ROOT_DIR}/scripts/setup.sh" "$MODEL"
# --- 7. next-step hint ---
echo ""
echo "[update] ✓ done."
echo ""
running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E '^(vllm-qwen36-27b|llama-cpp-qwen36-27b)' | head -1 || true)
if [[ -n "$running" ]]; then
echo "[update] A club-3090 container is currently running: ${running}"
echo "[update] To pick up the latest config + Genesis tree, restart it:"
echo "[update] bash scripts/switch.sh <variant>"
echo "[update] (Use 'bash scripts/switch.sh --list' to see available variants.)"
else
echo "[update] Next: bash scripts/launch.sh (or bash scripts/switch.sh <variant>)"
fi