Three-SHA Check: Asserting local == origin == github == the Commit Going Live
The bash assertion that runs before every ABS deploy: Local and origin and github must all carry the same commit SHA as the change about to ship. Failure shapes and the auto-repair pattern for each.

The 6-line script that runs before every ABS deploy
LOCAL=$(git rev-parse HEAD)
ORIGN=$(git ls-remote origin refs/heads/main | cut -f1)
GITHUB=$(git ls-remote github refs/heads/main | cut -f1)
[ "$LOCAL" = "$ORIGN" ] && [ "$ORIGN" = "$GITHUB" ] && echo "OK" || { echo "MISMATCH"; echo "L=$LOCAL O=$ORIGN G=$GITHUB"; exit 1; }
Six lines. No Python, no jq, no third-party tools. git rev-parse HEAD for the local SHA; git ls-remote is git’s plumbing command for asking a remote what its SHA is without cloning.
The exit-on-mismatch is the point. A deploy chain that runs this check but ignores the failure is just decoration; the chain has to short-circuit.
The four failure shapes
Each pair can disagree. The diagnostic for each is a git log --oneline between the two SHAs to see what’s behind:
-
local < origin — someone else pushed and you’re behind. The local working tree doesn’t reflect origin. Auto-repair:
git pull --ff-only. Verify nothing unexpected in the merged result, especially on shared branches.git fetch origin && git log --oneline origin/main ^HEAD | head -5Expected: empty output (you’re not behind).
-
local < github — you pushed to origin only, not github. Auto-repair:
git push github main.git fetch github && git log --oneline github/main ^HEAD | head -5 -
origin < github — origin is somehow behind GitHub. Usually means the VPS-side auto-bridge is broken (cron auto-commits to origin but never made it to GitHub). Auto-repair:
git push origin main.git fetch origin && git fetch github && \ git log --oneline github/main ^origin/main | head -5 -
local / origin / github all distinct — three independent pushes happened. Auto-repair: never force-push to “fix” this. Investigate which commit the deploy wants (
git log --all --oneline -10), then bring the other two to that commit.git log --all --oneline -10Pick the SHA you intend to deploy, set HEAD to it (
git reset --hard <SHA>), then push to whichever remotes are behind. Force-push is acceptable here only when you have confirmed the other two are not authoritative sources.
Each diagnostic runs in <1 second. Run them all every time — a MISMATCH with just one of the three ahead usually has a fourth shape just below the surface.
What not to do
- Don’t
--force-pushto fix a mismatch on a branch someone else might be using — that throws away commits. Use reverse-merge to bring the two sides together. - Don’t skip the check because it failed once and looked cosmetic — a one-off MISMATCH is exactly when the convention saves you.
- Don’t trust
git log --onelineto compare — that branch-local; use plumbing commands. - Don’t bypass the check with
git push --no-verifyor similar overrides on the deploy cron. The whole convention lives in the assertion’s exit code being respected. - Don’t run the check at the end of the deploy — run it before
npm run buildso the build never runs against an out-of-sync state.
The post-receive hook implementation
The hook at /srv/git/abs-site.git/hooks/post-receive runs after every push. Today’s six-step hook for ABS:
#!/bin/bash
set -e
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/main" ]; then
# 1. Checkout new SHA into the build work tree
GIT_DIR=/srv/git/abs-site.git GIT_WORK_TREE=/srv/abs-site \
git checkout -f main
# 2. Three-SHA check (run from /srv/abs-site, deploy.sh script)
bash /opt/hermes/deploy.sh --check-three-sha || { echo "three-SHA failed"; exit 1; }
# 3. Build
cd /srv/abs-site && npm run build || { echo "build failed"; exit 1; }
# 4. Regression suite
cd /srv/abs-site/scripts && \
for s in test_abs_*.py; do python3 "$s" || exit 1; done
# 5. Rsync dist to webroot
rsync -a --delete /srv/abs-site/dist/ /var/www/agenticbotsitter/
# 6. Mirror to GitHub
git push github main
# 7. CF purge
curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE}/purge_cache" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(/opt/data/scripts/build_purge_payload.py)" || true
fi
done
The check is step 2, before the build (step 3). When step 2 fails, the hook aborts; no build, no rsync, no GitHub mirror. If the local/origin/github are consistent, the build runs and the deploy proceeds. Even with the hook in place, run the check again in your head before you git push — it’s the operator’s last line of defense.
The hook today aborts on regression red with a manual rsync fallback (the user pastes the file via rsync -a /srv/abs-site/dist/ /var/www/agenticbotsitter/). That’s a deliberate bypass for the case where the test suite is outdated and the operator knows better than the test does — but use it sparingly.
Verification checklist
| # | Command | What it proves |
|---|---|---|
| 1 | git rev-parse HEAD | Local SHA known |
| 2 | git ls-remote origin refs/heads/main | cut -f1 | Origin SHA known |
| 3 | git ls-remote github refs/heads/main | cut -f1 | GitHub SHA known |
| 4 | The equality check | All three match |
| 5 | git log --all --oneline -10 | No unexpected branches behind |
Sources
- Git plumbing commands
- ABS companion: Git Commit and Push as the Deploy Gate
- ABS companion: ABS Regression Failure Modes
Last verified: 2026-07-14 against the live three-SHA check at /opt/hermes/deploy.sh and the post-receive hook at /srv/git/abs-site.git/hooks/post-receive.



Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.