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