mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## New Features - Initial work for showing configuration drift in Studio - This only works for Github-connected projects and it'll show a banner if the project state differs from the git-tracked `config.toml` - Currently behind a feature-flag `ConfigDrift`, enabled on local and staging. - There might be drift shown without changing any setting, this is work-in-progress. <img width="1217" height="1195" alt="Screenshot 2026-08-19 at 23 12 10" src="https://github.com/user-attachments/assets/fb0b18d8-1a93-4595-85cc-e8b8a3462847" /> ## How to test 1. Connect a project to a Github repo 2. Resync the branch on `/dashboard/project/_/branches`. This will trigger deployment of the `config.toml` on your project 3. Change some settings (I recommend `dashboard/project/_/auth/providers` 4. A banner should appear on all project pages with a link ## Tests - Added coverage for configuration conversion, normalization, matching, drift detection, and unmanaged settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
import {
|
|
CONFIG_SECTIONS,
|
|
getConfigValue,
|
|
getFieldDefinition,
|
|
getSectionFieldEntries,
|
|
isSecretConfigField,
|
|
type ConfigSection,
|
|
} from './github-config-field-registry'
|
|
import { gitHubConfigTomlSchema, type GitHubConfigToml } from './github-config.types'
|
|
|
|
// This file should be temporary and will be removed once we publish a "@supabase/config" package.
|
|
|
|
type GitHubConfigFieldStatus = 'unmanaged' | 'managed' | 'drifted'
|
|
|
|
interface GitHubConfigFieldState {
|
|
status: GitHubConfigFieldStatus
|
|
configPath?: string
|
|
settingHref?: (projectRef: string) => string
|
|
githubValue?: unknown
|
|
}
|
|
|
|
export interface GitHubConfigDriftField {
|
|
section: ConfigSection
|
|
configPath: string
|
|
settingHref: (projectRef: string) => string
|
|
dashboardValue: unknown
|
|
githubValue: unknown
|
|
}
|
|
|
|
export interface UnmanagedConfigField {
|
|
section: ConfigSection
|
|
configPath: string
|
|
dashboardValue: unknown
|
|
}
|
|
|
|
interface GitHubConfigDriftSummary {
|
|
managedCount: number
|
|
driftedFields: GitHubConfigDriftField[]
|
|
unmanagedFields: UnmanagedConfigField[]
|
|
}
|
|
|
|
function getConfigFieldState({
|
|
configPath,
|
|
dashboardConfig,
|
|
githubConfig,
|
|
}: {
|
|
configPath: string
|
|
dashboardConfig: GitHubConfigToml
|
|
githubConfig?: GitHubConfigToml
|
|
}): GitHubConfigFieldState {
|
|
const definition = getFieldDefinition(configPath)
|
|
if (!definition || !githubConfig || isSecretConfigField(definition.configPath)) {
|
|
return { status: 'unmanaged' }
|
|
}
|
|
|
|
const normalizedDashboardValue = getConfigValue(dashboardConfig, definition.configPath)
|
|
|
|
let githubValue = getConfigValue(githubConfig, definition.configPath)
|
|
if (githubValue === undefined) {
|
|
const isCodeOwned = getConfigValue(githubConfig, 'config_source') === 'code'
|
|
if (!isCodeOwned || definition.hostedDefault === undefined) return { status: 'unmanaged' }
|
|
|
|
const normalizedDefaultValue =
|
|
definition.normalizeGithubValue?.(definition.hostedDefault) ?? definition.hostedDefault
|
|
if (valuesMatch(normalizedDashboardValue, normalizedDefaultValue)) {
|
|
return { status: 'unmanaged' }
|
|
}
|
|
|
|
githubValue = definition.hostedDefault
|
|
}
|
|
|
|
const normalizedGithubValue = definition.normalizeGithubValue?.(githubValue) ?? githubValue
|
|
return {
|
|
status: valuesMatch(normalizedDashboardValue, normalizedGithubValue) ? 'managed' : 'drifted',
|
|
configPath: definition.configPath,
|
|
settingHref: definition.settingHref,
|
|
githubValue,
|
|
}
|
|
}
|
|
|
|
export function getConfigDriftSummary({
|
|
dashboardConfig,
|
|
githubConfig,
|
|
}: {
|
|
dashboardConfig?: GitHubConfigToml
|
|
githubConfig?: GitHubConfigToml
|
|
}): GitHubConfigDriftSummary {
|
|
if (!dashboardConfig || !githubConfig) {
|
|
return { managedCount: 0, driftedFields: [], unmanagedFields: [] }
|
|
}
|
|
|
|
const githubConfigResult = gitHubConfigTomlSchema.safeParse(githubConfig)
|
|
const dashboardConfigResult = gitHubConfigTomlSchema.safeParse(dashboardConfig)
|
|
if (!githubConfigResult.success || !dashboardConfigResult.success) {
|
|
return { managedCount: 0, driftedFields: [], unmanagedFields: [] }
|
|
}
|
|
const cleanedGithubConfig = githubConfigResult.data
|
|
const cleanedDashboardConfig = dashboardConfigResult.data
|
|
|
|
let managedCount = 0
|
|
const driftedFields: GitHubConfigDriftField[] = []
|
|
const unmanagedFields: UnmanagedConfigField[] = []
|
|
|
|
for (const section of CONFIG_SECTIONS) {
|
|
const sectionConfig = cleanedDashboardConfig[section]
|
|
if (!sectionConfig) continue
|
|
|
|
for (const { configPath, rawValue } of getSectionFieldEntries(section, sectionConfig)) {
|
|
if (isSecretConfigField(configPath)) continue
|
|
|
|
const state = getConfigFieldState({
|
|
configPath,
|
|
dashboardConfig: cleanedDashboardConfig,
|
|
githubConfig: cleanedGithubConfig,
|
|
})
|
|
|
|
if (state.status === 'managed') {
|
|
managedCount += 1
|
|
continue
|
|
}
|
|
if (state.status === 'unmanaged' || !state.configPath || !state.settingHref) {
|
|
unmanagedFields.push({ section, configPath, dashboardValue: rawValue })
|
|
continue
|
|
}
|
|
|
|
driftedFields.push({
|
|
section,
|
|
configPath: state.configPath,
|
|
settingHref: state.settingHref,
|
|
dashboardValue: rawValue,
|
|
githubValue: state.githubValue,
|
|
})
|
|
}
|
|
}
|
|
|
|
return { managedCount, driftedFields, unmanagedFields }
|
|
}
|
|
|
|
function valuesMatch(left: unknown, right: unknown): boolean {
|
|
return JSON.stringify(left) === JSON.stringify(right)
|
|
}
|