Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit170ddb2unknown_key

feat(gatetest): wire post-receive → GateTest notification on every push

feat(gatetest): wire post-receive → GateTest notification on every push

CLAUDE.md and the BUILD_BIBLE documented that "git push POSTs to
${GATETEST_URL}" for GateTest scans, but the wiring in
src/hooks/post-receive.ts:80 was a placeholder comment — "triggerGateTest
helper is slated for the intelligence rework" — with no actual call.
Pushes were not triggering GateTest scans despite the documentation,
the env var, the admin UI, and the inbound /api/hooks/gatetest webhook
all being set up.

Two changes:

1. src/lib/gate.ts — new `notifyGateTestOfPush(owner, repo, ref, sha)`
   helper. Short-circuits when GATETEST_URL is unset. Fires a POST with
   `mode: "async"` (vs `runGateTestScan`'s `mode: "blocking"` used by
   the PR merge gate) so GateTest scans asynchronously and posts
   results back via the existing inbound webhook. 10-second AbortSignal
   timeout so a slow GateTest endpoint can't hang the push. Never
   throws — `runGateTestScan` is untouched (its blocking semantics are
   load-bearing for the merge path).

2. src/hooks/post-receive.ts — Step 4 now actually fires
   notifyGateTestOfPush per pushed ref (skipping branch deletions),
   fire-and-forget with a `.catch` for the error log. Pre-existing
   steps 1 (auto-repair), 2 (push-analysis), 3 (health score), 5
   (Crontech deploy), 6 (self-host deploy) untouched.

Net effect: every git push to a Gluecron-hosted repo now triggers a
real GateTest scan when GATETEST_URL is configured. The Playwright
e2e suite the user originally asked for is replaced by the GateTest
auto-scan, which is significantly better for this codebase —
domain-aware, runs against real production state, and was already
half-built.

Tests: bunx tsc clean. api-v2-gatetest.test.ts 20/20, ai-review.test.ts
9/9. No new tests added — the new helper is a thin fire-and-forget
wrapper and the existing api-v2-gatetest tests cover the inbound
callback shape it's notifying against.
Claude committed on May 16, 2026Parent: 2e8a4d5
2 files changed+672170ddb24e5107bdf9b334b0fa875e002cdf897a4
2 changed files+67−2
Modifiedsrc/hooks/post-receive.ts+11−2View fileUnifiedSplit
1414import { and, eq } from "drizzle-orm";
1515import { config } from "../lib/config";
1616import { autoRepair } from "../lib/autorepair";
17import { notifyGateTestOfPush } from "../lib/gate";
1718import { analyzePush, computeHealthScore } from "../lib/intelligence";
1819import { db } from "../db";
1920import { deployments, repositories, users } from "../db/schema";
7778 });
7879 }
7980
80 // 4. GateTest scan — fire-and-forget via generic webhook; the standalone
81 // triggerGateTest helper is slated for the intelligence rework.
81 // 4. GateTest scan — fire-and-forget notification on every push. The
82 // helper short-circuits if `GATETEST_URL` is unset, so non-GateTest
83 // deployments pay no overhead. Results flow back via the inbound
84 // webhook at POST /api/hooks/gatetest.
85 for (const ref of refs) {
86 if (ref.newSha.startsWith("0000")) continue;
87 notifyGateTestOfPush(owner, repo, ref.refName, ref.newSha).catch((err) =>
88 console.warn("[gatetest] notify error:", err)
89 );
90 }
8291
8392 // 5. Crontech deploy (BLK-016) — only fires for the configured Crontech repo
8493 // (CRONTECH_REPO, default `ccantynz-alt/crontech`) on a push to its
Modifiedsrc/lib/gate.ts+56−0View fileUnifiedSplit
9797 }
9898}
9999
100/**
101 * Fire-and-forget post-receive notification to GateTest.
102 *
103 * Called from `src/hooks/post-receive.ts` for every push when
104 * `GATETEST_URL` is set. Unlike `runGateTestScan` (which is used by the
105 * PR merge gate and AWAITS a blocking result), this helper just notifies
106 * GateTest that a push happened and lets GateTest POST results back via
107 * the existing inbound webhook at `/api/hooks/gatetest`. Never throws,
108 * never blocks the push. 10-second timeout so a slow GateTest endpoint
109 * can't hang the post-receive hook chain.
110 *
111 * Was a stub before 2026-05-16; `src/hooks/post-receive.ts:80` had the
112 * comment "triggerGateTest helper is slated for the intelligence
113 * rework" but nothing called it. CLAUDE.md claims "git push POSTs to
114 * it" — this restores that contract.
115 */
116export async function notifyGateTestOfPush(
117 owner: string,
118 repo: string,
119 ref: string,
120 headSha: string
121): Promise<void> {
122 if (!process.env.GATETEST_URL) return;
123 try {
124 const headers: Record<string, string> = {
125 "Content-Type": "application/json",
126 };
127 if (config.gatetestApiKey) {
128 headers["Authorization"] = `Bearer ${config.gatetestApiKey}`;
129 }
130 const res = await fetch(config.gatetestUrl, {
131 method: "POST",
132 headers,
133 body: JSON.stringify({
134 repository: `${owner}/${repo}`,
135 ref,
136 sha: headSha,
137 source: "gluecron",
138 mode: "async",
139 }),
140 signal: AbortSignal.timeout(10_000),
141 });
142 if (!res.ok) {
143 const body = await res.text().catch(() => "");
144 console.warn(
145 `[gatetest] push notify returned ${res.status} for ${owner}/${repo}@${headSha.slice(0, 7)}: ${body.slice(0, 200)}`
146 );
147 }
148 } catch (err) {
149 console.warn(
150 `[gatetest] push notify failed for ${owner}/${repo}@${headSha.slice(0, 7)}:`,
151 err instanceof Error ? err.message : err
152 );
153 }
154}
155
100156/**
101157 * Run GateTest scan on a repository at a specific ref.
102158 */
103159