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

automation-settings.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

automation-settings.tsxBlame503 lines · 1 contributor
479dcd9Claude1/**
2 * Per-repo Automation settings — ONE page where a developer sees every
3 * automation on the platform and flips each between off / suggest (manual)
4 * / automatic, wherever those modes exist today.
5 *
6 * GET /:owner/:repo/settings/automation — the control table
7 * POST /:owner/:repo/settings/automation — save modes
8 *
9 * Admin-gated (same requireAuth + requireRepoAccess("admin") pattern as
10 * src/routes/repo-settings.tsx).
11 *
12 * Storage:
13 * - The five mode-controlled automations (AI review, PR triage, issue
14 * triage, auto-merge, CI autofix) live in `repo_automation_settings`
15 * (migration 0106) via src/lib/automation-settings.ts.
16 * - AI test generation and the dependency updater keep their existing
17 * homes on the repositories row (`auto_generate_tests`,
18 * `dep_updater_enabled`) — this page is just a second door to the same
19 * columns, so the older toggles stay in sync.
20 *
21 * Env kill-switches stay supreme: a feature disabled at the environment
22 * level is off regardless of what is selected here. The page says so.
23 */
24
25import { Hono } from "hono";
26import { eq } from "drizzle-orm";
27import { db } from "../db";
28import { repositories } from "../db/schema";
29import type { Repository } from "../db/schema";
30import { Layout } from "../views/layout";
31import { RepoHeader } from "../views/components";
32import { softAuth, requireAuth } from "../middleware/auth";
33import type { AuthEnv } from "../middleware/auth";
34import { requireRepoAccess } from "../middleware/repo-access";
35import {
36 getAutomationSettings,
37 upsertAutomationSettings,
38 normalizeMode,
39 type AutomationMode,
40 type AutomationSettings,
41} from "../lib/automation-settings";
42
43const automationSettings = new Hono<AuthEnv>();
44
45automationSettings.use("*", softAuth);
46
47// Scoped CSS — every class prefixed `.automation-` so styles cannot bleed.
48// Mirrors the section-card system in repo-settings.tsx.
49const automationStyles = `
50 .automation-container { max-width: 1080px; margin: 0 auto; padding: 0 var(--space-3) var(--space-8); }
51 .automation-hero {
52 position: relative;
53 margin: var(--space-5) 0;
54 padding: var(--space-5) var(--space-6);
55 background: var(--bg-elevated);
56 border: 1px solid var(--border);
57 border-radius: 16px;
58 overflow: hidden;
59 }
60 .automation-hero::before {
61 content: '';
62 position: absolute;
63 top: 0; left: 0; right: 0;
64 height: 2px;
64aa989Claude65 background: var(--border-strong);
479dcd9Claude66 pointer-events: none;
67 }
68 .automation-hero-eyebrow {
69 font-size: 11px;
70 font-weight: 600;
71 letter-spacing: 0.08em;
72 text-transform: uppercase;
73 color: var(--accent);
74 margin-bottom: 6px;
75 }
76 .automation-hero-title {
77 font-family: var(--font-display);
78 font-size: clamp(26px, 4vw, 36px);
79 font-weight: 800;
80 letter-spacing: -0.028em;
81 line-height: 1.05;
82 margin: 0 0 var(--space-2);
83 color: var(--text-strong);
84 }
85 .automation-hero-sub {
86 font-size: 14.5px;
87 color: var(--text-muted);
88 margin: 0;
89 line-height: 1.55;
90 max-width: 680px;
91 }
92 .automation-hero-sub code {
93 font-family: var(--font-mono);
94 font-size: 12.5px;
95 background: var(--bg-tertiary);
96 padding: 1px 5px;
97 border-radius: 4px;
98 }
99 .automation-banner {
100 display: flex;
101 align-items: center;
102 gap: 10px;
103 padding: 12px 16px;
104 border-radius: 12px;
105 font-size: 13.5px;
106 margin-bottom: var(--space-4);
107 line-height: 1.5;
108 }
109 .automation-banner-success {
110 background: rgba(52,211,153,0.08);
111 color: #6ee7b7;
112 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.30);
113 }
114 .automation-banner-error {
115 background: rgba(248,113,113,0.08);
116 color: #fca5a5;
117 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.30);
118 }
119 .automation-card {
120 background: var(--bg-elevated);
121 border: 1px solid var(--border);
122 border-radius: 14px;
123 overflow: hidden;
124 margin-bottom: var(--space-5);
125 }
126 .automation-table { width: 100%; border-collapse: collapse; }
127 .automation-table th {
128 text-align: left;
129 font-size: 11px;
130 font-weight: 600;
131 letter-spacing: 0.08em;
132 text-transform: uppercase;
133 color: var(--text-muted);
134 padding: 12px var(--space-5);
135 border-bottom: 1px solid var(--border);
136 }
137 .automation-table td {
138 padding: 14px var(--space-5);
139 border-bottom: 1px solid var(--border);
140 vertical-align: top;
141 }
142 .automation-table tr:last-child td { border-bottom: none; }
143 .automation-feature-name {
144 font-weight: 600;
145 font-size: 13.5px;
146 color: var(--text-strong);
147 white-space: nowrap;
148 }
149 .automation-feature-desc {
150 font-size: 12.5px;
151 color: var(--text-muted);
152 line-height: 1.5;
153 max-width: 520px;
154 }
155 .automation-feature-desc code {
156 font-family: var(--font-mono);
157 font-size: 11.5px;
158 background: var(--bg-tertiary);
159 padding: 1px 4px;
160 border-radius: 4px;
161 }
162 .automation-select {
163 background: var(--bg-tertiary);
164 color: var(--text-strong);
165 border: 1px solid var(--border);
166 border-radius: 8px;
167 padding: 6px 10px;
168 font-size: 13px;
169 min-width: 150px;
170 }
171 .automation-env-pill {
172 display: inline-block;
173 font-size: 11px;
174 font-weight: 600;
175 padding: 2px 8px;
176 border-radius: 9999px;
6fd5915Claude177 background: rgba(91,110,232,0.12);
178 color: #5b6ee8;
479dcd9Claude179 white-space: nowrap;
180 }
181 .automation-foot {
182 padding: var(--space-3) var(--space-5);
183 border-top: 1px solid var(--border);
184 background: rgba(255,255,255,0.012);
185 display: flex;
186 justify-content: flex-end;
187 align-items: center;
188 gap: var(--space-3);
189 }
190 .automation-foot-hint {
191 margin-right: auto;
192 font-size: 12.5px;
193 color: var(--text-muted);
194 }
195 .automation-cta {
196 display: inline-flex;
197 align-items: center;
198 gap: 7px;
199 padding: 9px 18px;
64aa989Claude200 background: var(--accent);
479dcd9Claude201 color: #fff;
202 border: none;
203 border-radius: 9px;
204 font-size: 13.5px;
205 font-weight: 600;
206 cursor: pointer;
207 }
208 .automation-cta:hover { filter: brightness(1.08); }
209`;
210
211/** One row of the control table — a three-way (or two-way) mode selector. */
212function ModeSelect(props: {
213 name: string;
214 value: AutomationMode;
215 modes: AutomationMode[];
216 labels?: Partial<Record<AutomationMode, string>>;
217}) {
218 const defaultLabels: Record<AutomationMode, string> = {
219 off: "Off",
220 suggest: "Suggest (manual)",
221 auto: "Automatic",
222 };
223 return (
224 <select class="automation-select" name={props.name} aria-label={props.name}>
225 {props.modes.map((m) => (
226 <option value={m} selected={m === props.value}>
227 {props.labels?.[m] ?? defaultLabels[m]}
228 </option>
229 ))}
230 </select>
231 );
232}
233
234automationSettings.get(
235 "/:owner/:repo/settings/automation",
236 requireAuth,
237 requireRepoAccess("admin"),
238 async (c) => {
239 const { owner: ownerName, repo: repoName } = c.req.param();
240 const user = c.get("user")!;
241 const repo = c.get("repository" as never) as Repository;
242 const success = c.req.query("success");
243 const error = c.req.query("error");
244
245 const settings = await getAutomationSettings(repo.id);
246
247 return c.html(
248 <Layout title={`Automation — ${ownerName}/${repoName}`} user={user}>
249 <RepoHeader owner={ownerName} repo={repoName} />
250 <style dangerouslySetInnerHTML={{ __html: automationStyles }} />
251 <div class="automation-container">
252 <div class="automation-hero">
253 <div class="automation-hero-eyebrow">Repository settings</div>
254 <h1 class="automation-hero-title">Automation</h1>
255 <p class="automation-hero-sub">
256 Every automation on this repository in one place. <strong>Off</strong>{" "}
257 disables a feature, <strong>Suggest</strong> posts advisory
258 comments and leaves the action to you, <strong>Automatic</strong>{" "}
259 lets Gluecron act on its own. Server-level kill-switches (e.g. a
260 missing <code>ANTHROPIC_API_KEY</code>) always win — a feature
261 disabled in the environment stays off no matter what you pick
262 here.
263 </p>
264 </div>
265
266 {success && (
267 <div class="automation-banner automation-banner-success">
268 {decodeURIComponent(success)}
269 </div>
270 )}
271 {error && (
272 <div class="automation-banner automation-banner-error">
273 {decodeURIComponent(error)}
274 </div>
275 )}
276
277 <form method="post" action={`/${ownerName}/${repoName}/settings/automation`}>
278 <div class="automation-card">
279 <table class="automation-table">
280 <thead>
281 <tr>
282 <th>Automation</th>
283 <th>What it does</th>
284 <th>Mode</th>
285 </tr>
286 </thead>
287 <tbody>
288 <tr>
289 <td class="automation-feature-name">AI code review</td>
290 <td class="automation-feature-desc">
291 Claude reviews every non-draft PR diff on open and posts
292 a summary plus inline findings. Review comments are
293 always advisory.
294 </td>
295 <td>
296 <ModeSelect
297 name="ai_review_mode"
298 value={settings.aiReviewMode}
299 modes={["off", "suggest"]}
300 labels={{ suggest: "Suggest (on)" }}
301 />
302 </td>
303 </tr>
304 <tr>
305 <td class="automation-feature-name">PR triage</td>
306 <td class="automation-feature-desc">
307 Suggests labels, reviewers, priority, and a risk area as
308 a comment when a PR opens. Nothing is applied for you.
309 </td>
310 <td>
311 <ModeSelect
312 name="pr_triage_mode"
313 value={settings.prTriageMode}
314 modes={["off", "suggest"]}
315 labels={{ suggest: "Suggest (on)" }}
316 />
317 </td>
318 </tr>
319 <tr>
320 <td class="automation-feature-name">Issue triage</td>
321 <td class="automation-feature-desc">
322 Suggests labels, priority, and possible duplicates as a
323 comment when an issue is created.
324 </td>
325 <td>
326 <ModeSelect
327 name="issue_triage_mode"
328 value={settings.issueTriageMode}
329 modes={["off", "suggest"]}
330 labels={{ suggest: "Suggest (on)" }}
331 />
332 </td>
333 </tr>
334 <tr>
335 <td class="automation-feature-name">Auto-merge</td>
336 <td class="automation-feature-desc">
337 Merges a PR once every branch-protection gate is green.
338 Still default-deny per branch — a rule with{" "}
339 <a href={`/${ownerName}/${repoName}/settings#branch-protection`}>
340 auto-merge enabled
341 </a>{" "}
342 must match the base branch. <em>Suggest</em> evaluates
343 and records the decision but leaves the Merge click to a
344 human.
345 </td>
346 <td>
347 <ModeSelect
348 name="auto_merge_mode"
349 value={settings.autoMergeMode}
350 modes={["off", "suggest", "auto"]}
351 />
352 </td>
353 </tr>
354 <tr>
355 <td class="automation-feature-name">CI auto-fix</td>
356 <td class="automation-feature-desc">
357 When a gate run fails on a PR, posts a ready-to-apply
358 patch (repair-cache first, then Claude). <em>Suggest</em>{" "}
359 stops at the comment with an Apply Fix button;{" "}
360 <em>Automatic</em> also applies the patch onto a{" "}
361 <code>fix/</code> branch.
362 </td>
363 <td>
364 <ModeSelect
365 name="ci_autofix_mode"
366 value={settings.ciAutofixMode}
367 modes={["off", "suggest", "auto"]}
368 />
369 </td>
370 </tr>
371 <tr>
372 <td class="automation-feature-name">AI test generation</td>
373 <td class="automation-feature-desc">
374 Writes tests for new code when a PR opens and commits
375 them onto the same branch. Acts on its own once enabled.
376 </td>
377 <td>
378 <ModeSelect
379 name="auto_generate_tests"
380 value={repo.autoGenerateTests ? "auto" : "off"}
381 modes={["off", "auto"]}
382 />
383 </td>
384 </tr>
385 <tr>
386 <td class="automation-feature-name">Dependency updates</td>
387 <td class="automation-feature-desc">
388 Daily patch/minor dependency bumps; auto-merges when the
389 gate passes, opens a PR with an AI migration guide when
390 it fails. Also needs <code>DEP_UPDATER_ENABLED=1</code>{" "}
391 on the server.{" "}
392 <a href={`/${ownerName}/${repoName}/settings/dep-updater`}>
393 Run history →
394 </a>
395 </td>
396 <td>
397 <ModeSelect
398 name="dep_updater_enabled"
399 value={
400 (repo as { depUpdaterEnabled?: boolean })
401 .depUpdaterEnabled
402 ? "auto"
403 : "off"
404 }
405 modes={["off", "auto"]}
406 />
407 </td>
408 </tr>
64aa989Claude409 <tr>
410 <td class="automation-feature-name">Auto-repair</td>
411 <td class="automation-feature-desc">
412 After every push, the bot scans the diff for common
413 issues (hardcoded secrets, insecure patterns, whitespace)
414 and <strong>commits fixes directly to your branch</strong>.{" "}
415 Set to <strong>Off</strong> to prevent the bot from
416 modifying your branch.
417 </td>
418 <td>
419 <ModeSelect
420 name="auto_repair_mode"
421 value={settings.autoRepairMode}
422 modes={["off", "suggest"]}
423 labels={{ suggest: "On (auto-commit)" }}
424 />
425 </td>
426 </tr>
479dcd9Claude427 <tr>
428 <td class="automation-feature-name">AI loop (issue → PR → merge)</td>
429 <td class="automation-feature-desc">
430 The fully-autonomous build loop. Controlled by the
431 server-level <code>AI_LOOP_ENABLED=1</code> flag — there
432 is no per-repo dial for it today.
433 </td>
434 <td>
435 <span class="automation-env-pill">env-controlled</span>
436 </td>
437 </tr>
438 </tbody>
439 </table>
440 <div class="automation-foot">
441 <span class="automation-foot-hint">
442 Defaults match prior behavior — saving without changes
443 changes nothing.
444 </span>
445 <button type="submit" class="automation-cta">
446 Save automation settings <span>→</span>
447 </button>
448 </div>
449 </div>
450 </form>
451 </div>
452 </Layout>
453 );
454 }
455);
456
457automationSettings.post(
458 "/:owner/:repo/settings/automation",
459 requireAuth,
460 requireRepoAccess("admin"),
461 async (c) => {
462 const { owner: ownerName, repo: repoName } = c.req.param();
463 const repo = c.get("repository" as never) as Repository;
464 const body = await c.req.parseBody();
465 const base = `/${ownerName}/${repoName}/settings/automation`;
466
467 try {
468 await upsertAutomationSettings(repo.id, {
469 aiReviewMode: normalizeMode(body["ai_review_mode"], "suggest"),
470 prTriageMode: normalizeMode(body["pr_triage_mode"], "suggest"),
471 issueTriageMode: normalizeMode(body["issue_triage_mode"], "suggest"),
64aa989Claude472 autoMergeMode: normalizeMode(body["auto_merge_mode"], "off"),
479dcd9Claude473 ciAutofixMode: normalizeMode(body["ci_autofix_mode"], "suggest"),
64aa989Claude474 autoRepairMode: normalizeMode(body["auto_repair_mode"], "suggest"),
479dcd9Claude475 });
476
477 // The two automations that already lived on the repositories row keep
478 // their existing storage so the older settings sections stay in sync.
479 await db
480 .update(repositories)
481 .set({
482 autoGenerateTests: body["auto_generate_tests"] === "auto",
483 depUpdaterEnabled: body["dep_updater_enabled"] === "auto",
484 updatedAt: new Date(),
485 })
486 .where(eq(repositories.id, repo.id));
487 } catch (err) {
488 console.error(
489 "[automation-settings] save failed:",
490 err instanceof Error ? err.message : err
491 );
492 return c.redirect(
493 `${base}?error=${encodeURIComponent("Could not save automation settings. Please try again.")}`
494 );
495 }
496
497 return c.redirect(
498 `${base}?success=${encodeURIComponent("Automation settings saved.")}`
499 );
500 }
501);
502
503export default automationSettings;