markdown · 4742 bytes Raw Blame History

Git hooks

Bare-repo git hooks are how the push pipeline (S14) gets called. shithub installs pre-receive (gates) and post-receive (enqueue) on every repo. Both are tiny shell shims that exec into the same shithubd binary the web/SSH/worker layers run.

git invokes hooks with stdin piped, a particular cwd, and a controlled env. The shim normalizes the entry point: exec /path/to/shithubd hook <name>. Hooks aren't symlinked because (a) macOS git treats some symlinks oddly under hardened runtime, and (b) the shim explicitly re-exec's so signals and exit codes propagate cleanly.

The shim is regenerated on every hooks.Install call — there's no manual editing path, by design. If the binary moves (deploy upgrade, versioned install path), run shithubd hooks reinstall --all to point every repo at the new location.

Hook execution flow

git push  ──▶  receive-pack
              │
              ▼
         pre-receive  (one process per push)
              │ stdin: "<old> <new> <ref>" lines
              │ env:   SHITHUB_USER_ID / _USERNAME / _REPO_ID / _REPO_FULL_NAME
              │        SHITHUB_PROTOCOL / _REMOTE_IP / _REQUEST_ID
              ▼
         exec shithubd hook pre-receive
              │ exit 0 → accept
              │ exit ≠ 0 → reject; stderr is shown to the pusher
              ▼
       (push proceeds)
              │
              ▼
         post-receive  (one process per push)
              │ stdin: same shape
              ▼
         exec shithubd hook post-receive
              │ INSERT push_events row per ref
              │ INSERT job (push:process)
              │ NOTIFY shithub_jobs
              │ exit 0
              ▼
       worker picks it up via LISTEN

pre-receive contract

Implements the minimum gates described in S14. The full branch protection engine is S20.

Re-checks the following from the DB even though the env carries them (env can be stale on a long-lived SSH session):

  • User not suspended (users.suspended_at IS NULL).
  • Repo not archived (repos.is_archived = false).
  • Repo not soft-deleted (repos.deleted_at IS NULL).

Failures emit a shithub: ... line on stderr that git surfaces directly to the pusher's terminal. Latency budget: <100ms p99.

post-receive contract

  • Reads stdin, ignores empty/malformed lines.
  • For each <old> <new> <ref> line: INSERT a push_events row, then enqueue a push:process job carrying the new event id.
  • Issues a single NOTIFY shithub_jobs per push (workers wake on the next tx commit).
  • Exits 0 even on internal errors. The push has already landed; the pipeline is async — partial enqueue failures surface in the worker logs and a backstop reconciler (post-MVP) can re-process orphaned events.

Installation

  • On repo create (internal/repos/create.go::Create): runs hooks.Install after RepoFS.InitBare succeeds. The S11 plumbing initial commit does not fire hooks — that's correct, the contract is hooks fire on user-driven pushes only.
  • On deploy: shithubd hooks reinstall --all walks every active repo via the DB and re-installs. Single repos: --repo owner/name.
  • The binary path baked into the shim is os.Executable() of the running shithubd at install time, resolved through filepath.Abs. Test fixtures that don't exercise hooks pass ShithubdPath: "" to repos.Create.Deps to skip installation.

Failure modes worth knowing

  • SHITHUB_* env not set (e.g. someone manually triggered a push via a path that bypassed S12/S13): pre-receive returns the "missing context" error. post-receive logs a warning and exits 0 — the push still lands.
  • DB unreachable from hook: pre-receive returns "server error"; the user sees a generic message, the push aborts. post-receive logs and exits 0; backstop reconciler will pick up the unprocessed push on next opportunity.
  • Binary path drift between install and execution: the shim's hard- coded path no longer exists. git will report "hook execution failed" and abort the push. Operators recover with hooks reinstall.
  • Stale shim from previous shithubd version: Install is idempotent and overwrites; re-running on a deploy is the right move.

Deferred: hook DB role split

S14's spec calls for the hook to connect with a least-privilege Postgres role distinct from the one the web server uses. S14 ships the dev path (single role) and defers the split to S37 deploy automation. The full GRANT recipe and the planned config plumbing live in db-roles.md, and the bullet is on the S37 deliverables list so it doesn't fall off.

View source
1 # Git hooks
2
3 Bare-repo git hooks are how the push pipeline (S14) gets called.
4 shithub installs **pre-receive** (gates) and **post-receive** (enqueue)
5 on every repo. Both are tiny shell shims that exec into the same
6 `shithubd` binary the web/SSH/worker layers run.
7
8 ## Why shell shims and not direct binary symlinks
9
10 git invokes hooks with stdin piped, a particular cwd, and a controlled
11 env. The shim normalizes the entry point: `exec /path/to/shithubd hook
12 <name>`. Hooks aren't symlinked because (a) macOS git treats some
13 symlinks oddly under hardened runtime, and (b) the shim explicitly
14 re-exec's so signals and exit codes propagate cleanly.
15
16 The shim is regenerated on every `hooks.Install` call — there's no
17 manual editing path, by design. If the binary moves (deploy upgrade,
18 versioned install path), run `shithubd hooks reinstall --all` to point
19 every repo at the new location.
20
21 ## Hook execution flow
22
23 ```
24 git push ──▶ receive-pack
25
26
27 pre-receive (one process per push)
28 │ stdin: "<old> <new> <ref>" lines
29 │ env: SHITHUB_USER_ID / _USERNAME / _REPO_ID / _REPO_FULL_NAME
30 │ SHITHUB_PROTOCOL / _REMOTE_IP / _REQUEST_ID
31
32 exec shithubd hook pre-receive
33 │ exit 0 → accept
34 │ exit ≠ 0 → reject; stderr is shown to the pusher
35
36 (push proceeds)
37
38
39 post-receive (one process per push)
40 │ stdin: same shape
41
42 exec shithubd hook post-receive
43 │ INSERT push_events row per ref
44 │ INSERT job (push:process)
45 │ NOTIFY shithub_jobs
46 │ exit 0
47
48 worker picks it up via LISTEN
49 ```
50
51 ## pre-receive contract
52
53 Implements the **minimum gates** described in S14. The full branch
54 protection engine is S20.
55
56 Re-checks the following from the DB even though the env carries them
57 (env can be stale on a long-lived SSH session):
58
59 * User not suspended (`users.suspended_at IS NULL`).
60 * Repo not archived (`repos.is_archived = false`).
61 * Repo not soft-deleted (`repos.deleted_at IS NULL`).
62
63 Failures emit a `shithub: ...` line on stderr that git surfaces directly
64 to the pusher's terminal. Latency budget: <100ms p99.
65
66 ## post-receive contract
67
68 * Reads stdin, ignores empty/malformed lines.
69 * For each `<old> <new> <ref>` line: INSERT a `push_events` row, then
70 enqueue a `push:process` job carrying the new event id.
71 * Issues a single `NOTIFY shithub_jobs` per push (workers wake on the
72 next tx commit).
73 * Exits 0 even on internal errors. The push has already landed; the
74 pipeline is async — partial enqueue failures surface in the worker
75 logs and a backstop reconciler (post-MVP) can re-process orphaned
76 events.
77
78 ## Installation
79
80 * On repo create (`internal/repos/create.go::Create`): runs
81 `hooks.Install` after `RepoFS.InitBare` succeeds. The S11 plumbing
82 initial commit does *not* fire hooks — that's correct, the contract
83 is hooks fire on user-driven pushes only.
84 * On deploy: `shithubd hooks reinstall --all` walks every active repo
85 via the DB and re-installs. Single repos: `--repo owner/name`.
86 * The binary path baked into the shim is `os.Executable()` of the
87 running shithubd at install time, resolved through `filepath.Abs`.
88 Test fixtures that don't exercise hooks pass `ShithubdPath: ""` to
89 `repos.Create.Deps` to skip installation.
90
91 ## Failure modes worth knowing
92
93 * **`SHITHUB_*` env not set** (e.g. someone manually triggered a push
94 via a path that bypassed S12/S13): pre-receive returns the "missing
95 context" error. post-receive logs a warning and exits 0 — the push
96 still lands.
97 * **DB unreachable from hook**: pre-receive returns "server error";
98 the user sees a generic message, the push aborts. post-receive logs
99 and exits 0; backstop reconciler will pick up the unprocessed push
100 on next opportunity.
101 * **Binary path drift between install and execution**: the shim's hard-
102 coded path no longer exists. git will report "hook execution failed"
103 and abort the push. Operators recover with `hooks reinstall`.
104 * **Stale shim from previous shithubd version**: `Install` is
105 idempotent and overwrites; re-running on a deploy is the right move.
106
107 ## Deferred: hook DB role split
108
109 S14's spec calls for the hook to connect with a least-privilege Postgres
110 role distinct from the one the web server uses. S14 ships the dev path
111 (single role) and defers the split to S37 deploy automation. The full
112 GRANT recipe and the planned config plumbing live in
113 [`db-roles.md`](./db-roles.md), and the bullet is on the S37
114 deliverables list so it doesn't fall off.