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

check-auto-merge-readiness.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.

check-auto-merge-readiness.tsBlame219 lines · 1 contributor
f764c07Claude1/**
2 * Block N1 — Auto-merge readiness preflight.
3 *
4 * Operator runs this BEFORE `enable-auto-merge.ts` to make sure the box
5 * is actually in a state where flipping the switch will produce useful
6 * behaviour. Exits 0 when every check is green, 1 when any check fails.
7 *
8 * Checks:
9 * 1. Migration 0040 has been applied (`branch_protection.enable_auto_merge`
10 * column exists). The bootstrap script depends on this column.
11 * 2. `ANTHROPIC_API_KEY` is set on the box — without it the AI approval
12 * gate degrades to "AI review unavailable", which is treated as
13 * not-approved, and every auto-merge candidate gets blocked.
14 * 3. The autopilot is running — `AUTOPILOT_DISABLED` is not `"1"`.
15 * The K3 sweep task is the only thing that actually performs the
16 * merge; if the autopilot is off, opt-in does nothing.
17 * 4. The K3 `auto-merge-sweep` task is registered in `defaultTasks()`.
18 * Guards against someone removing the task name during a refactor.
19 *
20 * Pure-ish: each check is a small async function that returns a Result
21 * record. The runner just iterates them and prints a checklist. Tests
22 * drive the pure helpers directly.
23 */
24
25import { sql } from "drizzle-orm";
26
27// ---------------------------------------------------------------------------
28// Types + pretty printers
29// ---------------------------------------------------------------------------
30
31export type CheckStatus = "pass" | "fail";
32
33export interface CheckResult {
34 name: string;
35 status: CheckStatus;
36 reason?: string;
37}
38
39const GREEN = "\x1b[32m";
40const RED = "\x1b[31m";
41const DIM = "\x1b[2m";
42const RESET = "\x1b[0m";
43
44function icon(s: CheckStatus): string {
45 return s === "pass" ? `${GREEN}v${RESET}` : `${RED}x${RESET}`;
46}
47
48// ---------------------------------------------------------------------------
49// Pure check helpers
50// ---------------------------------------------------------------------------
51
52/**
53 * Test the `ANTHROPIC_API_KEY` env var. Pure function over the supplied
54 * env object so tests don't have to mutate `process.env`.
55 */
56export function checkAnthropicKey(env: NodeJS.ProcessEnv): CheckResult {
57 const key = env.ANTHROPIC_API_KEY;
58 if (!key || key.length < 10) {
59 return {
60 name: "ANTHROPIC_API_KEY set",
61 status: "fail",
62 reason:
63 "ANTHROPIC_API_KEY is missing — AI approval gate will block every auto-merge candidate.",
64 };
65 }
66 return { name: "ANTHROPIC_API_KEY set", status: "pass" };
67}
68
69/**
70 * Confirm the autopilot ticker is enabled. `AUTOPILOT_DISABLED=1` turns
71 * the K3 sweep off, which means opt-in does nothing useful.
72 */
73export function checkAutopilotEnabled(env: NodeJS.ProcessEnv): CheckResult {
74 if (env.AUTOPILOT_DISABLED === "1") {
75 return {
76 name: "Autopilot enabled (AUTOPILOT_DISABLED != 1)",
77 status: "fail",
78 reason:
79 "AUTOPILOT_DISABLED=1 — unset (or set to 0) to let the K3 sweep run.",
80 };
81 }
82 return {
83 name: "Autopilot enabled (AUTOPILOT_DISABLED != 1)",
84 status: "pass",
85 };
86}
87
88/**
89 * Confirm the K3 sweep task is registered. We accept any iterable that
90 * yields objects with a `.name` so tests don't have to import the real
91 * autopilot module.
92 */
93export function checkAutoMergeSweepRegistered(
94 tasks: Array<{ name: string }>
95): CheckResult {
96 const found = tasks.some((t) => t.name === "auto-merge-sweep");
97 if (!found) {
98 return {
99 name: "K3 auto-merge-sweep task registered",
100 status: "fail",
101 reason:
102 "defaultTasks() does not include 'auto-merge-sweep' — the K3 sweep is missing.",
103 };
104 }
105 return {
106 name: "K3 auto-merge-sweep task registered",
107 status: "pass",
108 };
109}
110
111/**
112 * Verify migration 0040 has landed by probing for the
113 * `branch_protection.enable_auto_merge` column. Pure-ish: takes the
114 * runner as a callback so tests can stub it.
115 */
116export async function checkMigration0040(
117 runner: () => Promise<{ exists: boolean; error?: string }>
118): Promise<CheckResult> {
119 try {
120 const { exists, error } = await runner();
121 if (error) {
122 return {
123 name: "Migration 0040 applied",
124 status: "fail",
125 reason: `column probe failed: ${error}`,
126 };
127 }
128 if (!exists) {
129 return {
130 name: "Migration 0040 applied",
131 status: "fail",
132 reason:
133 "branch_protection.enable_auto_merge column missing — run `bun run db:migrate`.",
134 };
135 }
136 return { name: "Migration 0040 applied", status: "pass" };
137 } catch (err) {
138 return {
139 name: "Migration 0040 applied",
140 status: "fail",
141 reason: err instanceof Error ? err.message : String(err),
142 };
143 }
144}
145
146// ---------------------------------------------------------------------------
147// CLI driver
148// ---------------------------------------------------------------------------
149
150async function probeAutoMergeColumn(): Promise<{ exists: boolean; error?: string }> {
151 try {
152 const { db } = await import("../src/db");
153 // Pull the column out of information_schema. Works on Postgres and
154 // is safe to run on a healthy migrated DB.
155 const rows = await db.execute(
156 sql`SELECT column_name FROM information_schema.columns
157 WHERE table_name = 'branch_protection'
158 AND column_name = 'enable_auto_merge'
159 LIMIT 1`
160 );
161 const list =
162 (rows as any).rows ?? (Array.isArray(rows) ? rows : []);
163 return { exists: list.length > 0 };
164 } catch (err) {
165 return {
166 exists: false,
167 error: err instanceof Error ? err.message : String(err),
168 };
169 }
170}
171
172async function getDefaultTasks(): Promise<Array<{ name: string }>> {
173 try {
174 const mod = await import("../src/lib/autopilot");
175 return mod.defaultTasks();
176 } catch {
177 return [];
178 }
179}
180
181async function main() {
182 console.log(
183 `${DIM}gluecron auto-merge readiness — ${new Date().toISOString()}${RESET}`
184 );
185
186 const results: CheckResult[] = [];
187
188 results.push(
189 await checkMigration0040(probeAutoMergeColumn)
190 );
191 results.push(checkAnthropicKey(process.env));
192 results.push(checkAutopilotEnabled(process.env));
193 results.push(checkAutoMergeSweepRegistered(await getDefaultTasks()));
194
195 for (const r of results) {
196 const tail = r.reason ? ` — ${r.reason}` : "";
197 console.log(` ${icon(r.status)} ${r.name}${tail}`);
198 }
199
200 const failed = results.filter((r) => r.status === "fail").length;
201 console.log("");
202 if (failed > 0) {
203 console.log(
204 `${RED}readiness FAILED — fix the items above before running enable-auto-merge.ts${RESET}`
205 );
206 process.exit(1);
207 }
208 console.log(
209 `${GREEN}readiness clean — safe to run enable-auto-merge.ts${RESET}`
210 );
211 process.exit(0);
212}
213
214if (import.meta.main) {
215 main().catch((err) => {
216 console.error(err instanceof Error ? err.message : String(err));
217 process.exit(1);
218 });
219}