Files
crewli/scripts/test-lefthook-pre-push.sh
bert.hausmans 37af961b3e fix(lefthook): remove duplicate git-lfs pre-push command
Lefthook v2 runs `git lfs pre-push` internally for pre-push hooks (per
docs/usage/features/git-lfs.md; confirmed in internal/run/controller/
lfs.go where the internal handler invokes `git lfs pre-push <remote>
<url>` with a buffered `cachedStdin`). Our manual `git-lfs:` command
in lefthook.yml was a second invocation against the same remote; the
duplicate is directly visible in `LEFTHOOK_VERBOSE=1` output as
`[git-lfs] executing hook` (internal) followed by `[lefthook] run:
git lfs pre-push` (manual).

The previous fix attempt (piped: true, commit 1b06804) was based on a
wrong understanding of `piped`'s semantics — `piped` controls
fail-fast behavior, not stdin routing or sequencing. Default lefthook
behavior is already sequential per docs/configuration/parallel.md.
That "fix" was placebo; incident 2 (F2 push, zero LFS objects, commit
99eedb6) proved it.

Phase A investigation: documentary + source confirmation that lefthook
owns the LFS pre-push call. Phase B sandbox test against a filesystem
remote confirmed the duplicate execution in logs but did NOT reproduce
the production hang — likely because the duplicate manual call against
a local remote has no LFS server to interact with. A network-y remote
(Gitea over SSH/HTTPS) appears to be part of the trigger. Two
mechanisms remain plausible (H1: PTY-stdin without EOF in
`while read` loop per docs/configuration/use_stdin.md; H4: server-side
LFS interaction on the duplicate call). Both are eliminated by the
same fix: remove the manual command. LFS uploads continue to work via
lefthook's internal handler (verified in sandbox post-fix).

Regression coverage: scripts/test-lefthook-pre-push.sh asserts exactly
one internal LFS invocation, zero manual ones, and `Uploading LFS
objects: 100%` present, against a disposable sandbox.

See dev-docs/ADR-LEFTHOOK-LFS-INTEGRATION.md for full context, both
misconceptions to prevent regression, and the alternative-scenarios
playbook if Phase E ever regresses.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-11 00:18:56 +02:00

146 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Smoke test for the lefthook + git-lfs pre-push integration.
#
# Builds a disposable sandbox at SANDBOX_DIR, copies the repo's
# current lefthook.yml + .githooks/ into it, and runs a push that
# exercises both: lefthook's internal LFS handler and the sync-check
# user command. Passes when:
#
# 1. Push completes within the timeout.
# 2. Exactly one `[git-lfs] executing hook` line is present in the
# verbose log (proves no duplicate manual command).
# 3. `Uploading LFS objects: 100%` is present (proves the internal
# handler did the upload).
#
# Fails loudly otherwise. See dev-docs/ADR-LEFTHOOK-LFS-INTEGRATION.md
# for what each failure mode signals.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SANDBOX_DIR="${SANDBOX_DIR:-/tmp/lefthook-lfs-smoke-$$}"
TIMEOUT_SECS="${TIMEOUT_SECS:-30}"
# Resolve the lefthook binary the repo already has installed.
LEFTHOOK_BIN="${LEFTHOOK_BIN:-}"
if [ -z "${LEFTHOOK_BIN}" ]; then
candidate=$(find "${REPO_ROOT}/node_modules" -type f -name lefthook \
-path '*/lefthook-darwin-arm64/bin/*' 2>/dev/null | head -n 1)
if [ -z "${candidate}" ]; then
candidate=$(find "${REPO_ROOT}/node_modules" -type f -name lefthook \
2>/dev/null | head -n 1)
fi
LEFTHOOK_BIN="${candidate}"
fi
if [ -z "${LEFTHOOK_BIN}" ] || [ ! -x "${LEFTHOOK_BIN}" ]; then
echo "FAIL: could not locate lefthook binary (set LEFTHOOK_BIN explicitly)" >&2
exit 2
fi
if ! command -v git-lfs >/dev/null 2>&1; then
echo "FAIL: git-lfs not installed on this host" >&2
exit 2
fi
cleanup() { rm -rf "${SANDBOX_DIR}"; }
trap cleanup EXIT
echo "[smoke] sandbox: ${SANDBOX_DIR}"
echo "[smoke] lefthook: ${LEFTHOOK_BIN}"
mkdir -p "${SANDBOX_DIR}"
git init --bare -q "${SANDBOX_DIR}/remote.git"
git init -q "${SANDBOX_DIR}/work"
cd "${SANDBOX_DIR}/work"
git config user.email "smoke@crewli.local"
git config user.name "smoke"
git remote add origin "${SANDBOX_DIR}/remote.git"
# Mirror the repo's hook layer
cp "${REPO_ROOT}/lefthook.yml" .
mkdir .githooks
cp "${REPO_ROOT}/.githooks/pre-push" .githooks/
cp "${REPO_ROOT}/.githooks/post-commit" .githooks/
chmod +x .githooks/*
LEFTHOOK_BIN="${LEFTHOOK_BIN}" "${LEFTHOOK_BIN}" install >/dev/null
git lfs install --skip-repo >/dev/null
git lfs track "*.png" >/dev/null
# Tiny valid PNG so LFS has something to push
python3 - <<'PY'
import struct, zlib
hdr = b'\x89PNG\r\n\x1a\n'
def chunk(t, d):
return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d))
ihdr = chunk(b'IHDR', struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0))
idat = chunk(b'IDAT', zlib.compress(b'\x00\xff\xff\xff', 9))
iend = chunk(b'IEND', b'')
open('smoke.png', 'wb').write(hdr + ihdr + idat + iend)
PY
git add .gitattributes lefthook.yml .githooks smoke.png
LEFTHOOK=0 git commit -q -m "smoke test"
LOG_FILE="${SANDBOX_DIR}/push.log"
# Background push; kill if it exceeds the timeout
(
cd "${SANDBOX_DIR}/work"
LEFTHOOK_BIN="${LEFTHOOK_BIN}" \
LEFTHOOK_VERBOSE=1 \
git push -u origin master >"${LOG_FILE}" 2>&1
echo "EXIT=$?" >>"${LOG_FILE}"
) &
PUSH_PID=$!
elapsed=0
while kill -0 "${PUSH_PID}" 2>/dev/null; do
if [ "${elapsed}" -ge "${TIMEOUT_SECS}" ]; then
kill -KILL "${PUSH_PID}" 2>/dev/null || true
pkill -KILL -P "${PUSH_PID}" 2>/dev/null || true
echo "[smoke] FAIL: push exceeded ${TIMEOUT_SECS}s timeout" >&2
echo "[smoke] partial log:" >&2
cat "${LOG_FILE}" >&2 || true
exit 1
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "${PUSH_PID}" 2>/dev/null || true
if ! grep -q '^EXIT=0$' "${LOG_FILE}"; then
echo "[smoke] FAIL: push exited non-zero" >&2
cat "${LOG_FILE}" >&2
exit 1
fi
# Exactly one internal LFS invocation; zero manual ones
internal_count=$(grep -c '\[git-lfs\] executing hook' "${LOG_FILE}" || true)
manual_count=$(grep -c '\[lefthook\] run: git lfs pre-push' "${LOG_FILE}" || true)
upload_marker=$(grep -c 'Uploading LFS objects: 100%' "${LOG_FILE}" || true)
if [ "${internal_count}" != "1" ]; then
echo "[smoke] FAIL: expected 1 internal LFS invocation, found ${internal_count}" >&2
cat "${LOG_FILE}" >&2
exit 1
fi
if [ "${manual_count}" != "0" ]; then
echo "[smoke] FAIL: a manual 'git lfs pre-push' command is present" \
"(${manual_count} occurrences). The duplicate-execution regression has returned." >&2
echo "[smoke] See dev-docs/ADR-LEFTHOOK-LFS-INTEGRATION.md for why this fails." >&2
cat "${LOG_FILE}" >&2
exit 1
fi
if [ "${upload_marker}" = "0" ]; then
echo "[smoke] FAIL: no 'Uploading LFS objects' marker in log" >&2
cat "${LOG_FILE}" >&2
exit 1
fi
echo "[smoke] PASS: push completed; 1 internal LFS call, 0 manual, LFS upload confirmed."