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.
| 479dcd9 | 1 | /** |
| 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') | |
| 64aa989 | 25 | * autoMergeMode 'off' | 'suggest' | 'auto' (default 'off') |
| 479dcd9 | 26 | * ciAutofixMode 'off' | 'suggest' | 'auto' (default 'suggest') |
| f183fdf | 27 | * autoRepairMode 'off' | 'suggest' (default 'off') |
| 28 | * autoIssuesMode 'off' | 'suggest' (default 'off') | |
| 29 | * docDriftMode 'off' | 'suggest' (default 'off') | |
| 479dcd9 | 30 | * |
| 31 | * Where only on/off is meaningful, 'auto' is treated the same as 'suggest' | |
| 32 | * (i.e. "on") — `isAutomationOn` encodes that rule. | |
| 33 | */ | |
| 34 | ||
| 35 | import { eq } from "drizzle-orm"; | |
| 36 | import { db } from "../db"; | |
| 37 | import { repoAutomationSettings } from "../db/schema"; | |
| 38 | ||
| 39 | // --------------------------------------------------------------------------- | |
| 40 | // Types + defaults | |
| 41 | // --------------------------------------------------------------------------- | |
| 42 | ||
| 43 | export type AutomationMode = "off" | "suggest" | "auto"; | |
| 44 | ||
| 45 | export interface AutomationSettings { | |
| 46 | aiReviewMode: AutomationMode; | |
| 47 | prTriageMode: AutomationMode; | |
| 48 | issueTriageMode: AutomationMode; | |
| 49 | autoMergeMode: AutomationMode; | |
| 50 | ciAutofixMode: AutomationMode; | |
| 64aa989 | 51 | autoRepairMode: AutomationMode; |
| f183fdf | 52 | autoIssuesMode: AutomationMode; |
| 53 | docDriftMode: AutomationMode; | |
| 479dcd9 | 54 | } |
| 55 | ||
| 56 | /** Loader signature — the DI seam dispatch sites accept for tests. */ | |
| 57 | export 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 | */ | |
| 68 | export const AUTOMATION_DEFAULTS: Readonly<AutomationSettings> = Object.freeze({ | |
| 69 | aiReviewMode: "suggest", | |
| 70 | prTriageMode: "suggest", | |
| 71 | issueTriageMode: "suggest", | |
| 64aa989 | 72 | autoMergeMode: "off", |
| 479dcd9 | 73 | ciAutofixMode: "suggest", |
| f183fdf | 74 | autoRepairMode: "off", |
| 75 | autoIssuesMode: "off", | |
| 76 | docDriftMode: "off", | |
| 479dcd9 | 77 | }); |
| 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. */ | |
| 84 | export 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 | */ | |
| 98 | export 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. */ | |
| 107 | export 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 | */ | |
| 115 | export 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), | |
| 64aa989 | 128 | autoRepairMode: normalizeMode(row.autoRepairMode, AUTOMATION_DEFAULTS.autoRepairMode), |
| f183fdf | 129 | autoIssuesMode: normalizeMode(row.autoIssuesMode, AUTOMATION_DEFAULTS.autoIssuesMode), |
| 130 | docDriftMode: normalizeMode(row.docDriftMode, AUTOMATION_DEFAULTS.docDriftMode), | |
| 479dcd9 | 131 | }; |
| 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 | */ | |
| 143 | export 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 | */ | |
| 168 | export 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), | |
| 64aa989 | 179 | autoRepairMode: normalizeMode(patch.autoRepairMode, current.autoRepairMode), |
| f183fdf | 180 | autoIssuesMode: normalizeMode(patch.autoIssuesMode, current.autoIssuesMode), |
| 181 | docDriftMode: normalizeMode(patch.docDriftMode, current.docDriftMode), | |
| 479dcd9 | 182 | }; |
| 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 | } |