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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
check-auto-merge-readiness.ts6.7 KB · 219 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/**
 * Block N1 — Auto-merge readiness preflight.
 *
 * Operator runs this BEFORE `enable-auto-merge.ts` to make sure the box
 * is actually in a state where flipping the switch will produce useful
 * behaviour. Exits 0 when every check is green, 1 when any check fails.
 *
 * Checks:
 *   1. Migration 0040 has been applied (`branch_protection.enable_auto_merge`
 *      column exists). The bootstrap script depends on this column.
 *   2. `ANTHROPIC_API_KEY` is set on the box — without it the AI approval
 *      gate degrades to "AI review unavailable", which is treated as
 *      not-approved, and every auto-merge candidate gets blocked.
 *   3. The autopilot is running — `AUTOPILOT_DISABLED` is not `"1"`.
 *      The K3 sweep task is the only thing that actually performs the
 *      merge; if the autopilot is off, opt-in does nothing.
 *   4. The K3 `auto-merge-sweep` task is registered in `defaultTasks()`.
 *      Guards against someone removing the task name during a refactor.
 *
 * Pure-ish: each check is a small async function that returns a Result
 * record. The runner just iterates them and prints a checklist. Tests
 * drive the pure helpers directly.
 */

import { sql } from "drizzle-orm";

// ---------------------------------------------------------------------------
// Types + pretty printers
// ---------------------------------------------------------------------------

export type CheckStatus = "pass" | "fail";

export interface CheckResult {
  name: string;
  status: CheckStatus;
  reason?: string;
}

const GREEN = "\x1b[32m";
const RED = "\x1b[31m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";

function icon(s: CheckStatus): string {
  return s === "pass" ? `${GREEN}v${RESET}` : `${RED}x${RESET}`;
}

// ---------------------------------------------------------------------------
// Pure check helpers
// ---------------------------------------------------------------------------

/**
 * Test the `ANTHROPIC_API_KEY` env var. Pure function over the supplied
 * env object so tests don't have to mutate `process.env`.
 */
export function checkAnthropicKey(env: NodeJS.ProcessEnv): CheckResult {
  const key = env.ANTHROPIC_API_KEY;
  if (!key || key.length < 10) {
    return {
      name: "ANTHROPIC_API_KEY set",
      status: "fail",
      reason:
        "ANTHROPIC_API_KEY is missing — AI approval gate will block every auto-merge candidate.",
    };
  }
  return { name: "ANTHROPIC_API_KEY set", status: "pass" };
}

/**
 * Confirm the autopilot ticker is enabled. `AUTOPILOT_DISABLED=1` turns
 * the K3 sweep off, which means opt-in does nothing useful.
 */
export function checkAutopilotEnabled(env: NodeJS.ProcessEnv): CheckResult {
  if (env.AUTOPILOT_DISABLED === "1") {
    return {
      name: "Autopilot enabled (AUTOPILOT_DISABLED != 1)",
      status: "fail",
      reason:
        "AUTOPILOT_DISABLED=1 — unset (or set to 0) to let the K3 sweep run.",
    };
  }
  return {
    name: "Autopilot enabled (AUTOPILOT_DISABLED != 1)",
    status: "pass",
  };
}

/**
 * Confirm the K3 sweep task is registered. We accept any iterable that
 * yields objects with a `.name` so tests don't have to import the real
 * autopilot module.
 */
export function checkAutoMergeSweepRegistered(
  tasks: Array<{ name: string }>
): CheckResult {
  const found = tasks.some((t) => t.name === "auto-merge-sweep");
  if (!found) {
    return {
      name: "K3 auto-merge-sweep task registered",
      status: "fail",
      reason:
        "defaultTasks() does not include 'auto-merge-sweep' — the K3 sweep is missing.",
    };
  }
  return {
    name: "K3 auto-merge-sweep task registered",
    status: "pass",
  };
}

/**
 * Verify migration 0040 has landed by probing for the
 * `branch_protection.enable_auto_merge` column. Pure-ish: takes the
 * runner as a callback so tests can stub it.
 */
export async function checkMigration0040(
  runner: () => Promise<{ exists: boolean; error?: string }>
): Promise<CheckResult> {
  try {
    const { exists, error } = await runner();
    if (error) {
      return {
        name: "Migration 0040 applied",
        status: "fail",
        reason: `column probe failed: ${error}`,
      };
    }
    if (!exists) {
      return {
        name: "Migration 0040 applied",
        status: "fail",
        reason:
          "branch_protection.enable_auto_merge column missing — run `bun run db:migrate`.",
      };
    }
    return { name: "Migration 0040 applied", status: "pass" };
  } catch (err) {
    return {
      name: "Migration 0040 applied",
      status: "fail",
      reason: err instanceof Error ? err.message : String(err),
    };
  }
}

// ---------------------------------------------------------------------------
// CLI driver
// ---------------------------------------------------------------------------

async function probeAutoMergeColumn(): Promise<{ exists: boolean; error?: string }> {
  try {
    const { db } = await import("../src/db");
    // Pull the column out of information_schema. Works on Postgres and
    // is safe to run on a healthy migrated DB.
    const rows = await db.execute(
      sql`SELECT column_name FROM information_schema.columns
          WHERE table_name = 'branch_protection'
            AND column_name = 'enable_auto_merge'
          LIMIT 1`
    );
    const list =
      (rows as any).rows ?? (Array.isArray(rows) ? rows : []);
    return { exists: list.length > 0 };
  } catch (err) {
    return {
      exists: false,
      error: err instanceof Error ? err.message : String(err),
    };
  }
}

async function getDefaultTasks(): Promise<Array<{ name: string }>> {
  try {
    const mod = await import("../src/lib/autopilot");
    return mod.defaultTasks();
  } catch {
    return [];
  }
}

async function main() {
  console.log(
    `${DIM}gluecron auto-merge readiness — ${new Date().toISOString()}${RESET}`
  );

  const results: CheckResult[] = [];

  results.push(
    await checkMigration0040(probeAutoMergeColumn)
  );
  results.push(checkAnthropicKey(process.env));
  results.push(checkAutopilotEnabled(process.env));
  results.push(checkAutoMergeSweepRegistered(await getDefaultTasks()));

  for (const r of results) {
    const tail = r.reason ? ` — ${r.reason}` : "";
    console.log(`  ${icon(r.status)} ${r.name}${tail}`);
  }

  const failed = results.filter((r) => r.status === "fail").length;
  console.log("");
  if (failed > 0) {
    console.log(
      `${RED}readiness FAILED — fix the items above before running enable-auto-merge.ts${RESET}`
    );
    process.exit(1);
  }
  console.log(
    `${GREEN}readiness clean — safe to run enable-auto-merge.ts${RESET}`
  );
  process.exit(0);
}

if (import.meta.main) {
  main().catch((err) => {
    console.error(err instanceof Error ? err.message : String(err));
    process.exit(1);
  });
}