guide · computers

Git Commit and Push as the Deploy Gate: The ABS Convention

Why the live ABS site only deploys when local/origin/github are on the same commit. What this convention prevents, what it doesn't, and the post-receive hook that enforces it.

July 14, 2026 · By Alastair Fraser

A friendly retro robot standing between three doors labeled LOCAL, ORIGIN, and GITHUB, holding one key that fits all three locks, pointing at the doors in turn.

The convention in one sentence

The live ABS site only updates after git push succeeds and local, origin, and github all carry the same commit SHA. This is “three-SHA match”: before each push, the operator (or a cron) confirms local == origin == github.

The convention exists because deploying without a recorded commit breaks the audit trail — when the site goes wrong 3 weeks later, you need to be able to point at a SHA and say “what we shipped was this.”

Why three-SHA match, not one-SHA

One-SHA match (just verify local is up to date) means you ship without a backup on GitHub. If the VPS root-disk dies after the deploy, you’ve lost the source of the live site.

Two-SHA match (local + origin) is the typical git workflow. But origin on a self-hosted bare repo doesn’t have the same durability as GitHub — gitfs on the same disk is one disk-failure away from gone.

Three-SHA match (local + origin + GitHub) is three independent backups of the live site. The probability of losing all three at once is ~0.

What the post-receive hook does

The hook at /srv/git/abs-site.git/hooks/post-receive is the actual gate. On git push origin main from /srv/abs-site it does, in order, with a shared MATRIX_STATE_LOCK held across all of it:

  1. Acquire the matrix state lock on fd 9 from /opt/data/cron-state/matrix-abs-publish.lock. A concurrent daily cron queues behind the hook — serialised, not parallel. Released on trap EXIT, never rm-ed (a queued waiter could still hold the inode).
  2. Checkout + reset hard the working tree at /srv/abs-site to the just-pushed $newrev. If checkout fails, the hook aborts and never touches the live webroot.
  3. Stage fresh R2 data by invoking the same refresh_abs_from_matrix.sh orchestrator the daily cron uses, in MATRIX_ABS_STAGE_ONLY=1 mode with MATRIX_ABS_LOCK_HELD=1 (so the child does not deadlock on the lock the parent already owns). If the orchestrator is missing, unreadable, or exits non-zero, the hook aborts before npm run build runs.
  4. Build the site with npm run build. Build failure aborts.
  5. Run the regression suite — the per-concern scripts under /opt/data/scripts/ (search bar, search filter, CSS, analytics backend, backend security, Cloudflare baseline, daily briefing, mobile nav, grade sort). First red light stops the deploy. Per Alastair’s standing rule, every previously-fixed bug is now a permanent test.
  6. rsync dist/ to /var/www/agenticbotsitter/ — this is the moment the live site actually changes. Only happens if every prior step exited 0.
  7. Push the same commit to GitHub as MarvinAi5/ABS. If this push fails, the hook logs and continues — local is what users see, GitHub can be reconciled later — but the next three-SHA check will catch the gap.

The key shape: any of those five failure points (checkout, stage, build, regression, rsync) hard-aborts before the live webroot is touched. There is no “deploy the committed snapshot” fallback. That’s deliberate — the fallback is exactly the consistency violation the hook is here to prevent.

What this convention prevents

  • Deploying without a recorded commit. Every byte served from /var/www/agenticbotsitter/ is traceable to a SHA.
  • Live site drifting from the source-of-truth (origin). The hook is what reconciles them; nothing else writes to the webroot.
  • Live site drifting from the durable mirror (GitHub). Step 7 forwards the commit; the three-SHA check in the next deploy verifies the mirror caught up.
  • Pushing to origin and assuming GitHub synced. It doesn’t always — see the abs-news-cron-pipeline origin↔github gap. The check at deploy time is what makes the gap visible.
  • Race conditions where two deploys collide. The matrix state lock serialises the hook against the daily cron, and git push itself rejects non-fast-forwards by default.

What it doesn’t prevent

  • A bad commit pushed to all three. The convention gates how a commit lands live, not whether the commit is correct. That’s what the regression suite (step 5) is for, and even the suite only catches previously-seen bug classes.
  • Tunnel or origin failures. Those are caught by the watchdog sweep (cron-health-monitor, cron-health-monitor skill), not the deploy gate. The deploy gate is downstream of those being up.
  • Cache stale views. Cloudflare’s edge can keep serving old bytes for hours after a successful rsync. A separate CF purge step (cloudflare-edge-cache-purge) is required, and that’s its own concept.

The three-SHA check

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"

If the assertion fails, the deploy chain should hard-fail. The post-receive hook already enforces this implicitly (a git push that fails step 7 will not produce a matching GITHUB SHA, and the next operator run will see the mismatch), but the explicit check is what you run before you trust the live site to be what you think it is.

What not to do

  • Don’t push to origin and skip the github push — the next git reset --hard on origin will lose it. The post-receive hook forwards automatically, but a manual git push origin main from the working tree does not. Always verify with the three-SHA check.
  • Don’t edit files in /var/www/agenticbotsitter/ directly — that bypasses the gate and the SHA trail. If you find yourself doing this, stop and put the change in a commit on main so the hook can deploy it the right way.
  • Don’t accept a partial three-SHA match — partial means one of the three is lying, and you don’t know which. Re-run the push, check the hook log at /opt/data/logs/abs-deploy.log, fix the broken leg, then re-verify.

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 check aboveAll three match
5Live site curl returns the new guideThe deploy actually took

Step 5 closes the loop: even with three-SHA match, the deploy could be silently broken (a hook that runs but rsyncs an empty dist/, a tunnel that has the wrong origin, a Cloudflare cache that never got purged). The curl is what proves users are actually seeing the new SHA.

Sources

Last verified: 2026-07-14 against the live post-receive hook at /srv/git/abs-site.git/hooks/post-receive.

Sources

#git#deploy#commit#sha#three-sha

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.