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

repo-bootstrap.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.

repo-bootstrap.tsBlame287 lines · 1 contributor
3ef4c9dClaude1/**
2 * Repo bootstrap — wires up the "full green ecosystem by default" stance.
3 *
4 * Called immediately after a new repository row is created (including on fork).
5 * Every setting defaults to the most protective configuration — all gates on,
6 * auto-repair on, auto-deploy gated on all-green. Owners can turn things off
7 * in settings but they never have to turn things on.
8 *
9 * This is the heart of the "nothing broken reaches the customer" posture.
10 */
11
12import { db } from "../db";
13import {
14 repoSettings,
15 branchProtection,
16 labels,
17 issues,
18 issueComments,
cde9983Claude19 issueLabels,
20 repositories,
3ef4c9dClaude21} from "../db/schema";
22import { audit } from "./notify";
23
24const DEFAULT_LABELS = [
25 { name: "bug", color: "#f85149", description: "Something is broken" },
26 { name: "feature", color: "#1f6feb", description: "New capability" },
27 { name: "enhancement", color: "#58a6ff", description: "Improvement to existing behaviour" },
28 { name: "security", color: "#d29922", description: "Security-related" },
29 { name: "performance", color: "#a371f7", description: "Performance-related" },
30 { name: "docs", color: "#3fb950", description: "Documentation" },
31 { name: "question", color: "#8b949e", description: "Further info requested" },
32 { name: "good first issue", color: "#7ee787", description: "Suitable for new contributors" },
33 { name: "ai-triaged", color: "#bc8cff", description: "Auto-triaged by GlueCron AI" },
cde9983Claude34 { name: "ai:build", color: "#f0883e", description: "Autopilot will implement this and open a PR" },
3ef4c9dClaude35];
36
37const WELCOME_BODY = `Welcome to your new GlueCron repository.
38
39Every repository ships with the **full green ecosystem** enabled by default — nothing broken ever reaches your customers.
40
41## What's enabled out of the box
42
43- **AI code review** on every pull request
44- **Green gate enforcement** — GateTest + AI review + merge check must all pass before merge
45- **Secret & security scanning** on every push
46- **Automated merge conflict resolution** when conflicts arise
47- **AI auto-repair** — failing gates trigger a fix attempt before a human is pinged
48- **Branch protection** on \`main\` — PR required, all gates green, AI approval required
9ecf5a4Claude49- **Auto-deploy** to Vapron on every passing push to \`main\`
3ef4c9dClaude50- **AI commit messages, PR summaries, and release changelogs** on demand
51
52You can toggle any of this in **Settings → Gates & Auto-repair**. The safe defaults are on.
53
54## Quick start
55
56Push your first commit:
57
58\`\`\`
59git remote add gluecron https://gluecron.com/YOUR_USERNAME/YOUR_REPO.git
60git push -u gluecron main
61\`\`\`
62
63Ask the assistant anything:
64
65\`\`\`
66Click "Ask AI" in the repo nav or press Cmd+K and type your question.
67\`\`\`
68
69Happy shipping.`;
70
71export interface BootstrapResult {
72 settingsCreated: boolean;
73 protectionCreated: boolean;
74 labelsCreated: number;
75 welcomeIssueNumber?: number;
76}
77
78export async function bootstrapRepository(opts: {
79 repositoryId: string;
80 ownerUserId: string;
81 defaultBranch?: string;
82 skipWelcomeIssue?: boolean;
83}): Promise<BootstrapResult> {
84 const branch = opts.defaultBranch || "main";
85 let settingsCreated = false;
86 let protectionCreated = false;
87 let labelsCreated = 0;
88 let welcomeIssueNumber: number | undefined;
89
90 // 1. Settings — all gates on, all AI features on
91 try {
92 await db.insert(repoSettings).values({
93 repositoryId: opts.repositoryId,
94 });
95 settingsCreated = true;
96 } catch (err) {
97 // Ignore unique-violation if settings already exist (fork case)
98 console.warn("[bootstrap] settings:", (err as Error).message);
99 }
100
101 // 2. Branch protection on the default branch — maximum safety
102 try {
103 await db.insert(branchProtection).values({
104 repositoryId: opts.repositoryId,
105 pattern: branch,
106 requirePullRequest: true,
107 requireGreenGates: true,
108 requireAiApproval: true,
109 requireHumanReview: false,
110 requiredApprovals: 0,
111 allowForcePush: false,
112 allowDeletion: false,
113 dismissStaleReviews: true,
114 });
115 protectionCreated = true;
116 } catch (err) {
117 console.warn("[bootstrap] protection:", (err as Error).message);
118 }
119
120 // 3. Default labels
121 try {
122 const rows = DEFAULT_LABELS.map((l) => ({
123 repositoryId: opts.repositoryId,
124 name: l.name,
125 color: l.color,
126 description: l.description,
127 }));
128 await db.insert(labels).values(rows).onConflictDoNothing?.();
129 labelsCreated = rows.length;
130 } catch (err) {
131 // onConflictDoNothing might not be available on all drizzle adapters; best-effort insert
132 for (const l of DEFAULT_LABELS) {
133 try {
134 await db.insert(labels).values({
135 repositoryId: opts.repositoryId,
136 name: l.name,
137 color: l.color,
138 description: l.description,
139 });
140 labelsCreated++;
141 } catch {
142 // already exists — ignore
143 }
144 }
145 }
146
147 // 4. Welcome issue (skippable for forks)
148 if (!opts.skipWelcomeIssue) {
149 try {
150 const [issue] = await db
151 .insert(issues)
152 .values({
153 repositoryId: opts.repositoryId,
154 authorId: opts.ownerUserId,
155 title: "Welcome to GlueCron",
156 body: WELCOME_BODY,
157 state: "open",
158 })
159 .returning();
160 welcomeIssueNumber = issue?.number;
161 } catch (err) {
162 console.warn("[bootstrap] welcome issue:", (err as Error).message);
163 }
164 }
165
166 await audit({
167 userId: opts.ownerUserId,
168 repositoryId: opts.repositoryId,
169 action: "repo.bootstrap",
170 metadata: {
171 settingsCreated,
172 protectionCreated,
173 labelsCreated,
174 welcomeIssueNumber,
175 },
176 });
177
178 return {
179 settingsCreated,
180 protectionCreated,
181 labelsCreated,
182 welcomeIssueNumber,
183 };
184}
185
cde9983Claude186const AI_BUILD_SEED_TITLE = "Add a welcome README with project overview";
187
188const AI_BUILD_SEED_BODY = `## What to build
189
190Add a \`README.md\` to this repository with:
191- A one-paragraph description of what this project does
192- Quick start instructions (install + run)
193- A brief note about the tech stack
194
195## Why this issue exists
196
197This issue is labelled \`ai:build\`. Gluecron's autopilot will automatically:
1981. Read this spec
1992. Ask Claude to implement it
2003. Open a draft pull request with the code
201
202You don't need to do anything. Just watch the PR appear. ✨
203
204To build your own features this way, open an issue, describe what you want, and add the \`ai:build\` label.`;
205
206/**
207 * Seeds an `ai:build`-labelled issue on a user's FIRST repository so they
208 * immediately discover the autopilot feature.
209 *
210 * Silently skips if this is not the owner's first repo, or if anything fails.
211 * Must be called fire-and-forget — it never throws.
212 */
213export async function ensureAiBuildSeedIssue(
214 repositoryId: string,
215 ownerId: string
216): Promise<void> {
217 try {
218 const { eq, and, sql } = await import("drizzle-orm");
219
220 // 1. Only seed on the owner's first repo.
221 const [{ n }] = await db
222 .select({ n: sql<number>`count(*)::int` })
223 .from(repositories)
224 .where(eq(repositories.ownerId, ownerId));
225 if ((n ?? 0) > 1) return;
226
227 // 2. Find the ai:build label for this repo (seeded by bootstrapRepository).
228 const [label] = await db
229 .select({ id: labels.id })
230 .from(labels)
231 .where(and(eq(labels.repositoryId, repositoryId), eq(labels.name, "ai:build")))
232 .limit(1);
233
234 if (!label) {
235 console.warn("[ai-build-seed] ai:build label not found for repo", repositoryId);
236 return;
237 }
238
239 // 3. Create the seed issue.
240 const [issue] = await db
241 .insert(issues)
242 .values({
243 repositoryId,
244 authorId: ownerId,
245 title: AI_BUILD_SEED_TITLE,
246 body: AI_BUILD_SEED_BODY,
247 state: "open",
248 })
249 .returning();
250
251 if (!issue) return;
252
253 // 4. Attach the ai:build label.
254 await db.insert(issueLabels).values({ issueId: issue.id, labelId: label.id });
255 } catch (err) {
256 console.warn("[ai-build-seed] failed:", (err as Error)?.message);
257 }
258}
259
3ef4c9dClaude260/**
261 * Convenience helper to load settings (creates defaults if missing).
262 */
263export async function getOrCreateSettings(repositoryId: string) {
264 const { eq } = await import("drizzle-orm");
265 const [existing] = await db
266 .select()
267 .from(repoSettings)
268 .where(eq(repoSettings.repositoryId, repositoryId))
269 .limit(1);
270 if (existing) return existing;
271
272 try {
273 const [row] = await db
274 .insert(repoSettings)
275 .values({ repositoryId })
276 .returning();
277 return row;
278 } catch {
279 // Race — someone else inserted, re-select
280 const [row] = await db
281 .select()
282 .from(repoSettings)
283 .where(eq(repoSettings.repositoryId, repositoryId))
284 .limit(1);
285 return row;
286 }
287}