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

Bulk supabase edge function update (#2002)

Context: - https://github.com/orgs/supabase/discussions/33720 - https://supabase.com/docs/reference/api/v1-deploy-a-function <!-- CURSOR_SUMMARY --> > [!NOTE] > Introduces a two-step deploy flow for edge functions: parallel bundling followed by bulk activation, plus API shape updates. > > - Adds `DeployedFunctionResponse` and updates `deploySupabaseFunction` to support `bundleOnly`, return the deployed function payload, and use `import_map_path` > - Implements `bulkUpdateFunctions` (PUT `/projects/{id}/functions`) to activate multiple functions at once > - In `deployAllSupabaseFunctions`, filters to functions with `index.ts`, bundles them in parallel (`bundleOnly=true`), aggregates successes/errors, then bulk-activates successful bundles > - Cleans up import map generation (`imports: {}`) and switches deploy URL construction to include `bundleOnly` query param > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7b1d63ee73a56dc24b3465d812838bc5bf5bd0e5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
上级 bf44acd7
...@@ -42,6 +42,21 @@ interface FunctionFilesResult { ...@@ -42,6 +42,21 @@ interface FunctionFilesResult {
cacheKey: string; cacheKey: string;
} }
export interface DeployedFunctionResponse {
id: string;
slug: string;
name: string;
status: "ACTIVE" | "REMOVED" | "THROTTLED";
version: number;
created_at?: number;
updated_at?: number;
verify_jwt?: boolean;
import_map?: boolean;
entrypoint_path?: string;
import_map_path?: string;
ezbr_sha256?: string;
}
// Caches for shared files to avoid re-reading unchanged files // Caches for shared files to avoid re-reading unchanged files
const sharedFilesCache = new Map<string, CachedSharedFiles>(); const sharedFilesCache = new Map<string, CachedSharedFiles>();
...@@ -325,11 +340,13 @@ export async function deploySupabaseFunction({ ...@@ -325,11 +340,13 @@ export async function deploySupabaseFunction({
supabaseProjectId, supabaseProjectId,
functionName, functionName,
appPath, appPath,
bundleOnly = false,
}: { }: {
supabaseProjectId: string; supabaseProjectId: string;
functionName: string; functionName: string;
appPath: string; appPath: string;
}): Promise<void> { bundleOnly?: boolean;
}): Promise<DeployedFunctionResponse> {
logger.info( logger.info(
`Deploying Supabase function: ${functionName} to project: ${supabaseProjectId}`, `Deploying Supabase function: ${functionName} to project: ${supabaseProjectId}`,
); );
...@@ -359,11 +376,7 @@ export async function deploySupabaseFunction({ ...@@ -359,11 +376,7 @@ export async function deploySupabaseFunction({
const importMapRelPath = path.posix.join(entryDir, "import_map.json"); const importMapRelPath = path.posix.join(entryDir, "import_map.json");
const importMapObject = { const importMapObject = {
imports: { imports: {},
// This resolves "_shared/" imports to the _shared directory
// From {functionName}/index.ts, ../_shared/ goes up to root then into _shared/
"_shared/": "../_shared/",
},
}; };
// Add the import map file into the upload list // Add the import map file into the upload list
...@@ -382,7 +395,7 @@ export async function deploySupabaseFunction({ ...@@ -382,7 +395,7 @@ export async function deploySupabaseFunction({
entrypoint_path: entrypointPath, entrypoint_path: entrypointPath,
name: functionName, name: functionName,
verify_jwt: false, verify_jwt: false,
import_map: importMapRelPath, import_map_path: importMapRelPath,
}; };
formData.append("metadata", JSON.stringify(metadata)); formData.append("metadata", JSON.stringify(metadata));
...@@ -396,28 +409,63 @@ export async function deploySupabaseFunction({ ...@@ -396,28 +409,63 @@ export async function deploySupabaseFunction({
} }
// 6) Perform the deploy request // 6) Perform the deploy request
const deployUrl = `https://api.supabase.com/v1/projects/${encodeURIComponent(
supabaseProjectId,
)}/functions/deploy?slug=${encodeURIComponent(functionName)}${bundleOnly ? "&bundleOnly=true" : ""}`;
const response = await fetch(deployUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${(supabase as any).options.accessToken}`,
},
body: formData,
});
if (response.status !== 201) {
throw await createResponseError(response, "create function");
}
const result: DeployedFunctionResponse = await response.json();
logger.info(
`Deployed Supabase function: ${functionName} to project: ${supabaseProjectId}${bundleOnly ? " (bundle only)" : ""}`,
);
return result;
}
export async function bulkUpdateFunctions({
supabaseProjectId,
functions,
}: {
supabaseProjectId: string;
functions: DeployedFunctionResponse[];
}): Promise<void> {
logger.info(
`Bulk updating ${functions.length} functions for project: ${supabaseProjectId}`,
);
const supabase = await getSupabaseClient();
const response = await fetch( const response = await fetch(
`https://api.supabase.com/v1/projects/${encodeURIComponent( `https://api.supabase.com/v1/projects/${encodeURIComponent(supabaseProjectId)}/functions`,
supabaseProjectId,
)}/functions/deploy?slug=${encodeURIComponent(functionName)}`,
{ {
method: "POST", method: "PUT",
headers: { headers: {
Authorization: `Bearer ${(supabase as any).options.accessToken}`, Authorization: `Bearer ${(supabase as any).options.accessToken}`,
"Content-Type": "application/json",
}, },
body: formData, body: JSON.stringify(functions),
}, },
); );
if (response.status !== 201) { if (response.status !== 200) {
throw await createResponseError(response, "create function"); throw await createResponseError(response, "bulk update functions");
} }
logger.info( logger.info(
`Deployed Supabase function: ${functionName} to project: ${supabaseProjectId}`, `Successfully bulk updated ${functions.length} functions for project: ${supabaseProjectId}`,
); );
await response.json();
} }
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
......
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import log from "electron-log"; import log from "electron-log";
import { deploySupabaseFunction } from "./supabase_management_client"; import {
bulkUpdateFunctions,
deploySupabaseFunction,
type DeployedFunctionResponse,
} from "./supabase_management_client";
const logger = log.scope("supabase_utils"); const logger = log.scope("supabase_utils");
...@@ -105,34 +109,75 @@ export async function deployAllSupabaseFunctions({ ...@@ -105,34 +109,75 @@ export async function deployAllSupabaseFunctions({
`Found ${functionDirs.length} functions to deploy in ${functionsDir}`, `Found ${functionDirs.length} functions to deploy in ${functionsDir}`,
); );
// Deploy each function // Filter to only functions with index.ts
const validFunctions: string[] = [];
for (const functionDir of functionDirs) { for (const functionDir of functionDirs) {
const functionName = functionDir.name; const functionName = functionDir.name;
const functionPath = path.join(functionsDir, functionName); const functionPath = path.join(functionsDir, functionName);
const indexPath = path.join(functionPath, "index.ts"); const indexPath = path.join(functionPath, "index.ts");
// Check if index.ts exists
try { try {
await fs.access(indexPath); await fs.access(indexPath);
validFunctions.push(functionName);
} catch { } catch {
logger.warn( logger.warn(
`Skipping ${functionName}: index.ts not found at ${indexPath}`, `Skipping ${functionName}: index.ts not found at ${indexPath}`,
); );
continue;
} }
}
try { if (validFunctions.length === 0) {
logger.info(`Deploying function: ${functionName}`); logger.info("No valid functions to deploy");
return [];
}
// Deploy all functions in parallel with bundleOnly=true
logger.info(`Bundling ${validFunctions.length} functions in parallel...`);
await deploySupabaseFunction({ const deployResults = await Promise.allSettled(
validFunctions.map(async (functionName) => {
logger.info(`Bundling function: ${functionName}`);
const result = await deploySupabaseFunction({
supabaseProjectId, supabaseProjectId,
functionName, functionName,
appPath, appPath,
bundleOnly: true,
}); });
logger.info(`Successfully bundled function: ${functionName}`);
return result;
}),
);
logger.info(`Successfully deployed function: ${functionName}`); // Collect successful results and errors
const successfulDeploys: DeployedFunctionResponse[] = [];
for (let i = 0; i < deployResults.length; i++) {
const result = deployResults[i];
const functionName = validFunctions[i];
if (result.status === "fulfilled") {
successfulDeploys.push(result.value);
} else {
const errorMessage = `Failed to bundle ${functionName}: ${result.reason?.message || result.reason}`;
logger.error(errorMessage, result.reason);
errors.push(errorMessage);
}
}
// Bulk update all successfully bundled functions to activate them
if (successfulDeploys.length > 0) {
logger.info(
`Activating ${successfulDeploys.length} functions via bulk update...`,
);
try {
await bulkUpdateFunctions({
supabaseProjectId,
functions: successfulDeploys,
});
logger.info(
`Successfully activated ${successfulDeploys.length} functions`,
);
} catch (error: any) { } catch (error: any) {
const errorMessage = `Failed to deploy ${functionName}: ${error.message}`; const errorMessage = `Failed to bulk update functions: ${error.message}`;
logger.error(errorMessage, error); logger.error(errorMessage, error);
errors.push(errorMessage); errors.push(errorMessage);
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论