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