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

automation-settings.tsBlame183 lines · 1 contributor
479dcd9Claude1/**
2 * Per-repo automation settings — the single read/write surface for the
3 * "Automation" settings page (`/:owner/:repo/settings/automation`) and for
4 * every automation dispatch site (AI review, PR/issue triage, auto-merge,
5 * CI autofix).
6 *
7 * Contract:
8 *
9 * - One `repo_automation_settings` row per repository (migration 0106).
10 * No row → `AUTOMATION_DEFAULTS`, which match pre-0106 behavior exactly,
11 * so a repo that never touches the page behaves as it always did.
12 * - `getAutomationSettings` FAILS OPEN: any DB error returns the defaults
13 * so a broken lookup can never change platform behavior.
14 * - Env kill-switches stay supreme. A feature disabled at the environment
15 * level (missing ANTHROPIC_API_KEY, AI_LOOP_ENABLED unset, …) is off
16 * regardless of the stored mode — see `resolveEffectiveMode`. Dispatch
17 * sites keep their existing env guards; this module only ever *narrows*
18 * what runs, never widens it.
19 *
20 * Mode semantics per feature (see drizzle/0106 for the long-form docs):
21 *
22 * aiReviewMode 'off' | 'suggest' (default 'suggest')
23 * prTriageMode 'off' | 'suggest' (default 'suggest')
24 * issueTriageMode 'off' | 'suggest' (default 'suggest')
64aa989Claude25 * autoMergeMode 'off' | 'suggest' | 'auto' (default 'off')
479dcd9Claude26 * ciAutofixMode 'off' | 'suggest' | 'auto' (default 'suggest')
64aa989Claude27 * autoRepairMode 'off' | 'suggest' (default 'suggest')
479dcd9Claude28 *
29 * Where only on/off is meaningful, 'auto' is treated the same as 'suggest'
30 * (i.e. "on") — `isAutomationOn` encodes that rule.
31 */
32
33import { eq } from "drizzle-orm";
34import { db } from "../db";
35import { repoAutomationSettings } from "../db/schema";
36
37// ---------------------------------------------------------------------------
38// Types + defaults
39// ---------------------------------------------------------------------------
40
41export type AutomationMode = "off" | "suggest" | "auto";
42
43export interface AutomationSettings {
44 aiReviewMode: AutomationMode;
45 prTriageMode: AutomationMode;
46 issueTriageMode: AutomationMode;
47 autoMergeMode: AutomationMode;
48 ciAutofixMode: AutomationMode;
64aa989Claude49 autoRepairMode: AutomationMode;
479dcd9Claude50}
51
52/** Loader signature — the DI seam dispatch sites accept for tests. */
53export type AutomationSettingsLoader = (
54 repositoryId: string
55) => Promise<AutomationSettings>;
56
57/**
58 * Defaults = pre-0106 platform behavior. Auto-merge defaults to 'auto'
59 * because the K2/K3 path already merges automatically (still default-deny
60 * per branch via branch_protection.enable_auto_merge); everything else
61 * defaults to 'suggest' because those features only ever posted advisory
62 * comments.
63 */
64export const AUTOMATION_DEFAULTS: Readonly<AutomationSettings> = Object.freeze({
65 aiReviewMode: "suggest",
66 prTriageMode: "suggest",
67 issueTriageMode: "suggest",
64aa989Claude68 autoMergeMode: "off",
479dcd9Claude69 ciAutofixMode: "suggest",
64aa989Claude70 autoRepairMode: "suggest",
479dcd9Claude71});
72
73// ---------------------------------------------------------------------------
74// Pure mode-resolution helpers (unit-testable, no DB)
75// ---------------------------------------------------------------------------
76
77/** Parse an untrusted value (form input, stale DB row) into a mode. */
78export function normalizeMode(
79 value: unknown,
80 fallback: AutomationMode
81): AutomationMode {
82 return value === "off" || value === "suggest" || value === "auto"
83 ? value
84 : fallback;
85}
86
87/**
88 * Env-supremacy rule: when the environment kill-switch for a feature is
89 * off, the effective mode is 'off' no matter what the repo row says. The
90 * repo setting can only narrow further (e.g. env on + repo 'off' → 'off').
91 */
92export function resolveEffectiveMode(
93 repoMode: AutomationMode,
94 envEnabled: boolean
95): AutomationMode {
96 if (!envEnabled) return "off";
97 return repoMode;
98}
99
100/** "Is this feature on at all?" — 'suggest' and 'auto' both count as on. */
101export function isAutomationOn(mode: AutomationMode): boolean {
102 return mode !== "off";
103}
104
105/**
106 * Coerce a raw row (or anything row-shaped) into a fully-valid settings
107 * object, falling back per-field to the defaults. Exported for tests.
108 */
109export function settingsFromRow(
110 row: Partial<Record<keyof AutomationSettings, unknown>> | null | undefined
111): AutomationSettings {
112 if (!row) return { ...AUTOMATION_DEFAULTS };
113 return {
114 aiReviewMode: normalizeMode(row.aiReviewMode, AUTOMATION_DEFAULTS.aiReviewMode),
115 prTriageMode: normalizeMode(row.prTriageMode, AUTOMATION_DEFAULTS.prTriageMode),
116 issueTriageMode: normalizeMode(
117 row.issueTriageMode,
118 AUTOMATION_DEFAULTS.issueTriageMode
119 ),
120 autoMergeMode: normalizeMode(row.autoMergeMode, AUTOMATION_DEFAULTS.autoMergeMode),
121 ciAutofixMode: normalizeMode(row.ciAutofixMode, AUTOMATION_DEFAULTS.ciAutofixMode),
64aa989Claude122 autoRepairMode: normalizeMode(row.autoRepairMode, AUTOMATION_DEFAULTS.autoRepairMode),
479dcd9Claude123 };
124}
125
126// ---------------------------------------------------------------------------
127// DB-backed loader + upsert
128// ---------------------------------------------------------------------------
129
130/**
131 * Load the automation settings for a repository. No row → defaults.
132 * FAILS OPEN: any DB error also returns the defaults (logged once at warn)
133 * so a broken settings lookup never alters current platform behavior.
134 */
135export async function getAutomationSettings(
136 repositoryId: string
137): Promise<AutomationSettings> {
138 try {
139 const [row] = await db
140 .select()
141 .from(repoAutomationSettings)
142 .where(eq(repoAutomationSettings.repositoryId, repositoryId))
143 .limit(1);
144 return settingsFromRow(row ?? null);
145 } catch (err) {
146 console.warn(
147 "[automation-settings] lookup failed (falling back to defaults):",
148 err instanceof Error ? err.message : err
149 );
150 return { ...AUTOMATION_DEFAULTS };
151 }
152}
153
154/**
155 * Create or update the settings row for a repository. Partial patches are
156 * merged over the current effective settings so a form that only posts one
157 * field can't reset the others. Throws on DB failure — callers (the
158 * settings route) surface the error to the user.
159 */
160export async function upsertAutomationSettings(
161 repositoryId: string,
162 patch: Partial<AutomationSettings>
163): Promise<AutomationSettings> {
164 const current = await getAutomationSettings(repositoryId);
165 const next: AutomationSettings = {
166 aiReviewMode: normalizeMode(patch.aiReviewMode, current.aiReviewMode),
167 prTriageMode: normalizeMode(patch.prTriageMode, current.prTriageMode),
168 issueTriageMode: normalizeMode(patch.issueTriageMode, current.issueTriageMode),
169 autoMergeMode: normalizeMode(patch.autoMergeMode, current.autoMergeMode),
170 ciAutofixMode: normalizeMode(patch.ciAutofixMode, current.ciAutofixMode),
64aa989Claude171 autoRepairMode: normalizeMode(patch.autoRepairMode, current.autoRepairMode),
479dcd9Claude172 };
173
174 await db
175 .insert(repoAutomationSettings)
176 .values({ repositoryId, ...next })
177 .onConflictDoUpdate({
178 target: repoAutomationSettings.repositoryId,
179 set: { ...next, updatedAt: new Date() },
180 });
181
182 return next;
183}