diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index b748d57ec5..047b63c77e 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -236,11 +236,15 @@ jobs: # Gate the auto-sync on its own output: the regenerated types are only # trustworthy if they still compile against the backend that consumes # them. Prod is often behind local migrations, so a regenerated types - # file can drop columns the code still references. Fail the job here - # instead of pushing an unverified commit that reddens `main` for the - # next contributor. + # file can drop columns the code still references. Skip the push in + # that case instead of landing an unverified commit that reddens + # `main` — and exit 0 so migration-touching releases are not blocked. echo "Schema/types drift detected; verifying regenerated types compile against the codebase." - bun typecheck + if ! bun typecheck; then + echo "Regenerated schema/types do not compile (prod likely behind local migrations). Skipping auto-sync push." >&2 + git checkout -- supabase/schemas/prod.sql src/types/supabase.types.ts supabase/functions/_backend/utils/supabase.types.ts + exit 0 + fi git config --local user.name "github-actions[bot]" git config --local user.email "github-actions[bot]@users.noreply.github.com" diff --git a/supabase/functions/_backend/triggers/logsnag_insights.ts b/supabase/functions/_backend/triggers/logsnag_insights.ts index 30e05db40e..c39f009a33 100644 --- a/supabase/functions/_backend/triggers/logsnag_insights.ts +++ b/supabase/functions/_backend/triggers/logsnag_insights.ts @@ -327,8 +327,15 @@ type RequiredGlobalStatsShard = typeof REQUIRED_GLOBAL_STATS_SHARDS[number] type GlobalStatsNotificationStepAction = 'send' | 'complete_claimed' | 'skip' type GlobalStatsUpdate = Database['public']['Tables']['global_stats']['Update'] type GlobalStatsRow = Database['public']['Tables']['global_stats']['Row'] -type GlobalStatsSnapshotPatch = GlobalStatsUpdate & { orgs?: number } -type GlobalStatsSnapshotRow = GlobalStatsRow & { orgs?: number | null } +type GlobalStatsSnapshotPatch = GlobalStatsUpdate & { + orgs?: number + // Present in repo migrations before prod deploy; keep writable while generated types lag. + apps_with_preview?: number +} +type GlobalStatsSnapshotRow = GlobalStatsRow & { + orgs?: number | null + apps_with_preview?: number | null +} interface LogsnagInsightsPayload { retry_count?: unknown @@ -790,14 +797,24 @@ function shouldRefreshMutablePastDueStats( return !hasPersistedPastDueStats(snapshot) } -function isMissingBuildMetricColumnError(error: unknown): boolean { +function isMissingSchemaColumnError(error: unknown, columnHints: string[]): boolean { const errorCode = String((error as any)?.code ?? '').toUpperCase() const message = String((error as any)?.message ?? '').toLowerCase() return errorCode === 'PGRST204' || errorCode === '42703' - || message.includes('build_total_seconds_day') - || message.includes('build_avg_seconds_day') - || message.includes('build_count_day') + || columnHints.some(hint => message.includes(hint.toLowerCase())) +} + +function isMissingBuildMetricColumnError(error: unknown): boolean { + return isMissingSchemaColumnError(error, [ + 'build_total_seconds_day', + 'build_avg_seconds_day', + 'build_count_day', + ]) +} + +function isMissingAppsWithPreviewColumnError(error: unknown): boolean { + return isMissingSchemaColumnError(error, ['apps_with_preview']) } async function calculateRevenue(c: Context, referenceDate?: Date): Promise { @@ -1592,14 +1609,35 @@ async function ensureGlobalStatsSnapshotRows(c: Context, dateIds: readonly strin async function updateGlobalStatsSnapshot(c: Context, dateId: string, patch: GlobalStatsSnapshotPatch): Promise { await ensureGlobalStatsSnapshotRow(c, dateId) - const { orgs, ...globalStatsPatch } = patch + const { orgs, apps_with_preview, ...globalStatsPatch } = patch + const updatePayload = { + ...globalStatsPatch, + ...(apps_with_preview === undefined ? {} : { apps_with_preview }), + } as GlobalStatsUpdate const { error } = await supabaseAdmin(c) .from('global_stats') - .update(globalStatsPatch as GlobalStatsUpdate) + .update(updatePayload) .eq('date_id', dateId) - if (error) - throw error + if (error) { + if (apps_with_preview !== undefined && isMissingAppsWithPreviewColumnError(error)) { + cloudlog({ + requestId: c.get('requestId'), + message: 'global_stats.apps_with_preview missing; retrying snapshot update without it', + dateId, + error, + }) + const { error: legacyError } = await supabaseAdmin(c) + .from('global_stats') + .update(globalStatsPatch as GlobalStatsUpdate) + .eq('date_id', dateId) + if (legacyError) + throw legacyError + } + else { + throw error + } + } if (orgs !== undefined) await updateGlobalStatsSnapshotOrgCount(c, dateId, orgs) diff --git a/tests/logsnag-insights-revenue.unit.test.ts b/tests/logsnag-insights-revenue.unit.test.ts index 03ad2a945a..25330b9212 100644 --- a/tests/logsnag-insights-revenue.unit.test.ts +++ b/tests/logsnag-insights-revenue.unit.test.ts @@ -691,6 +691,9 @@ describe('logsnag revenue metric helpers', () => { expect(countFn).toContain('snapshotEnd') expect(coreShard).toContain('countAppsWithPreview(c, window.prevDayEnd)') expect(coreShard).toContain('apps_with_preview,') + // Keep writable while prod types lag the migration (auto-sync gate). + expect(source).toContain('apps_with_preview?: number') + expect(source).toContain('isMissingAppsWithPreviewColumnError') }) it.concurrent('normalizes logsnag insights retry payload counts', () => { expect(logsnagInsightsTestUtils.normalizeLogsnagInsightsRetryCount('2')).toBe(2)