Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

post-receive.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

post-receive.tsBlame231 lines · 1 contributor
fc1817aClaude1/**
2 * Post-receive hook logic.
2c34075Claude3 *
4 * Called after every successful git push. This is gluecron's intelligence layer:
5 * 1. Auto-repair — fix common issues and commit automatically
6 * 2. Push analysis — detect breaking changes, security issues
7 * 3. Health score — recompute repo health
8 * 4. GateTest scan — external security scanning
9 * 5. Crontech deploy — auto-deploy on push to main
10 * 6. Webhooks — fire registered webhook URLs
fc1817aClaude11 */
12
3ef4c9dClaude13import { and, eq } from "drizzle-orm";
fc1817aClaude14import { config } from "../lib/config";
2c34075Claude15import { autoRepair } from "../lib/autorepair";
16import { analyzePush, computeHealthScore } from "../lib/intelligence";
fc1817aClaude17
18interface PushRef {
19 oldSha: string;
20 newSha: string;
21 refName: string;
22}
23
24export async function onPostReceive(
25 owner: string,
26 repo: string,
27 refs: PushRef[]
28): Promise<void> {
2c34075Claude29 for (const ref of refs) {
30 if (ref.newSha.startsWith("0000")) continue; // Branch deletion
31 const branchName = ref.refName.replace("refs/heads/", "");
fc1817aClaude32
2c34075Claude33 // 1. Auto-repair (runs first, may create a new commit)
34 try {
35 const repair = await autoRepair(owner, repo, branchName);
36 if (repair.repaired) {
37 console.log(
38 `[autorepair] ${owner}/${repo}@${branchName}: ${repair.repairs.length} repairs committed`
39 );
40 }
41 } catch (err) {
42 console.error(`[autorepair] error:`, err);
43 }
fc1817aClaude44
2c34075Claude45 // 2. Push analysis
46 try {
47 const analysis = await analyzePush(owner, repo, ref.oldSha, ref.newSha);
48 console.log(
49 `[push-analysis] ${owner}/${repo}: ${analysis.summary}`
50 );
51 if (analysis.riskScore > 50) {
52 console.warn(
53 `[push-analysis] HIGH RISK push detected (score: ${analysis.riskScore})`
54 );
55 }
56 if (analysis.breakingChangeSignals.length > 0) {
57 console.warn(
58 `[push-analysis] Breaking changes: ${analysis.breakingChangeSignals.join("; ")}`
59 );
60 }
61 } catch (err) {
62 console.error(`[push-analysis] error:`, err);
63 }
64
65 // 3. Health score (async, don't block)
66 computeHealthScore(owner, repo).then((report) => {
67 console.log(
68 `[health] ${owner}/${repo}: ${report.grade} (${report.score}/100)`
69 );
70 }).catch((err) => {
71 console.error(`[health] error:`, err);
72 });
73 }
74
75 // 4. GateTest scan
76 triggerGateTest(owner, repo, refs).catch((err) =>
77 console.error(`[gatetest] error:`, err)
78 );
79
80 // 5. Crontech deploy on push to main
fc1817aClaude81 const mainPush = refs.find(
82 (r) => r.refName === "refs/heads/main" && !r.newSha.startsWith("0000")
83 );
84 if (mainPush) {
2c34075Claude85 triggerCrontechDeploy(owner, repo, mainPush.newSha).catch((err) =>
86 console.error(`[crontech] error:`, err)
87 );
fc1817aClaude88 }
89}
90
43cf9b0Claude91/**
92 * Trigger Crontech auto-deploy via the outbound webhook.
93 *
94 * Wire contract (Gluecron's copy — do not import from Crontech):
95 *
96 * POST https://crontech.ai/api/hooks/gluecron/push
97 * Authorization: Bearer ${GLUECRON_WEBHOOK_SECRET}
98 * Content-Type: application/json
99 *
100 * {
101 * "repository": "owner/name",
102 * "sha": "<40-hex>",
103 * "branch": "main",
104 * "ref": "refs/heads/main",
105 * "source": "gluecron",
106 * "timestamp": "<ISO-8601>"
107 * }
108 *
109 * → 200 { ok: true, deploymentId, status: "queued" | "skipped" }
110 * → 401 invalid bearer token
111 * → 400 malformed payload
112 * → 404 repository not configured for auto-deploy on Crontech
113 *
114 * If `GLUECRON_WEBHOOK_SECRET` is unset we silently omit the Authorization
115 * header — Crontech will then respond 401, which we treat as a failed deploy
116 * row exactly like any other non-ok HTTP response.
117 */
fc1817aClaude118async function triggerCrontechDeploy(
119 owner: string,
120 repo: string,
3ef4c9dClaude121 sha: string,
122 repositoryId: string
fc1817aClaude123): Promise<void> {
3ef4c9dClaude124 let deployId = "";
fc1817aClaude125 try {
3ef4c9dClaude126 const [row] = await db
127 .insert(deployments)
128 .values({
129 repositoryId,
130 environment: "production",
131 commitSha: sha,
132 ref: "refs/heads/main",
133 status: "pending",
134 target: "crontech",
135 })
136 .returning();
137 deployId = row?.id || "";
138 } catch {
139 /* ignore */
fc1817aClaude140 }
141
142 try {
43cf9b0Claude143 const headers: Record<string, string> = {
144 "Content-Type": "application/json",
145 };
146 if (config.gluecronWebhookSecret) {
147 headers["Authorization"] = `Bearer ${config.gluecronWebhookSecret}`;
148 }
fc1817aClaude149 const response = await fetch(config.crontechDeployUrl, {
150 method: "POST",
43cf9b0Claude151 headers,
fc1817aClaude152 body: JSON.stringify({
153 repository: `${owner}/${repo}`,
154 sha,
155 branch: "main",
43cf9b0Claude156 ref: "refs/heads/main",
fc1817aClaude157 source: "gluecron",
43cf9b0Claude158 timestamp: new Date().toISOString(),
fc1817aClaude159 }),
160 });
161 console.log(
162 `[crontech] deploy triggered for ${owner}/${repo}@${sha.slice(0, 7)}: ${response.status}`
163 );
3ef4c9dClaude164 if (deployId) {
165 await db
166 .update(deployments)
167 .set({
168 status: response.ok ? "success" : "failed",
169 completedAt: new Date(),
170 })
171 .where(eq(deployments.id, deployId));
172 }
1e162a8Claude173 // D4: when Crontech returns a non-ok HTTP status, kick off the AI
174 // incident responder AFTER the deployment row is flipped to "failed".
175 if (!response.ok && deployId) {
176 void onDeployFailure({
177 repositoryId,
178 deploymentId: deployId,
179 ref: "refs/heads/main",
180 commitSha: sha,
181 target: "crontech",
182 errorMessage: `HTTP ${response.status}`,
183 }).catch((e) => console.error("[ai-incident]", e));
184 }
fc1817aClaude185 } catch (err) {
186 console.error(`[crontech] failed to trigger deploy:`, err);
3ef4c9dClaude187 if (deployId) {
188 await db
189 .update(deployments)
190 .set({
191 status: "failed",
192 blockedReason: (err as Error).message,
193 completedAt: new Date(),
194 })
195 .where(eq(deployments.id, deployId));
1e162a8Claude196 // D4: fire-and-forget incident analysis AFTER marking the row failed.
197 void onDeployFailure({
198 repositoryId,
199 deploymentId: deployId,
200 ref: "refs/heads/main",
201 commitSha: sha,
202 target: "crontech",
203 errorMessage: (err as Error).message,
204 }).catch((e) => console.error("[ai-incident]", e));
3ef4c9dClaude205 }
206 }
207}
208
209async function fanoutWebhooks(
210 repositoryId: string,
211 owner: string,
212 repo: string,
213 refs: PushRef[]
214): Promise<void> {
215 try {
216 const { fireWebhooks } = await import("../routes/webhooks");
217 await fireWebhooks(repositoryId, "push", {
218 repository: `${owner}/${repo}`,
219 refs: refs.map((r) => ({
220 ref: r.refName,
221 before: r.oldSha,
222 after: r.newSha,
223 })),
224 });
225 } catch {
226 // best-effort
fc1817aClaude227 }
228}
43cf9b0Claude229
230/** Test-only access to internal helpers. */
231export const __test = { triggerCrontechDeploy };