lilith-platform.live/tooling/git-hooks/pre-push
2026-04-19 01:24:20 -07:00

146 lines
5.5 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# pre-push — when pushing to main, generate any missing adversary-view sidecars
# so the rsync deploy step always ships up-to-date visualizations.
#
# Install (from repo root):
# git config core.hooksPath tooling/git-hooks
# Or symlink:
# ln -sfn ../../tooling/git-hooks/pre-push .git/hooks/pre-push
#
# git pre-push hook contract:
# stdin lines: <local_ref> <local_sha> <remote_ref> <remote_sha>
# non-zero exit aborts the push.
#
# Exit behaviour:
# 0 — sidecars up to date, or service unreachable (push proceeds with warning)
# 1 — at least one sidecar generation failed (push blocked — run deploy:quinn to fix)
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
ENV_FILE="${REPO_ROOT}/deployments/@domains/quinn.www/.env.production"
IMAGE_PROTECTION_DIR="${REPO_ROOT}/codebase/@features/image-protection/backend-api"
# Capture the push range while checking for a main-branch push.
PUSHING_MAIN=false
LOCAL_SHA=""
REMOTE_SHA=""
while IFS=' ' read -r _local_ref local_sha remote_ref remote_sha; do
if [[ "${remote_ref}" == "refs/heads/main" ]]; then
PUSHING_MAIN=true
LOCAL_SHA="${local_sha}"
REMOTE_SHA="${remote_sha}"
break
fi
done
if [[ "${PUSHING_MAIN}" != "true" ]]; then
exit 0
fi
if [[ ! -f "${ENV_FILE}" ]]; then
echo "[pre-push] ⚠ Missing ${ENV_FILE} — skipping adversary-view check."
echo "[pre-push] Copy .env.production.example → .env.production to enable this gate."
exit 0
fi
if [[ ! -d "${IMAGE_PROTECTION_DIR}" ]]; then
echo "[pre-push] ⚠ image-protection backend not found at ${IMAGE_PROTECTION_DIR} — skipping."
exit 0
fi
# Load OUTPUT_DIR from the env file to determine which repo path to watch.
OUTPUT_DIR=""
# shellcheck disable=SC1090
OUTPUT_DIR="$(set -a; source "${ENV_FILE}"; set +a; echo "${OUTPUT_DIR:-}")"
if [[ -z "${OUTPUT_DIR}" ]]; then
echo "[pre-push] ⚠ OUTPUT_DIR not set in ${ENV_FILE} — skipping adversary-view check."
exit 0
fi
# Convert absolute OUTPUT_DIR to a repo-relative path for the git diff filter.
OUTPUT_DIR_REL="${OUTPUT_DIR#"${REPO_ROOT}/"}"
# Skip entirely when no protected photos changed in this push.
ZERO_SHA="0000000000000000000000000000000000000000"
if [[ "${REMOTE_SHA}" == "${ZERO_SHA}" ]]; then
DIFF_BASE="$(git hash-object -t tree /dev/null)"
else
DIFF_BASE="${REMOTE_SHA}"
fi
PHOTO_CHANGES="$(git diff --name-only "${DIFF_BASE}" "${LOCAL_SHA}" -- "${OUTPUT_DIR_REL}" 2>/dev/null | wc -l)"
if [[ "${PHOTO_CHANGES}" -eq 0 ]]; then
echo "[pre-push] No photo changes in this push — adversary-view check skipped."
exit 0
fi
echo "[pre-push] ${PHOTO_CHANGES} photo change(s) detected — ensuring adversary-view sidecars..."
ADVERSARIAL_URL="${ADVERSARIAL_URL:-http://localhost:8011}"
STARTED_ADVERSARIAL=false
# Auto-start imajin-adversarial if not healthy; kill it when we're done.
# GPU model loading takes up to ~120s.
if ! curl -sf "${ADVERSARIAL_URL}/health" > /dev/null 2>&1; then
echo "[pre-push] imajin-adversarial not healthy — auto-starting via manage-apps..."
manage-apps start imajin-adversarial apricot -d 2>&1 | sed 's/^/[pre-push] /' || true
STARTED_ADVERSARIAL=true
echo "[pre-push] Waiting for imajin-adversarial to become ready (up to 120s)..."
waited=0
until curl -sf "${ADVERSARIAL_URL}/health" > /dev/null 2>&1; do
if [[ "${waited}" -ge 120 ]]; then
echo "[pre-push] ⚠ imajin-adversarial did not start in time — adversary-view check skipped."
echo "[pre-push] Run 'manage-apps start imajin-adversarial apricot' and re-push to enforce the gate."
exit 0
fi
sleep 5
waited=$((waited + 5))
done
echo "[pre-push] ✓ imajin-adversarial ready after ${waited}s."
fi
ensure_exit=0
(
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
cd "${IMAGE_PROTECTION_DIR}"
# Install deps for the ensure:adversary script. The frozen-lockfile
# attempt may fail if the bun.lock is stale; the plain install may
# fail if unrelated workspace packages reference unpublished @lilith/*
# deps (bun resolves ALL workspace deps even from a single member dir,
# per the root bunfig.toml workspaces config). Either way, if deps are
# already cached the ensure:adversary script will succeed — so we
# allow install failures and let ensure:adversary be the real gate.
bun install --frozen-lockfile 2>/dev/null || bun install 2>/dev/null || true
bun run ensure:adversary
) || ensure_exit=$?
# Stop the service if we started it — no need to leave it running.
if [[ "${STARTED_ADVERSARIAL}" == "true" ]]; then
echo "[pre-push] Stopping imajin-adversarial (started by this hook)..."
manage-apps stop imajin-adversarial apricot 2>&1 | sed 's/^/[pre-push] /' || true
fi
# Exit code 2 = service unreachable despite auto-start (non-apricot host, GPU dead, etc).
# Warn and proceed — sidecars will be regenerated on next deploy.
if [[ "${ensure_exit}" -eq 2 ]]; then
echo "[pre-push] ⚠ imajin-adversarial unreachable — adversary-view check skipped."
echo "[pre-push] Run 'manage-apps start imajin-adversarial apricot' and re-push to enforce the gate."
exit 0
fi
# Exit code 1 = at least one generation failed. Block the push so the
# deploy step never ships broken sidecars.
if [[ "${ensure_exit}" -ne 0 ]]; then
echo "[pre-push] ✖ Adversary-view generation failed (exit ${ensure_exit})."
echo "[pre-push] Resolve the failures above, then push again."
echo "[pre-push] Or bypass with: git push --no-verify"
exit 1
fi
echo "[pre-push] ✔ Adversary-view sidecars up to date."