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.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.tsBlame193 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')
f183fdfClaude27 * autoRepairMode 'off' | 'suggest' (default 'off')
28 * autoIssuesMode 'off' | 'suggest' (default 'off')
29 * docDriftMode 'off' | 'suggest' (default 'off')
479dcd9Claude30 *
31 * Where only on/off is meaningful, 'auto' is treated the same as 'suggest'
32 * (i.e. "on") — `isAutomationOn` encodes that rule.
33 */
34
35import { eq } from "drizzle-orm";
36import { db } from "../db";
37import { repoAutomationSettings } from "../db/schema";
38
39// ---------------------------------------------------------------------------
40// Types + defaults
41// ---------------------------------------------------------------------------
42
43export type AutomationMode = "off" | "suggest" | "auto";
44
45export interface AutomationSettings {
46 aiReviewMode: AutomationMode;
47 prTriageMode: AutomationMode;
48 issueTriageMode: AutomationMode;
49 autoMergeMode: AutomationMode;
50 ciAutofixMode: AutomationMode;
64aa989Claude51 autoRepairMode: AutomationMode;
f183fdfClaude52 autoIssuesMode: AutomationMode;
53 docDriftMode: AutomationMode;
479dcd9Claude54}
55
56/** Loader signature — the DI seam dispatch sites accept for tests. */
57export type AutomationSettingsLoader = (
58 repositoryId: string
59) => Promise<AutomationSettings>;
60
61/**
62 * Defaults = pre-0106 platform behavior. Auto-merge defaults to 'auto'
63 * because the K2/K3 path already merges automatically (still default-deny
64 * per branch via branch_protection.enable_auto_merge); everything else
65 * defaults to 'suggest' because those features only ever posted advisory
66 * comments.
67 */
68export const AUTOMATION_DEFAULTS: Readonly<AutomationSettings> = Object.freeze({
69 aiReviewMode: "suggest",
70 prTriageMode: "suggest",
71 issueTriageMode: "suggest",
64aa989Claude72 autoMergeMode: "off",
479dcd9Claude73 ciAutofixMode: "suggest",
f183fdfClaude74 autoRepairMode: "off",
75 autoIssuesMode: "off",
76 docDriftMode: "off",
479dcd9Claude77});
78
79// ---------------------------------------------------------------------------
80// Pure mode-resolution helpers (unit-testable, no DB)
81// ---------------------------------------------------------------------------
82
83/** Parse an untrusted value (form input, stale DB row) into a mode. */
84export function normalizeMode(
85 value: unknown,
86 fallback: AutomationMode
87): AutomationMode {
88 return value === "off" || value === "suggest" || value === "auto"
89 ? value
90 : fallback;
91}
92
93/**
94 * Env-supremacy rule: when the environment kill-switch for a feature is
95 * off, the effective mode is 'off' no matter what the repo row says. The
96 * repo setting can only narrow further (e.g. env on + repo 'off' → 'off').
97 */
98export function resolveEffectiveMode(
99 repoMode: AutomationMode,
100 envEnabled: boolean
101): AutomationMode {
102 if (!envEnabled) return "off";
103 return repoMode;
104}
105
106/** "Is this feature on at all?" — 'suggest' and 'auto' both count as on. */
107export function isAutomationOn(mode: AutomationMode): boolean {
108 return mode !== "off";
109}
110
111/**
112 * Coerce a raw row (or anything row-shaped) into a fully-valid settings
113 * object, falling back per-field to the defaults. Exported for tests.
114 */
115export function settingsFromRow(
116 row: Partial<Record<keyof AutomationSettings, unknown>> | null | undefined
117): AutomationSettings {
118 if (!row) return { ...AUTOMATION_DEFAULTS };
119 return {
120 aiReviewMode: normalizeMode(row.aiReviewMode, AUTOMATION_DEFAULTS.aiReviewMode),
121 prTriageMode: normalizeMode(row.prTriageMode, AUTOMATION_DEFAULTS.prTriageMode),
122 issueTriageMode: normalizeMode(
123 row.issueTriageMode,
124 AUTOMATION_DEFAULTS.issueTriageMode
125 ),
126 autoMergeMode: normalizeMode(row.autoMergeMode, AUTOMATION_DEFAULTS.autoMergeMode),
127 ciAutofixMode: normalizeMode(row.ciAutofixMode, AUTOMATION_DEFAULTS.ciAutofixMode),
64aa989Claude128 autoRepairMode: normalizeMode(row.autoRepairMode, AUTOMATION_DEFAULTS.autoRepairMode),
f183fdfClaude129 autoIssuesMode: normalizeMode(row.autoIssuesMode, AUTOMATION_DEFAULTS.autoIssuesMode),
130 docDriftMode: normalizeMode(row.docDriftMode, AUTOMATION_DEFAULTS.docDriftMode),
479dcd9Claude131 };
132}
133
134// ---------------------------------------------------------------------------
135// DB-backed loader + upsert
136// ---------------------------------------------------------------------------
137
138/**
139 * Load the automation settings for a repository. No row → defaults.
140 * FAILS OPEN: any DB error also returns the defaults (logged once at warn)
141 * so a broken settings lookup never alters current platform behavior.
142 */
143export async function getAutomationSettings(
144 repositoryId: string
145): Promise<AutomationSettings> {
146 try {
147 const [row] = await db
148 .select()
149 .from(repoAutomationSettings)
150 .where(eq(repoAutomationSettings.repositoryId, repositoryId))
151 .limit(1);
152 return settingsFromRow(row ?? null);
153 } catch (err) {
154 console.warn(
155 "[automation-settings] lookup failed (falling back to defaults):",
156 err instanceof Error ? err.message : err
157 );
158 return { ...AUTOMATION_DEFAULTS };
159 }
160}
161
162/**
163 * Create or update the settings row for a repository. Partial patches are
164 * merged over the current effective settings so a form that only posts one
165 * field can't reset the others. Throws on DB failure — callers (the
166 * settings route) surface the error to the user.
167 */
168export async function upsertAutomationSettings(
169 repositoryId: string,
170 patch: Partial<AutomationSettings>
171): Promise<AutomationSettings> {
172 const current = await getAutomationSettings(repositoryId);
173 const next: AutomationSettings = {
174 aiReviewMode: normalizeMode(patch.aiReviewMode, current.aiReviewMode),
175 prTriageMode: normalizeMode(patch.prTriageMode, current.prTriageMode),
176 issueTriageMode: normalizeMode(patch.issueTriageMode, current.issueTriageMode),
177 autoMergeMode: normalizeMode(patch.autoMergeMode, current.autoMergeMode),
178 ciAutofixMode: normalizeMode(patch.ciAutofixMode, current.ciAutofixMode),
64aa989Claude179 autoRepairMode: normalizeMode(patch.autoRepairMode, current.autoRepairMode),
f183fdfClaude180 autoIssuesMode: normalizeMode(patch.autoIssuesMode, current.autoIssuesMode),
181 docDriftMode: normalizeMode(patch.docDriftMode, current.docDriftMode),
479dcd9Claude182 };
183
184 await db
185 .insert(repoAutomationSettings)
186 .values({ repositoryId, ...next })
187 .onConflictDoUpdate({
188 target: repoAutomationSettings.repositoryId,
189 set: { ...next, updatedAt: new Date() },
190 });
191
192 return next;
193}