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.
| f764c07 | 1 | /** |
| 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 | ||
| 25 | import { sql } from "drizzle-orm"; | |
| 26 | ||
| 27 | // --------------------------------------------------------------------------- | |
| 28 | // Types + pretty printers | |
| 29 | // --------------------------------------------------------------------------- | |
| 30 | ||
| 31 | export type CheckStatus = "pass" | "fail"; | |
| 32 | ||
| 33 | export interface CheckResult { | |
| 34 | name: string; | |
| 35 | status: CheckStatus; | |
| 36 | reason?: string; | |
| 37 | } | |
| 38 | ||
| 39 | const GREEN = "\x1b[32m"; | |
| 40 | const RED = "\x1b[31m"; | |
| 41 | const DIM = "\x1b[2m"; | |
| 42 | const RESET = "\x1b[0m"; | |
| 43 | ||
| 44 | function 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 | */ | |
| 56 | export 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 | */ | |
| 73 | export 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 | */ | |
| 93 | export 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 | */ | |
| 116 | export 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 | ||
| 150 | async 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 | ||
| 172 | async 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 | ||
| 181 | async 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 | ||
| 214 | if (import.meta.main) { | |
| 215 | main().catch((err) => { | |
| 216 | console.error(err instanceof Error ? err.message : String(err)); | |
| 217 | process.exit(1); | |
| 218 | }); | |
| 219 | } |