141 lines
5.9 KiB
Bash
Executable File
141 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# The repo's own guards, before a commit rather than after it.
|
|
#
|
|
# ## Why this exists in the repository and not in .git/hooks
|
|
#
|
|
# `.git/hooks` is not versioned, so a hook living there protects exactly one
|
|
# checkout and silently protects nothing anywhere else. This directory is
|
|
# committed, and `core.hooksPath` points at it:
|
|
#
|
|
# git config core.hooksPath .githooks
|
|
#
|
|
# That one line is the only setup, and it is in the README beside the test
|
|
# command.
|
|
#
|
|
# ## ADAPTED FOR THIS PROJECT
|
|
#
|
|
# The template's version runs `npx tsc --noEmit` and then `npx vitest run`. This
|
|
# project has neither: no TypeScript, no tsconfig, no test runner, and every
|
|
# source file is plain .jsx. Installed unchanged, that hook refuses every commit
|
|
# on a project where nothing is wrong.
|
|
#
|
|
# So it runs the two things that exist:
|
|
#
|
|
# 1. The secret scan, on the staged diff. This is the reason the hook earns its
|
|
# place here at all. The Zoho WebToLead form identifiers were hardcoded in
|
|
# index.html and later src/pages/Contact.jsx and reached four commits of a
|
|
# then-PUBLIC repository before anyone noticed. A secret caught here costs a
|
|
# `git reset`; the same secret caught after a push costs a rotation, because
|
|
# deleting the line does not remove it from a commit that already exists.
|
|
#
|
|
# 2. `npm run build`, when source is staged. Three steps — client bundle, SSR
|
|
# bundle, then prerender across every route — and it fails on a broken
|
|
# import, a syntax error, or a page component that cannot render in Node.
|
|
#
|
|
# **The second is a build, not a test.** It proves the imports resolve. A form
|
|
# that posts to the wrong URL builds perfectly. Do not read a green hook as
|
|
# "the change works"; `docs/qa/ClaudeQACoverage.md` is honest about what is
|
|
# actually untested here, which is nearly everything.
|
|
#
|
|
# ## It warns about unstaged changes rather than failing on them
|
|
#
|
|
# Both commands run against the **working tree**, not against the index. So a
|
|
# clean run proves the working tree is good, which is only the same thing as the
|
|
# commit being good when nothing is left unstaged.
|
|
#
|
|
# That distinction matters in this checkout specifically: it is edited by more
|
|
# than one person at a time, and commits are staged by explicit path. A green
|
|
# hook beside three unstaged files has verified something other than what is
|
|
# about to be committed, and the honest thing is to say so rather than imply a
|
|
# guarantee that was not made.
|
|
#
|
|
# ## Escape hatch
|
|
#
|
|
# SKIP_GUARDS=1 git commit ... skips both, loudly
|
|
# git commit --no-verify ... skips the hook entirely, silently
|
|
#
|
|
# The first is preferred: it leaves a line in the terminal saying the guards did
|
|
# not run, which is the difference between a deliberate exception and a habit.
|
|
|
|
set -uo pipefail
|
|
|
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
|
|
say() { printf '\033[1mpre-commit:\033[0m %s\n' "$*" >&2; }
|
|
|
|
if [ -n "${SKIP_GUARDS:-}" ]; then
|
|
say "SKIP_GUARDS set — typecheck and tests did NOT run for this commit."
|
|
exit 0
|
|
fi
|
|
|
|
# Nothing staged is not this hook's problem; git will refuse on its own.
|
|
if git diff --cached --quiet; then
|
|
exit 0
|
|
fi
|
|
|
|
# Only worth running when source or tests changed. A commit that touches docs or
|
|
# migrations alone still gets the typecheck, because a migration can be
|
|
# referenced from a test, but it should not wait on the whole suite.
|
|
#
|
|
# The four files after the `|` are not source, and they are here because of what
|
|
# `tests/version.test.ts` guards: package.json, the Dockerfile, and the two
|
|
# image pins in README.md and docker-compose.example.yml must all name the same
|
|
# version. With `src|tests` alone, a commit that hand-edits only the README pin
|
|
# — precisely the drift that left those pins six versions stale — would get the
|
|
# typecheck and skip the one test that would have caught it. The guard has to
|
|
# run on the commits it exists to police.
|
|
staged=$(git diff --cached --name-only)
|
|
|
|
# What is worth a build. src/ and server/ are the obvious ones; the other four
|
|
# are here because each can break the build on its own — index.html is the Vite
|
|
# entry, vite.config.js owns the alias resolution and the build inputs,
|
|
# package.json can remove a dependency the bundle imports, and prerender.js runs
|
|
# as the third build step against every route.
|
|
touches_code=$(printf '%s\n' "$staged" \
|
|
| grep -cE '^(src|server|scripts)/|^(package\.json|index\.html|vite\.config\.js|tailwind\.config\.js|postcss\.config\.js)$' || true)
|
|
|
|
# Credentials, before the commit exists. FIRST, and cheap: it reads the staged
|
|
# diff only, and it runs on every commit including a docs-only one — a key
|
|
# pasted into a markdown file is still a key.
|
|
if [ -x scripts/secrets.sh ] || [ -f scripts/secrets.sh ]; then
|
|
say "secrets…"
|
|
if ! bash scripts/secrets.sh; then
|
|
say "possible credential in the staged changes — commit refused."
|
|
say "If it is real, ROTATE IT FIRST. Deleting the line does not remove it"
|
|
say "from a commit that already exists. If it is not, --allow the path or"
|
|
say "adjust the patterns in scripts/secrets.sh; do not silence the check."
|
|
exit 1
|
|
fi
|
|
else
|
|
# Said out loud. A missing scanner and a clean scan look identical from here,
|
|
# and this hook has exactly two jobs.
|
|
say "WARNING: scripts/secrets.sh is missing — NOTHING was scanned for"
|
|
say " credentials. That is not a pass."
|
|
fi
|
|
|
|
if [ "$touches_code" -gt 0 ]; then
|
|
say "build…"
|
|
|
|
if ! command -v npm >/dev/null 2>&1; then
|
|
say "npm is not on PATH, so the build did not run — commit refused."
|
|
say "This hook will not pass a commit it could not check."
|
|
exit 1
|
|
fi
|
|
|
|
if ! npm run build; then
|
|
say "build failed — commit refused."
|
|
exit 1
|
|
fi
|
|
else
|
|
say "no source staged — skipping the build."
|
|
fi
|
|
|
|
# Said last so it is the thing still on screen when the editor opens.
|
|
if ! git diff --quiet; then
|
|
say "NOTE: unstaged changes are present. The guards ran against the working"
|
|
say " tree, so they did not verify this commit in isolation."
|
|
fi
|
|
|
|
exit 0
|