146 lines
5.5 KiB
Bash
Executable file
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."
|