px-explorer-v4 / push_hf.sh
BuildBot
push_hf: sparse-branch für HF-Push (nur Code, 0 LFS)
9644d0b
Raw
History Blame Contribute Delete
11.1 kB
#!/usr/bin/env bash
# push_hf.sh — Push das PX-Repo zu HuggingFace Space (nur Code, keine LFS-Orphans).
#
# Plan 2026-07-07: vermeidet 1 GB Storage-Limit auf HF free tier.
# 1. Erstellt einen frischen Sparse-Branch von ui-styling, der
# scratches/, telemetry/, sessions/, local_debug.log UND alle
# LFS-tracked files (logs/local_debug.log) AUS dem Index nimmt.
# 2. Pusht diesen Branch als "main" zu einem NEUEN HF Space.
# 3. Original-Branch ui-styling und Working-Tree bleiben 100% unangetastet.
#
# Verwendung:
# bash push_hf.sh # nutzt .env + px-explorer-v4 default
# HF_TOKEN=hf_xxx SPACE_NAME=px-explorer-v4 ./push_hf.sh # override aus env
#
# Standard-Space: px-explorer-v4 (User-Instruction 2026-07-09:
# "wir bleiben für immer auf v4. mal merken.").
# HF free-tier lehnt neue gradio-Spaces seit 2026-07 mit 402 ab.
#
# Voraussetzungen:
# - git lfs installiert (git-lfs >= 2.x)
# - HF_TOKEN env var (write-Scope auf User-Account)
# - Lokal: branch 'ui-styling' existiert mit dem Code
#
# Lessons Learned (2026-07-07):
# - HF free tier: 1 GB Storage account-weit. LFS-Orphans zählen mit.
# - DELETE /api/spaces/{id} → 404. Space-Löschung nur via Browser.
# - POST /api/spaces → 404. POST /api/repos/create → 200 (korrekter Endpoint).
# - 'git push --force' ist OK für HF Spaces (kein lokales Tracking).
# - 'git worktree add' scheitert wenn Branch im Hauptverzeichnis gecheckt ist.
set -euo pipefail
# ── .env laden (Plan 2026-07-09) ────────────────────────────────
# Lokale Credentials liegen in .env (HF_TOKEN), .env ist .gitignored.
# Falls .env nicht existiert, wird HF_TOKEN aus dem Env erwartet.
if [[ -f .env ]]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
# ── Konfiguration ────────────────────────────────────────────────
BRANCH_LOCAL="ui-styling"
BRANCH_HF="ui-styling-hf"
HF_USER="neuralworm"
# User-Instruction 2026-07-09: "wir bleiben für immer auf v4. mal merken."
# → v4 ist der permanente Space. Neue Spaces (v5/v6) werden seit 2026-07
# von HF free-tier mit 402 (PRO-only) abgelehnt.
SPACE_NAME="${SPACE_NAME:-px-explorer-v4}"
REMOTE_URL="https://${HF_USER}:${HF_TOKEN}@huggingface.co/spaces/${HF_USER}/${SPACE_NAME}"
# Was NIE in den Push soll (HF free tier 1 GB, diese Files füllen das Limit)
# ACHTUNG: sessions_bak3/temp_media/ enthält .png-Files die HF pre-receive-hook
# auch dann ablehnt, wenn sie nicht referenziert sind — Hook checkt das WORKING-TREE.
EXCLUDE_PATHS=(
"scratches/" # 575M emergence-Experimente
"telemetry/" # 20M Telemetrie-Snapshots
"sessions/" # 28M User-Chat-Historien (Daten, nicht Code)
"sessions_bak/" # Session-Backup (Daten, nicht Code)
"sessions_bak3/" # Session-Backup mit PNGs in temp_media/
"local_debug.log" # 1.8M Server-Debug-Log
"logs/local_debug.log" # LFS-pointer (verhindert LFS-Upload komplett)
)
# ── Sanity-Checks ────────────────────────────────────────────────
if [ -z "${HF_TOKEN:-}" ]; then
echo "ERROR: HF_TOKEN env var nicht gesetzt. Abbruch." >&2
exit 1
fi
if [ ! -d ".git" ]; then
echo "ERROR: nicht in einem Git-Repo ($(pwd)). Abbruch." >&2
exit 1
fi
if ! git rev-parse --verify "$BRANCH_LOCAL" >/dev/null 2>&1; then
echo "ERROR: branch '$BRANCH_LOCAL' existiert nicht lokal. Abbruch." >&2
exit 1
fi
# Aktuellen Working-Tree-Status checken (nichts darf modified sein)
if [ -n "$(git status --porcelain)" ]; then
echo "WARN: Working-Tree hat uncommitted changes:"
git status --short
if [ -t 0 ]; then
# Interaktiv: frage User
read -p "Trotzdem fortfahren? (j/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Jj]$ ]]; then
echo "Abbruch."
exit 1
fi
else
# Non-interaktiv (Pipe/Subshell): fortfahren
echo "Non-interactive mode: fortfahren..."
fi
fi
# ── HF Space erstellen (idempotent: skip wenn schon da) ────────
echo "=== HF Space '$SPACE_NAME' ==="
SPACE_CHECK=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${HF_TOKEN}" \
"https://huggingface.co/api/spaces/${HF_USER}/${SPACE_NAME}")
if [ "$SPACE_CHECK" = "200" ]; then
echo "Space existiert bereits. usedStorage:"
curl -s -H "Authorization: Bearer ${HF_TOKEN}" \
"https://huggingface.co/api/spaces/${HF_USER}/${SPACE_NAME}" \
| python3 -c "import sys, json; d=json.load(sys.stdin); print(f' {d.get(\"usedStorage\", 0)/1024/1024:.1f} MB / 1024 MB (1 GB free tier)')"
else
echo "Erstelle Space..."
CREATE_RESP=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: Bearer ${HF_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${SPACE_NAME}\",\"type\":\"space\",\"sdk\":\"gradio\"}" \
"https://huggingface.co/api/repos/create")
HTTP_CODE=$(echo "$CREATE_RESP" | tail -n1)
if [ "$HTTP_CODE" != "200" ]; then
echo "ERROR: Space-Erstellung fehlgeschlagen (HTTP $HTTP_CODE):" >&2
echo "$CREATE_RESP" | head -n1 >&2
exit 1
fi
echo "Space erstellt: https://huggingface.co/spaces/${HF_USER}/${SPACE_NAME}"
fi
# ── Sparse-Branch vorbereiten ───────────────────────────────────
echo "=== Sparse-Branch '$BRANCH_HF' ==="
# Wenn Branch existiert → löschen (sauberer Reset)
# Erst zurück zu BRANCH_LOCAL falls ui-styling-hf ausgecheckt ist
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "$BRANCH_HF" ]; then
echo "Branch '$BRANCH_HF' ist aktuell gecheckt — switche zu '$BRANCH_LOCAL'..."
# -f: force, falls working-tree-files nicht im Ziel-branch-index sind
# (passiert nach `git rm --cached` auf einem Branch, weil die Files nur
# aus dem Index, nicht aus dem Working-Tree, entfernt wurden).
git checkout -f "$BRANCH_LOCAL" >/dev/null
fi
if git rev-parse --verify "$BRANCH_HF" >/dev/null 2>&1; then
echo "Lösche existierenden Branch '$BRANCH_HF'..."
git branch -D "$BRANCH_HF" >/dev/null
fi
# WICHTIG: Wir bauen den Sparse-Branch als ORPHAN (kein Parent), damit
# die komplette History von $BRANCH_LOCAL (die noch alle LFS-tracked files
# in der History referenziert) NICHT mit-gepusht wird. Sonst triggert der
# pre-receive-hook "contains binary files" für ALLE je committeten
# .png/.bin files im Branch — auch wenn wir sie im aktuellen Index nicht
# haben.
echo "Branch '$BRANCH_HF' von '$BRANCH_LOCAL' erstellt (ORPHAN, ohne History)."
git checkout --orphan "$BRANCH_HF" >/dev/null
git checkout "$BRANCH_LOCAL" -- . 2>&1 | head -3 || true
# Excluded paths aus Index entfernen (Working-Tree bleibt unangetastet)
EXCLUDED_COUNT=0
for path in "${EXCLUDE_PATHS[@]}"; do
# Pfad-spezifische Behandlung
case "$path" in
*/)
# Verzeichnis
if [ -d "$path" ]; then
# git rm -r kann bei leeren/non-existent Pfaden failen — wir
# unterdrücken ALLE outputs und zählen nur die echten deletes.
RM_OUT=$(git rm -r --cached "$path" 2>&1 | grep -c "^rm " || true)
EXCLUDED_COUNT=$((EXCLUDED_COUNT + RM_OUT))
fi
;;
*)
# Einzelne Datei
if [ -f "$path" ]; then
if git rm --cached "$path" >/dev/null 2>&1; then
EXCLUDED_COUNT=$((EXCLUDED_COUNT + 1))
fi
fi
;;
esac
done
echo "Aus Index entfernt: ${EXCLUDED_COUNT} files (${EXCLUDE_PATHS[*]})"
# Falls es keine Änderungen gibt (alles schon exkludiert) — leeren Commit
if [ -z "$(git status --porcelain)" ]; then
echo "Keine Änderungen — Working-Tree war bereits clean."
else
git commit -m "push_hf: sparse-branch für HF-Push (nur Code, 0 LFS)" >/dev/null
echo "Sparse-Commit erstellt."
fi
# Code-Size + LFS-Check (ohne pipefail — xargs+stat kann non-zero exits haben
# bei symlinks oder special files; das ist hier OK)
set +o pipefail
CODE_BYTES=$(git ls-files | xargs -I {} stat -c '%s' {} 2>/dev/null | awk '{s+=$1} END {print s+0}')
set -o pipefail
CODE_MB=$(awk -v b="$CODE_BYTES" 'BEGIN {printf "%.2f", b/1024/1024}')
LFS_COUNT=$(git lfs ls-files 2>/dev/null | wc -l)
echo "Push-Größe: ${CODE_MB} MB Code, ${LFS_COUNT} LFS-Files."
# ── Push ────────────────────────────────────────────────────────
echo "=== Push zu $REMOTE_URL ==="
# Remote-URL setzen/updaten
git remote set-url hf "$REMOTE_URL" 2>/dev/null || git remote add hf "$REMOTE_URL"
# Force-Push (HF Spaces haben initial-commit, force ist OK)
# GIT_LFS_SKIP_PUSH=true verhindert dass git-lfs alle 238 LFS-Objekte im
# Local-Cache hochlädt (auch wenn der sparse-branch 0 LFS-Referenzen hat).
# Bei sparse-branches mit 0 LFS-files wollen wir NUR plain git push.
GIT_LFS_SKIP_PUSH=true PUSH_OUTPUT=$(GIT_LFS_SKIP_PUSH=true git push --force hf "$BRANCH_HF:main" 2>&1) || PUSH_EXIT=$?
PUSH_EXIT=${PUSH_EXIT:-0}
if [ "$PUSH_EXIT" = "0" ]; then
echo ""
echo "✓ Push erfolgreich!"
echo " Space: https://huggingface.co/spaces/${HF_USER}/${SPACE_NAME}"
echo " Code: ${CODE_MB} MB (kein LFS, keine Scratches)"
# Verify final storage
sleep 3
FINAL_STORAGE=$(curl -s -H "Authorization: Bearer ${HF_TOKEN}" \
"https://huggingface.co/api/spaces/${HF_USER}/${SPACE_NAME}" \
| python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('usedStorage', 0)/1024/1024)" 2>/dev/null)
echo " Storage: ${FINAL_STORAGE:-?} MB / 1024 MB"
else
echo "✗ Push fehlgeschlagen (exit $PUSH_EXIT):" >&2
echo "$PUSH_OUTPUT" | tail -10 >&2
echo " Mögliche Ursache: HF-Account-Storage-Limit erreicht." >&2
echo " Cleanup: alten Space im Browser löschen, dann SPACE_NAME ändern." >&2
# Cleanup trotzdem
git checkout -f "$BRANCH_LOCAL" >/dev/null 2>&1 || true
git branch -D "$BRANCH_HF" >/dev/null 2>&1 || true
exit 1
fi
# ── Zurück zu ui-styling ───────────────────────────────────────
echo ""
echo "=== Cleanup ==="
# -f: force, weil unversionierte Files (local_debug.log etc.) im Working-Tree
# den Checkout sonst blocken — sie existieren auf ui-styling als getrackt,
# wurden aber im Sparse-Branch aus dem Index entfernt. Working-Tree bleibt
# unangetastet (git checkout -f überschreibt nur Index-Mismatches).
git checkout -f "$BRANCH_LOCAL" >/dev/null
git branch -D "$BRANCH_HF" >/dev/null
echo "Zurück auf '$BRANCH_LOCAL'. Sparse-Branch gelöscht."
echo "Working-Tree ist 100% identisch zu vorher (Index-Cleanup war --cached-only)."