Files
ComfyUI-OpenClaw/scripts/pre_push_checks.sh
T

308 lines
9.6 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
set -o errtrace
trap 'echo "[pre-push] ERROR at line ${LINENO}: ${BASH_COMMAND}" >&2' ERR
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
echo "[pre-push] repo: $ROOT_DIR"
# Pre-commit cache strategy:
# - Always use repo-local cache to avoid readonly / locked user-level caches.
# - Windows is especially prone to cache lock issues (WinError 5), so we keep
# the cache local and aggressively reset it when manifest/permission errors occur.
UNAME_S="$(uname -s || true)"
case "$UNAME_S" in
MINGW*|MSYS*|CYGWIN*)
# CRITICAL (Windows): do NOT use the user-level ~/.cache/pre-commit path.
# It frequently leaves locked .exe files (WinError 5) and blocks hook cleanup.
export PRE_COMMIT_HOME="${PRE_COMMIT_HOME:-$ROOT_DIR/.tmp/pre-commit-win}"
mkdir -p "$PRE_COMMIT_HOME"
# Keep Black cache local to avoid AppData lock/permission errors.
export BLACK_CACHE_DIR="${BLACK_CACHE_DIR:-$ROOT_DIR/.tmp/black-cache}"
mkdir -p "$BLACK_CACHE_DIR"
;;
*)
export PRE_COMMIT_HOME="${PRE_COMMIT_HOME:-$ROOT_DIR/.tmp/pre-commit}"
mkdir -p "$PRE_COMMIT_HOME"
export BLACK_CACHE_DIR="${BLACK_CACHE_DIR:-$ROOT_DIR/.tmp/black-cache}"
mkdir -p "$BLACK_CACHE_DIR"
;;
esac
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "[pre-push] ERROR: missing command: $cmd" >&2
exit 1
fi
}
pip_install_or_fail() {
local why="$1"
shift
if "$VENV_PY" -m pip install "$@"; then
return 0
fi
echo "[pre-push] ERROR: failed to install dependency ($why): $*" >&2
exit 1
}
is_wsl() {
grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null
}
select_venv_dir() {
# Explicit override for advanced/local setups.
if [ -n "${OPENCLAW_TEST_VENV:-}" ]; then
echo "$OPENCLAW_TEST_VENV"
return 0
fi
case "$UNAME_S" in
MINGW*|MSYS*|CYGWIN*)
echo "$ROOT_DIR/.venv"
;;
*)
# IMPORTANT:
# In WSL, prefer dedicated Linux venv to avoid mixing with Windows .venv.
if is_wsl; then
echo "$ROOT_DIR/.venv-wsl"
else
echo "$ROOT_DIR/.venv"
fi
;;
esac
}
resolve_venv_python() {
case "$UNAME_S" in
MINGW*|MSYS*|CYGWIN*)
echo "$VENV_DIR/Scripts/python.exe"
;;
*)
echo "$VENV_DIR/bin/python"
;;
esac
}
is_venv_python_healthy() {
local venv_py="$1"
# CRITICAL: on Git Bash/Windows, `test -x` is unreliable for `.exe`.
# Use existence + actual interpreter execution probe instead.
# DO NOT replace with `-x` checks; that regression previously caused false
# "invalid venv" detection and fallback to the wrong interpreter.
[ -f "$venv_py" ] || return 1
"$venv_py" -c "import sys; print(sys.executable)" >/dev/null 2>&1
}
bootstrap_venv() {
local venv_py
venv_py="$(resolve_venv_python)"
if is_venv_python_healthy "$venv_py"; then
echo "$venv_py"
return 0
fi
if [ -e "$venv_py" ]; then
echo "[pre-push] WARN: existing venv is invalid; recreating: $VENV_DIR" >&2
rm -rf "$VENV_DIR"
fi
echo "[pre-push] INFO: creating project venv at $VENV_DIR ..." >&2
case "$UNAME_S" in
MINGW*|MSYS*|CYGWIN*)
# CRITICAL: on Git Bash, `python3` may resolve to MSYS `/usr/bin/python`,
# which creates a broken Windows venv (`No Python at "/usr/bin\python.exe"`).
# Always prefer Windows-native launchers/interpreters.
if command -v py.exe >/dev/null 2>&1; then
py.exe -3 -m venv "$VENV_DIR"
elif [ -x "/c/Windows/py.exe" ]; then
/c/Windows/py.exe -3 -m venv "$VENV_DIR"
elif command -v python.exe >/dev/null 2>&1; then
python.exe -m venv "$VENV_DIR"
elif command -v py >/dev/null 2>&1; then
py -3 -m venv "$VENV_DIR"
else
echo "[pre-push] ERROR: no Windows Python launcher found (py.exe/python.exe)." >&2
exit 1
fi
;;
*)
if command -v python3 >/dev/null 2>&1; then
python3 -m venv "$VENV_DIR"
elif command -v python >/dev/null 2>&1; then
python -m venv "$VENV_DIR"
else
echo "[pre-push] ERROR: no bootstrap Python found (python3/python)." >&2
exit 1
fi
;;
esac
if ! is_venv_python_healthy "$venv_py"; then
echo "[pre-push] ERROR: failed to initialize project venv: $VENV_DIR" >&2
exit 1
fi
echo "$venv_py"
}
pre_commit_cmd() {
"$VENV_PY" -m pre_commit "$@"
}
# CRITICAL: pre-push must always run pre-commit from project venv.
# Do not switch this back to global `pre-commit` command lookup.
# This prevents mixed global/user installs from hijacking hook execution.
VENV_DIR="$(select_venv_dir)"
VENV_PY="$(bootstrap_venv)"
if ! "$VENV_PY" -m pre_commit --version >/dev/null 2>&1; then
echo "[pre-push] INFO: installing pre-commit into project venv ($VENV_DIR) ..." >&2
pip_install_or_fail "required for pre-commit hooks" -U pip pre-commit
fi
if ! "$VENV_PY" -c "import black" >/dev/null 2>&1; then
# Keep black in the same interpreter used by local black-single hook.
echo "[pre-push] INFO: installing black into project venv ($VENV_DIR) ..." >&2
pip_install_or_fail "required by black-single hook" black==24.1.1
fi
# IMPORTANT:
# Pre-push now runs backend unit tests. Keep minimal runtime deps aligned with
# CI unit-test job to avoid "passes locally, fails on GitHub" drift.
if ! "$VENV_PY" -c "import numpy, PIL" >/dev/null 2>&1; then
echo "[pre-push] INFO: installing numpy/pillow into project venv ($VENV_DIR) ..." >&2
pip_install_or_fail "required by unit tests" numpy pillow
fi
if ! "$VENV_PY" -c "import aiohttp" >/dev/null 2>&1; then
echo "[pre-push] INFO: installing aiohttp into project venv ($VENV_DIR) ..." >&2
pip_install_or_fail "required by unit tests/import paths" aiohttp
fi
require_cmd npm
run_pre_commit_safe() {
local tmp_log
tmp_log="$(mktemp)"
local lower_log
lower_log="$(mktemp)"
reset_cache() {
echo "[pre-push] WARN: resetting pre-commit cache: ${PRE_COMMIT_HOME:-<unset>}" >&2
if [ -n "${PRE_COMMIT_HOME:-}" ] && [ -d "$PRE_COMMIT_HOME" ]; then
rm -rf "$PRE_COMMIT_HOME" 2>/dev/null || true
# On Git for Windows, rm may fail on locked files. Retry via cmd.exe.
if [ -d "$PRE_COMMIT_HOME" ] && command -v cygpath >/dev/null 2>&1 && command -v cmd.exe >/dev/null 2>&1; then
local pre_commit_home_win
pre_commit_home_win="$(cygpath -w "$PRE_COMMIT_HOME")"
cmd.exe /c "rmdir /s /q \"$pre_commit_home_win\"" >/dev/null 2>&1 || true
fi
mkdir -p "$PRE_COMMIT_HOME"
fi
}
if pre_commit_cmd "$@" 2>&1 | tee "$tmp_log"; then
rm -f "$tmp_log"
rm -f "$lower_log"
return 0
fi
tr '[:upper:]' '[:lower:]' < "$tmp_log" > "$lower_log" || true
if grep -q "invalidmanifesterror" "$lower_log"; then
echo "[pre-push] WARN: pre-commit cache manifest is corrupted; running clean + cache reset + single retry." >&2
if ! pre_commit_cmd clean; then
echo "[pre-push] WARN: 'pre-commit clean' failed; trying manual cache reset." >&2
reset_cache
fi
reset_cache
pre_commit_cmd "$@"
rm -f "$tmp_log"
rm -f "$lower_log"
return 0
fi
if grep -q "permissionerror" "$lower_log" || grep -q "winerror 5" "$lower_log" || grep -q "access is denied" "$lower_log"; then
# CRITICAL: treat lock-file errors as cache corruption and self-heal once.
# Avoid removing this block; without it, pre-push can hang/fail repeatedly on Windows.
echo "[pre-push] WARN: pre-commit cache appears locked by another process; running cache reset + single retry." >&2
reset_cache
if [ -n "${BLACK_CACHE_DIR:-}" ] && [ -d "$BLACK_CACHE_DIR" ]; then
rm -rf "$BLACK_CACHE_DIR" 2>/dev/null || true
mkdir -p "$BLACK_CACHE_DIR"
fi
pre_commit_cmd "$@"
rm -f "$tmp_log"
rm -f "$lower_log"
return 0
fi
rm -f "$tmp_log"
rm -f "$lower_log"
return 1
}
# Ensure Node 18+ for Playwright/E2E.
# CI uses Node 20; local baseline is Node 18.
if [ -n "${NVM_DIR:-}" ] && [ -s "${NVM_DIR}/nvm.sh" ]; then
# shellcheck disable=SC1090
. "${NVM_DIR}/nvm.sh"
elif [ -s "${HOME}/.nvm/nvm.sh" ]; then
# shellcheck disable=SC1091
. "${HOME}/.nvm/nvm.sh"
fi
if command -v nvm >/dev/null 2>&1; then
if [ -f ".nvmrc" ]; then
if ! nvm use >/dev/null 2>&1; then
echo "[pre-push] WARN: nvm use (.nvmrc) failed; using current node in PATH." >&2
fi
else
if ! nvm use 18 >/dev/null 2>&1; then
echo "[pre-push] WARN: nvm use 18 failed; using current node in PATH." >&2
fi
fi
fi
require_cmd node
NODE_MAJOR="$(node -p "process.versions.node.split('.')[0]")"
if [ "$NODE_MAJOR" -lt 18 ]; then
echo "[pre-push] ERROR: Node >=18 required, current=$(node -v)" >&2
echo "[pre-push] Hint: install nvm and run 'nvm use 18'." >&2
exit 1
fi
echo "[pre-push] Node version: $(node -v)"
echo "[pre-push] 1/4 detect-secrets"
run_pre_commit_safe run detect-secrets --all-files
echo "[pre-push] 2/4 pre-commit all hooks (pass 1)"
if run_pre_commit_safe run --all-files --show-diff-on-failure; then
:
else
echo "[pre-push] INFO: pre-commit reported changes/issues; running pass 2 verification..." >&2
if run_pre_commit_safe run --all-files --show-diff-on-failure; then
if ! git diff --quiet -- .; then
echo "[pre-push] ERROR: hooks auto-fixed files during pre-push." >&2
echo "[pre-push] Please review, commit the fixes, then push again." >&2
git status --short
exit 1
fi
echo "[pre-push] WARN: first pass failed but second pass succeeded without local file changes." >&2
else
exit 1
fi
fi
echo "[pre-push] 3/4 backend unit tests"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_pre_push_unit" \
"$VENV_PY" scripts/run_unittests.py --start-dir tests --pattern "test_*.py"
echo "[pre-push] 4/4 npm test (Playwright)"
npm test
echo "[pre-push] PASS"