diff --git a/tools/ci/install-hooks.sh b/tools/ci/install-hooks.sh index f138ab7c5..2da7de22b 100755 --- a/tools/ci/install-hooks.sh +++ b/tools/ci/install-hooks.sh @@ -1,12 +1,22 @@ #!/usr/bin/env bash -# Installs the repository's git hooks into this checkout (pre-push: no conflict markers, the site builds). +# Installs the repository's git hooks into this checkout (pre-push: no conflict markers, the site builds). Linked +# worktrees share the main repository's .git/hooks, so one install covers every worktree. +# The hook is READ-ONLY (6 October 2026): it builds the site in a temporary copy and never writes a tracked file in the +# worktree. Before this, `node build.mjs` in site/ rewrote downloads.json, journey.json, bench.html and the stamped pages +# in whatever worktree the push ran from (five worktrees carried 0.3.14's rows as uncommitted edits, and generated pages +# kept changing under agents after 063bbca). Only the ship step writes, with SITE_DOWNLOADS_REFRESH=1 node site/build.mjs. set -euo pipefail cd "$(dirname "$0")/../.." -cat > .git/hooks/pre-push <<'HOOK' +HOOKS="$(git rev-parse --git-common-dir)/hooks" +mkdir -p "$HOOKS" +cat > "$HOOKS/pre-push" <<'HOOK' #!/usr/bin/env bash set -e cd "$(git rev-parse --show-toplevel)" bash tools/ci/no-conflict-markers.sh -(cd site && node build.mjs >/dev/null) || { echo "pre-push: the site build fails; fix it before pushing" >&2; exit 1; } +# the site build, in a temporary copy: a failing build blocks the push; nothing in the worktree is written +tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT +cp -R site "$tmp/site" +(cd "$tmp/site" && SITE_DOWNLOADS_OFFLINE=1 node build.mjs >/dev/null) || { echo "pre-push: the site build fails; fix it before pushing" >&2; exit 1; } HOOK -chmod +x .git/hooks/pre-push; echo "pre-push hook installed" +chmod +x "$HOOKS/pre-push"; echo "pre-push hook installed at $HOOKS/pre-push (read-only site build)" diff --git a/tools/ci/no-foreign-tree-writes.sh b/tools/ci/no-foreign-tree-writes.sh index 969032bc8..860086793 100755 --- a/tools/ci/no-foreign-tree-writes.sh +++ b/tools/ci/no-foreign-tree-writes.sh @@ -35,6 +35,14 @@ if [ "${1:-}" = "--self-test" ]; then echo "self-test passed: a Projects path fails, a worktree-list loop with a write fails, an own-toplevel write passes"; exit 0 fi fail=0 +# a git hook never writes a tracked file: the hook body in tools/ci/install-hooks.sh (between <<'HOOK' and HOOK) may run the +# site build only inside a mktemp copy (6 October 2026: the in-place build rewrote generated pages in other worktrees) +if [ -f tools/ci/install-hooks.sh ]; then + body="$(sed -n "/<<'HOOK'/,/^HOOK$/p" tools/ci/install-hooks.sh)" + if printf '%s' "$body" | grep -qE "build\.mjs" && ! printf '%s' "$body" | grep -qE "mktemp"; then + echo "foreign-tree: the pre-push hook runs the site build in the worktree (no mktemp copy): a hook never writes a tracked file"; fail=1 + fi +fi while IFS= read -r f; do grep -nE "$WARN" "$f" | grep -vE '^[0-9]+:\s*(#|//)' | sed "s|^|foreign-tree (warning, env-only default owed): $f:|" | cut -c1-200; done < <(git ls-files 'tools/**' 'packaging/**' 'infra/**' 'relay/**' | grep -E '\.(sh|mjs|js|py|ps1)$' | grep -v '^tools/ci/no-foreign-tree-writes.sh$') while IFS= read -r f; do check_file "$f" || fail=1; done < <(git ls-files 'tools/**' 'packaging/**' 'site/*.mjs' 'infra/**' 'relay/**' | grep -E '\.(sh|mjs|js|py|ps1)$' | grep -v '^tools/ci/no-foreign-tree-writes.sh$') [ "$fail" = 0 ] && echo "foreign-tree: every script writes under its own toplevel"