Run a Pre-Push Build Gate
How to set up a shared pre-push git hook that runs the module build and tests locally before a push reaches main, so direct pushes are far less likely to break the pipeline.
This is a best-effort, client-side gate: it runs on the developer’s machine and can be skipped with
git push --no-verify. It does not replace server-side enforcement. For why a direct push cannot be gated on CI results, and when to require a pull request instead, see GitHub Branch Protection Rationale.
Table of Contents
When to use this
Use a pre-push hook when collaborators push directly to main (Trunk-Based Development) and you want each push validated the same way CI validates it — without the ceremony of a pull request. If the repository instead requires pull requests, the server-side required status checks already cover this, and a hook is optional.
Add the hook
The hook mirrors the path-filtered CI: it inspects which paths changed in the commits being pushed and runs only the affected module’s build, so a docs-only or single-stack push stays fast.
Steps
-
Create a
.githooks/directory at the repository root and add an executablepre-pushfile:#!/usr/bin/env bash # .githooks/pre-push — run the same checks CI runs, for the module that changed. set -euo pipefail # Commits being pushed, compared against their upstream (falls back to origin/main). range="$(git rev-parse --abbrev-ref --symbolic-full-name @{push} 2>/dev/null || echo origin/main)" changed="$(git diff --name-only "$range"...HEAD)" if grep -q '^backend/' <<< "$changed"; then echo "▶ backend changed — running mvn -B package" (cd backend && mvn -B package) fi if grep -q '^frontend/' <<< "$changed"; then echo "▶ frontend changed — running lint, build, test" (cd frontend && npm ci && npm run lint && npm run build && npm test) fi echo "✓ pre-push checks passed" -
Make it executable and commit it so every collaborator gets the same hook:
chmod +x .githooks/pre-push git add .githooks/pre-push git commit -m "build: add pre-push build gate" -
Point git at the committed hooks directory.
core.hooksPathis a per-clone setting, so each collaborator runs this once after cloning:git config core.hooksPath .githooksTo make this automatic for the frontend module, add a
preparescript tofrontend/package.jsonsonpm installwires it up:"scripts": { "prepare": "git config core.hooksPath .githooks" }
What happens next
- On
git push, the hook runs before any commit reaches the remote. Ifmvn -B packageor the frontend checks fail, the push is aborted andmainnever sees the broken commit. - A push that touches neither
backend/norfrontend/(for example, docs only) runs no build and completes immediately — matching the CI path filters. - The exit code matters: the hook aborts the push only if a command exits non-zero.
set -euo pipefailensures the first failing command stops the script.
Bypassing and its limits
The hook can be skipped with:
git push --no-verify
This is intentional — git hooks are advisory. A pre-push gate catches honest mistakes and keeps main green in day-to-day work, but it cannot enforce anything against a determined bypass or a collaborator who has not run git config core.hooksPath. When a guarantee is required, move the gate server-side by requiring a pull request and status checks.
Reference: Git Hooks — pre-push · core.hooksPath