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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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})"
|
||||
|
||||
Executable
+143
@@ -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
|
||||
Reference in New Issue
Block a user