guide · computers

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.

July 14, 2026 · By Alastair Fraser

A friendly retro robot holding three tiny key-shaped SHA hashes side-by-side labeled LOCAL, ORIGIN, and GITHUB, with a magnifying glass hovering over them to confirm they all match.

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:

  1. 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 -5

    Expected: empty output (you’re not behind).

  2. 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
  3. 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
  4. 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 -10

    Pick 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-push to 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 --oneline to compare — that branch-local; use plumbing commands.
  • Don’t bypass the check with git push --no-verify or 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 build so 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

#CommandWhat it proves
1git rev-parse HEADLocal SHA known
2git ls-remote origin refs/heads/main | cut -f1Origin SHA known
3git ls-remote github refs/heads/main | cut -f1GitHub SHA known
4The equality checkAll three match
5git log --all --oneline -10No unexpected branches behind

Sources

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.

Sources

#git#three-sha#deploy#sha#bash

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.

Your email address will not be published. Required fields are marked.