Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

gate.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.

gate.tsBlame184 lines · 1 contributor
e883329Claude1/**
2 * Green gate enforcement.
3 *
4 * Checks that all quality gates pass before a merge is allowed:
5 * 1. GateTest scan — runs automated tests/checks via the GateTest API
6 * 2. AI code review — must be approved (no blocking issues)
7 *
8 * Nothing ships unless everything is green.
9 */
10
11import { config } from "./config";
12
13export interface GateCheckResult {
14 name: string;
15 passed: boolean;
16 details: string;
17}
18
19export interface GateResult {
20 allPassed: boolean;
21 checks: GateCheckResult[];
22}
23
24/**
25 * Run GateTest scan on a repository at a specific ref.
26 * Returns pass/fail with details.
27 */
28export async function runGateTestScan(
29 owner: string,
30 repo: string,
31 ref: string,
32 headSha: string
33): Promise<GateCheckResult> {
34 if (!config.gatetestUrl) {
35 return { name: "GateTest", passed: true, details: "GateTest URL not configured — skipped" };
36 }
37
38 try {
39 const headers: Record<string, string> = {
40 "Content-Type": "application/json",
41 };
42 if (config.gatetestApiKey) {
43 headers["Authorization"] = `Bearer ${config.gatetestApiKey}`;
44 }
45
46 const response = await fetch(config.gatetestUrl, {
47 method: "POST",
48 headers,
49 body: JSON.stringify({
50 repository: `${owner}/${repo}`,
51 ref,
52 sha: headSha,
53 source: "gluecron",
54 mode: "blocking", // Wait for results instead of fire-and-forget
55 }),
56 });
57
58 if (!response.ok) {
59 const body = await response.text().catch(() => "");
60 return {
61 name: "GateTest",
62 passed: false,
63 details: `GateTest returned ${response.status}: ${body.slice(0, 200)}`,
64 };
65 }
66
67 const result = await response.json().catch(() => ({})) as Record<string, unknown>;
68
69 // GateTest API returns { passed: boolean, summary: string, issues: [...] }
70 const passed = result.passed === true || result.status === "passed" || result.status === "success";
71 const summary = (result.summary as string) || (result.message as string) || (passed ? "All checks passed" : "Checks failed");
72
73 return {
74 name: "GateTest",
75 passed,
76 details: summary,
77 };
78 } catch (err) {
79 console.error("[gate] GateTest scan error:", err);
80 return {
81 name: "GateTest",
82 passed: false,
83 details: `GateTest scan failed: ${err instanceof Error ? err.message : "Unknown error"}`,
84 };
85 }
86}
87
88/**
89 * Check for merge conflicts between branches.
90 */
91export async function checkMergeability(
92 owner: string,
93 repo: string,
94 baseBranch: string,
95 headBranch: string
96): Promise<GateCheckResult> {
97 const { getRepoPath } = await import("../git/repository");
98 const repoDir = getRepoPath(owner, repo);
99
100 const proc = Bun.spawn(
101 ["git", "merge-tree", `$(git merge-base ${baseBranch} ${headBranch})`, baseBranch, headBranch],
102 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
103 );
104 // merge-tree isn't ideal — use merge --no-commit in a worktree style check
105 await proc.exited;
106
107 // Simpler: check if merge-base --is-ancestor works (fast-forward possible)
108 const ffCheck = Bun.spawn(
109 ["git", "merge-base", "--is-ancestor", baseBranch, headBranch],
110 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
111 );
112 const ffExit = await ffCheck.exited;
113
114 if (ffExit === 0) {
115 return { name: "Merge check", passed: true, details: "Fast-forward merge possible" };
116 }
117
118 // Check if there would be conflicts
119 const mergeBase = Bun.spawn(
120 ["git", "merge-base", baseBranch, headBranch],
121 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
122 );
123 const baseOut = await new Response(mergeBase.stdout).text();
124 const baseExit = await mergeBase.exited;
125
126 if (baseExit !== 0) {
127 return { name: "Merge check", passed: false, details: "Branches have no common ancestor" };
128 }
129
130 // Use merge-tree (three-way) to detect conflicts without touching working tree
131 const mergeTree = Bun.spawn(
132 ["git", "merge-tree", baseOut.trim(), baseBranch, headBranch],
133 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
134 );
135 const treeOut = await new Response(mergeTree.stdout).text();
136 await mergeTree.exited;
137
138 const hasConflicts = treeOut.includes("<<<<<<<");
139
140 return {
141 name: "Merge check",
142 passed: !hasConflicts,
143 details: hasConflicts
144 ? "Merge conflicts detected — auto-resolution will be attempted"
145 : "Clean merge possible",
146 };
147}
148
149/**
150 * Run all gate checks for a PR merge.
151 */
152export async function runAllGateChecks(
153 owner: string,
154 repo: string,
155 baseBranch: string,
156 headBranch: string,
157 headSha: string,
158 aiReviewApproved: boolean
159): Promise<GateResult> {
160 const checks: GateCheckResult[] = [];
161
162 // Run GateTest and mergeability check in parallel
163 const [gateTestResult, mergeResult] = await Promise.all([
164 runGateTestScan(owner, repo, `refs/heads/${headBranch}`, headSha),
165 checkMergeability(owner, repo, baseBranch, headBranch),
166 ]);
167
168 checks.push(gateTestResult);
169 checks.push(mergeResult);
170
171 // AI review check
172 checks.push({
173 name: "AI Review",
174 passed: aiReviewApproved,
175 details: aiReviewApproved
176 ? "AI review approved"
177 : "AI review found blocking issues — resolve before merging",
178 });
179
180 return {
181 allPassed: checks.every((c) => c.passed),
182 checks,
183 };
184}