Unverified 提交 4b2f0b5a authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add skipPruneEdgeFunctions setting to control edge function pruning (#2228)

Add a new setting "Keep extra Supabase edge functions" that controls whether dangling edge functions (deployed to Supabase but not in codebase) are automatically deleted during sync operations. When disabled (default), edge functions are pruned during batch deployments triggered by: - Shared module changes - Version reverts - Local agent file operations Changes: - Add skipPruneEdgeFunctions to UserSettings schema - Add listSupabaseFunctions API method to management client - Modify deployAllSupabaseFunctions to prune dangling functions - Add UI toggle in SupabaseIntegration component - Update all call sites to pass the setting <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a user setting to control pruning of Supabase edge functions and integrates pruning into batch deployments. > > - Adds `skipPruneEdgeFunctions` to `UserSettings` and a toggle in `SupabaseIntegration` > - Extends management client with `listSupabaseFunctions` > - Updates `deployAllSupabaseFunctions` to optionally prune deployed functions not in the codebase, using `deleteSupabaseFunction`; controlled by `skipPruneEdgeFunctions` > - Threads the setting through all batch deploy call sites: shared module edits (`app_handlers`), version reverts (`version_handlers`), chat/agent file ops (`response_processor`, local agent `file_operations`) > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6af28fb682e7a6352426e0033d67ee1e750201fc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a setting to control pruning of extra Supabase edge functions during batch deployments. By default, dangling functions (deployed but not in code) are pruned; enable “Keep extra Supabase edge functions” to skip pruning. - **New Features** - Added skipPruneEdgeFunctions to UserSettings and a toggle in SupabaseIntegration. - Implemented listSupabaseFunctions API and pruning in deployAllSupabaseFunctions. - Passed the setting through all batch deploy paths (shared module changes, version reverts, local agent file operations). <sup>Written for commit 6af28fb682e7a6352426e0033d67ee1e750201fc. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2228"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarClaude <noreply@anthropic.com>
上级 0b2d34a4
......@@ -64,6 +64,17 @@ export function SupabaseIntegration() {
}
};
const handleSkipPruneSettingChange = async (enabled: boolean) => {
try {
await updateSettings({
skipPruneEdgeFunctions: enabled,
});
showSuccess("Setting updated");
} catch (err: any) {
showError(err.message || "Failed to update setting");
}
};
if (!isConnected) {
return null;
}
......@@ -146,6 +157,29 @@ export function SupabaseIntegration() {
</div>
</div>
</div>
<div className="mt-4">
<div className="flex items-center space-x-3">
<Switch
id="skip-prune-edge-functions"
checked={!!settings?.skipPruneEdgeFunctions}
onCheckedChange={handleSkipPruneSettingChange}
/>
<div className="space-y-1">
<Label
htmlFor="skip-prune-edge-functions"
className="text-sm font-medium"
>
Keep extra Supabase edge functions
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">
When disabled, edge functions deployed to Supabase but not present
in your codebase will be automatically deleted during sync
operations (e.g., after reverting or modifying shared modules).
</p>
</div>
</div>
</div>
</div>
);
}
......@@ -1248,10 +1248,12 @@ export function registerAppHandlers() {
logger.info(
`Shared module ${filePath} modified, redeploying all Supabase functions`,
);
const settings = readSettings();
const deployErrors = await deployAllSupabaseFunctions({
appPath,
supabaseProjectId: app.supabaseProjectId,
supabaseOrganizationSlug: app.supabaseOrganizationSlug ?? null,
skipPruneEdgeFunctions: settings.skipPruneEdgeFunctions ?? false,
});
if (deployErrors.length > 0) {
return {
......
......@@ -11,6 +11,7 @@ import { createTypedHandler } from "./base";
import { versionContracts } from "../types/version";
import { deployAllSupabaseFunctions } from "../../supabase_admin/supabase_utils";
import { readSettings } from "../../main/settings";
import {
gitCheckout,
gitCommit,
......@@ -333,10 +334,12 @@ export function registerVersionHandlers() {
logger.info(
`Re-deploying all Supabase edge functions for app ${appId} after revert`,
);
const settings = readSettings();
const deployErrors = await deployAllSupabaseFunctions({
appPath,
supabaseProjectId: app.supabaseProjectId,
supabaseOrganizationSlug: app.supabaseOrganizationSlug ?? null,
skipPruneEdgeFunctions: settings.skipPruneEdgeFunctions ?? false,
});
if (deployErrors.length > 0) {
......
......@@ -487,11 +487,13 @@ export async function processFullResponseActions(
logger.info(
"Shared modules changed, redeploying all Supabase functions",
);
const settings = readSettings();
const deployErrors = await deployAllSupabaseFunctions({
appPath,
supabaseProjectId: chatWithApp.app.supabaseProjectId,
supabaseOrganizationSlug:
chatWithApp.app.supabaseOrganizationSlug ?? null,
skipPruneEdgeFunctions: settings.skipPruneEdgeFunctions ?? false,
});
if (deployErrors.length > 0) {
for (const err of deployErrors) {
......
......@@ -293,6 +293,7 @@ export const UserSettingsSchema = z
selectedTemplateId: z.string(),
selectedThemeId: z.string().optional(),
enableSupabaseWriteSqlMigration: z.boolean().optional(),
skipPruneEdgeFunctions: z.boolean().optional(),
selectedChatMode: ChatModeSchema.optional(),
defaultChatMode: ChatModeSchema.optional(),
acceptedCommunityCode: z.boolean().optional(),
......
......@@ -9,6 +9,7 @@ import {
getGitUncommittedFiles,
} from "@/ipc/utils/git_utils";
import { deployAllSupabaseFunctions } from "../../../../../../supabase_admin/supabase_utils";
import { readSettings } from "../../../../../../main/settings";
import type { AgentContext } from "../tools/types";
const logger = log.scope("file_operations");
......@@ -37,10 +38,12 @@ export async function deployAllFunctionsIfNeeded(
try {
logger.info("Shared modules changed, redeploying all Supabase functions");
const settings = readSettings();
const deployErrors = await deployAllSupabaseFunctions({
appPath: ctx.appPath,
supabaseProjectId: ctx.supabaseProjectId,
supabaseOrganizationSlug: ctx.supabaseOrganizationSlug ?? null,
skipPruneEdgeFunctions: settings.skipPruneEdgeFunctions ?? false,
});
if (deployErrors.length > 0) {
......
......@@ -627,6 +627,42 @@ export async function deleteSupabaseFunction({
);
}
export async function listSupabaseFunctions({
supabaseProjectId,
organizationSlug,
}: {
supabaseProjectId: string;
organizationSlug: string | null;
}): Promise<DeployedFunctionResponse[]> {
if (IS_TEST_BUILD) {
return [];
}
logger.info(`Listing Supabase functions for project: ${supabaseProjectId}`);
const supabase = await getSupabaseClient({ organizationSlug });
const response = await fetchWithRetry(
`https://api.supabase.com/v1/projects/${supabaseProjectId}/functions`,
{
method: "GET",
headers: {
Authorization: `Bearer ${(supabase as any).options.accessToken}`,
},
},
`List Supabase functions for ${supabaseProjectId}`,
);
if (response.status !== 200) {
throw await createResponseError(response, "list functions");
}
const functions: DeployedFunctionResponse[] = await response.json();
logger.info(
`Found ${functions.length} functions for project: ${supabaseProjectId}`,
);
return functions;
}
export async function listSupabaseBranches({
supabaseProjectId,
organizationSlug,
......
......@@ -3,7 +3,9 @@ import path from "node:path";
import log from "electron-log";
import {
bulkUpdateFunctions,
deleteSupabaseFunction,
deploySupabaseFunction,
listSupabaseFunctions,
type DeployedFunctionResponse,
} from "./supabase_management_client";
......@@ -76,16 +78,20 @@ export function extractFunctionNameFromPath(filePath: string): string {
* Deploys all Supabase edge functions found in the app's supabase/functions directory
* @param appPath - The absolute path to the app directory
* @param supabaseProjectId - The Supabase project ID
* @param supabaseOrganizationSlug - The Supabase organization slug
* @param skipPruneEdgeFunctions - If false, delete any deployed edge functions that are not in the codebase
* @returns An array of error messages for functions that failed to deploy (empty if all succeeded)
*/
export async function deployAllSupabaseFunctions({
appPath,
supabaseProjectId,
supabaseOrganizationSlug,
skipPruneEdgeFunctions,
}: {
appPath: string;
supabaseProjectId: string;
supabaseOrganizationSlug: string | null;
skipPruneEdgeFunctions: boolean;
}): Promise<string[]> {
const functionsDir = path.join(appPath, "supabase", "functions");
......@@ -186,6 +192,49 @@ export async function deployAllSupabaseFunctions({
errors.push(errorMessage);
}
}
// Prune dangling edge functions (deployed but not in codebase)
if (!skipPruneEdgeFunctions) {
try {
logger.info("Checking for dangling edge functions to prune...");
const deployedFunctions = await listSupabaseFunctions({
supabaseProjectId,
organizationSlug: supabaseOrganizationSlug,
});
const localFunctionNames = new Set(validFunctions);
const danglingFunctions = deployedFunctions.filter(
(fn) => !localFunctionNames.has(fn.slug),
);
if (danglingFunctions.length > 0) {
logger.info(
`Found ${danglingFunctions.length} dangling edge functions to prune: ${danglingFunctions.map((fn) => fn.slug).join(", ")}`,
);
for (const fn of danglingFunctions) {
try {
await deleteSupabaseFunction({
supabaseProjectId,
functionName: fn.slug,
organizationSlug: supabaseOrganizationSlug,
});
logger.info(`Pruned dangling edge function: ${fn.slug}`);
} catch (deleteError: any) {
const errorMessage = `Failed to prune edge function ${fn.slug}: ${deleteError.message}`;
logger.error(errorMessage, deleteError);
errors.push(errorMessage);
}
}
} else {
logger.info("No dangling edge functions found");
}
} catch (pruneError: any) {
const errorMessage = `Failed to check for dangling edge functions: ${pruneError.message}`;
logger.error(errorMessage, pruneError);
errors.push(errorMessage);
}
}
} catch (error: any) {
const errorMessage = `Error reading functions directory: ${error.message}`;
logger.error(errorMessage, error);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论