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

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.tsxBlame577 lines · 2 contributors
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`}>
f183fdfClaude278 <div class="automation-card" style="margin-bottom: var(--space-4);">
279 <div style="padding: 14px var(--space-5); border-bottom: 1px solid var(--border);">
280 <div style="font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px;">Direct actions — these automations modify your repository</div>
281 <div style="font-size: 12.5px; color: var(--text-muted);">These three automations commit code or open PRs/issues on your behalf. Off by default.</div>
282 </div>
283 <div style="display: flex; gap: var(--space-4); padding: var(--space-4) var(--space-5); flex-wrap: wrap;">
284 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
285 <input type="checkbox" name="auto_repair_quick" value="suggest"
286 checked={settings.autoRepairMode !== "off"}
8ed88f2ccantynz-alt287 onchange="this.form.querySelector('[name=auto_repair_mode]').value = this.checked ? 'suggest' : 'off'"
f183fdfClaude288 style="width: 16px; height: 16px; cursor: pointer;" />
289 <div>
290 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Auto-repair</div>
291 <div style="font-size: 11.5px; color: var(--text-muted);">Commits fixes to your branch</div>
292 </div>
293 </label>
294 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
295 <input type="checkbox" name="auto_issues_quick" value="suggest"
296 checked={settings.autoIssuesMode !== "off"}
8ed88f2ccantynz-alt297 onchange="this.form.querySelector('[name=auto_issues_mode]').value = this.checked ? 'suggest' : 'off'"
f183fdfClaude298 style="width: 16px; height: 16px; cursor: pointer;" />
299 <div>
300 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Auto-issues</div>
301 <div style="font-size: 11.5px; color: var(--text-muted);">Opens issues for TODOs and security patterns</div>
302 </div>
303 </label>
304 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
305 <input type="checkbox" name="doc_drift_quick" value="suggest"
306 checked={settings.docDriftMode !== "off"}
8ed88f2ccantynz-alt307 onchange="this.form.querySelector('[name=doc_drift_mode]').value = this.checked ? 'suggest' : 'off'"
f183fdfClaude308 style="width: 16px; height: 16px; cursor: pointer;" />
309 <div>
310 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Doc-drift PRs</div>
311 <div style="font-size: 11.5px; color: var(--text-muted);">Opens PRs when docs drift from source</div>
312 </div>
313 </label>
314 </div>
315 </div>
479dcd9Claude316 <div class="automation-card">
317 <table class="automation-table">
318 <thead>
319 <tr>
320 <th>Automation</th>
321 <th>What it does</th>
322 <th>Mode</th>
323 </tr>
324 </thead>
325 <tbody>
326 <tr>
327 <td class="automation-feature-name">AI code review</td>
328 <td class="automation-feature-desc">
329 Claude reviews every non-draft PR diff on open and posts
330 a summary plus inline findings. Review comments are
331 always advisory.
332 </td>
333 <td>
334 <ModeSelect
335 name="ai_review_mode"
336 value={settings.aiReviewMode}
337 modes={["off", "suggest"]}
338 labels={{ suggest: "Suggest (on)" }}
339 />
340 </td>
341 </tr>
342 <tr>
343 <td class="automation-feature-name">PR triage</td>
344 <td class="automation-feature-desc">
345 Suggests labels, reviewers, priority, and a risk area as
346 a comment when a PR opens. Nothing is applied for you.
347 </td>
348 <td>
349 <ModeSelect
350 name="pr_triage_mode"
351 value={settings.prTriageMode}
352 modes={["off", "suggest"]}
353 labels={{ suggest: "Suggest (on)" }}
354 />
355 </td>
356 </tr>
357 <tr>
358 <td class="automation-feature-name">Issue triage</td>
359 <td class="automation-feature-desc">
360 Suggests labels, priority, and possible duplicates as a
361 comment when an issue is created.
362 </td>
363 <td>
364 <ModeSelect
365 name="issue_triage_mode"
366 value={settings.issueTriageMode}
367 modes={["off", "suggest"]}
368 labels={{ suggest: "Suggest (on)" }}
369 />
370 </td>
371 </tr>
372 <tr>
373 <td class="automation-feature-name">Auto-merge</td>
374 <td class="automation-feature-desc">
375 Merges a PR once every branch-protection gate is green.
376 Still default-deny per branch — a rule with{" "}
377 <a href={`/${ownerName}/${repoName}/settings#branch-protection`}>
378 auto-merge enabled
379 </a>{" "}
380 must match the base branch. <em>Suggest</em> evaluates
381 and records the decision but leaves the Merge click to a
382 human.
383 </td>
384 <td>
385 <ModeSelect
386 name="auto_merge_mode"
387 value={settings.autoMergeMode}
388 modes={["off", "suggest", "auto"]}
389 />
390 </td>
391 </tr>
392 <tr>
393 <td class="automation-feature-name">CI auto-fix</td>
394 <td class="automation-feature-desc">
395 When a gate run fails on a PR, posts a ready-to-apply
396 patch (repair-cache first, then Claude). <em>Suggest</em>{" "}
397 stops at the comment with an Apply Fix button;{" "}
398 <em>Automatic</em> also applies the patch onto a{" "}
399 <code>fix/</code> branch.
400 </td>
401 <td>
402 <ModeSelect
403 name="ci_autofix_mode"
404 value={settings.ciAutofixMode}
405 modes={["off", "suggest", "auto"]}
406 />
407 </td>
408 </tr>
409 <tr>
410 <td class="automation-feature-name">AI test generation</td>
411 <td class="automation-feature-desc">
412 Writes tests for new code when a PR opens and commits
413 them onto the same branch. Acts on its own once enabled.
414 </td>
415 <td>
416 <ModeSelect
417 name="auto_generate_tests"
418 value={repo.autoGenerateTests ? "auto" : "off"}
419 modes={["off", "auto"]}
420 />
421 </td>
422 </tr>
423 <tr>
424 <td class="automation-feature-name">Dependency updates</td>
425 <td class="automation-feature-desc">
426 Daily patch/minor dependency bumps; auto-merges when the
427 gate passes, opens a PR with an AI migration guide when
428 it fails. Also needs <code>DEP_UPDATER_ENABLED=1</code>{" "}
429 on the server.{" "}
430 <a href={`/${ownerName}/${repoName}/settings/dep-updater`}>
431 Run history →
432 </a>
433 </td>
434 <td>
435 <ModeSelect
436 name="dep_updater_enabled"
437 value={
438 (repo as { depUpdaterEnabled?: boolean })
439 .depUpdaterEnabled
440 ? "auto"
441 : "off"
442 }
443 modes={["off", "auto"]}
444 />
445 </td>
446 </tr>
64aa989Claude447 <tr>
448 <td class="automation-feature-name">Auto-repair</td>
449 <td class="automation-feature-desc">
450 After every push, the bot scans the diff for common
451 issues (hardcoded secrets, insecure patterns, whitespace)
452 and <strong>commits fixes directly to your branch</strong>.{" "}
453 Set to <strong>Off</strong> to prevent the bot from
454 modifying your branch.
455 </td>
456 <td>
457 <ModeSelect
458 name="auto_repair_mode"
459 value={settings.autoRepairMode}
460 modes={["off", "suggest"]}
461 labels={{ suggest: "On (auto-commit)" }}
462 />
463 </td>
464 </tr>
f183fdfClaude465 <tr>
466 <td class="automation-feature-name">Auto-issues</td>
467 <td class="automation-feature-desc">
468 Scans the diff on each push for TODO/FIXME comments,
469 hardcoded secrets, and SQL injection patterns. Opens one
470 issue per finding type per file (capped at 5 per push).
471 Requires <code>AI_AUTO_ISSUES=1</code> server-side.
472 </td>
473 <td>
474 <ModeSelect
475 name="auto_issues_mode"
476 value={settings.autoIssuesMode}
477 modes={["off", "suggest"]}
478 labels={{ suggest: "On" }}
479 />
480 </td>
481 </tr>
482 <tr>
483 <td class="automation-feature-name">Doc-drift PRs</td>
484 <td class="automation-feature-desc">
485 After each push, checks whether docs marked with{" "}
486 <code>{"<!-- gluecron:doc-track -->"}</code> have drifted
487 from their referenced source. Opens a PR labelled{" "}
488 <code>ai:doc-update</code> when they have.
489 </td>
490 <td>
491 <ModeSelect
492 name="doc_drift_mode"
493 value={settings.docDriftMode}
494 modes={["off", "suggest"]}
495 labels={{ suggest: "On" }}
496 />
497 </td>
498 </tr>
479dcd9Claude499 <tr>
500 <td class="automation-feature-name">AI loop (issue → PR → merge)</td>
501 <td class="automation-feature-desc">
502 The fully-autonomous build loop. Controlled by the
503 server-level <code>AI_LOOP_ENABLED=1</code> flag — there
504 is no per-repo dial for it today.
505 </td>
506 <td>
507 <span class="automation-env-pill">env-controlled</span>
508 </td>
509 </tr>
510 </tbody>
511 </table>
512 <div class="automation-foot">
513 <span class="automation-foot-hint">
514 Defaults match prior behavior — saving without changes
515 changes nothing.
516 </span>
517 <button type="submit" class="automation-cta">
518 Save automation settings <span>→</span>
519 </button>
520 </div>
521 </div>
522 </form>
523 </div>
524 </Layout>
525 );
526 }
527);
528
529automationSettings.post(
530 "/:owner/:repo/settings/automation",
531 requireAuth,
532 requireRepoAccess("admin"),
533 async (c) => {
534 const { owner: ownerName, repo: repoName } = c.req.param();
535 const repo = c.get("repository" as never) as Repository;
536 const body = await c.req.parseBody();
537 const base = `/${ownerName}/${repoName}/settings/automation`;
538
539 try {
540 await upsertAutomationSettings(repo.id, {
541 aiReviewMode: normalizeMode(body["ai_review_mode"], "suggest"),
542 prTriageMode: normalizeMode(body["pr_triage_mode"], "suggest"),
543 issueTriageMode: normalizeMode(body["issue_triage_mode"], "suggest"),
64aa989Claude544 autoMergeMode: normalizeMode(body["auto_merge_mode"], "off"),
479dcd9Claude545 ciAutofixMode: normalizeMode(body["ci_autofix_mode"], "suggest"),
f183fdfClaude546 autoRepairMode: normalizeMode(body["auto_repair_mode"], "off"),
547 autoIssuesMode: normalizeMode(body["auto_issues_mode"], "off"),
548 docDriftMode: normalizeMode(body["doc_drift_mode"], "off"),
479dcd9Claude549 });
550
551 // The two automations that already lived on the repositories row keep
552 // their existing storage so the older settings sections stay in sync.
553 await db
554 .update(repositories)
555 .set({
556 autoGenerateTests: body["auto_generate_tests"] === "auto",
557 depUpdaterEnabled: body["dep_updater_enabled"] === "auto",
558 updatedAt: new Date(),
559 })
560 .where(eq(repositories.id, repo.id));
561 } catch (err) {
562 console.error(
563 "[automation-settings] save failed:",
564 err instanceof Error ? err.message : err
565 );
566 return c.redirect(
567 `${base}?error=${encodeURIComponent("Could not save automation settings. Please try again.")}`
568 );
569 }
570
571 return c.redirect(
572 `${base}?success=${encodeURIComponent("Automation settings saved.")}`
573 );
574 }
575);
576
577export default automationSettings;