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 {
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
const sharedFilesCache = new Map<string, CachedSharedFiles>();
......@@ -325,11 +340,13 @@ export async function deploySupabaseFunction({
supabaseProjectId,
functionName,
appPath,
bundleOnly = false,
}: {
supabaseProjectId: string;
functionName: string;
appPath: string;
}): Promise<void> {
bundleOnly?: boolean;
}): Promise<DeployedFunctionResponse> {
logger.info(
`Deploying Supabase function: ${functionName} to project: ${supabaseProjectId}`,
);
......@@ -359,11 +376,7 @@ export async function deploySupabaseFunction({
const importMapRelPath = path.posix.join(entryDir, "import_map.json");
const importMapObject = {
imports: {
// This resolves "_shared/" imports to the _shared directory
// From {functionName}/index.ts, ../_shared/ goes up to root then into _shared/
"_shared/": "../_shared/",
},
imports: {},
};
// Add the import map file into the upload list
......@@ -382,7 +395,7 @@ export async function deploySupabaseFunction({
entrypoint_path: entrypointPath,
name: functionName,
verify_jwt: false,
import_map: importMapRelPath,
import_map_path: importMapRelPath,
};
formData.append("metadata", JSON.stringify(metadata));
......@@ -396,28 +409,63 @@ export async function deploySupabaseFunction({
}
// 6) Perform the deploy request
const response = await fetch(
`https://api.supabase.com/v1/projects/${encodeURIComponent(
const deployUrl = `https://api.supabase.com/v1/projects/${encodeURIComponent(
supabaseProjectId,
)}/functions/deploy?slug=${encodeURIComponent(functionName)}`,
{
)}/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}`,
`Deployed Supabase function: ${functionName} to project: ${supabaseProjectId}${bundleOnly ? " (bundle only)" : ""}`,
);
await response.json();
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(
`https://api.supabase.com/v1/projects/${encodeURIComponent(supabaseProjectId)}/functions`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${(supabase as any).options.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(functions),
},
);
if (response.status !== 200) {
throw await createResponseError(response, "bulk update functions");
}
logger.info(
`Successfully bulk updated ${functions.length} functions for project: ${supabaseProjectId}`,
);
}
// ─────────────────────────────────────────────────────────────────────
......
import fs from "node:fs/promises";
import path from "node:path";
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");
......@@ -105,34 +109,75 @@ export async function deployAllSupabaseFunctions({
`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) {
const functionName = functionDir.name;
const functionPath = path.join(functionsDir, functionName);
const indexPath = path.join(functionPath, "index.ts");
// Check if index.ts exists
try {
await fs.access(indexPath);
validFunctions.push(functionName);
} catch {
logger.warn(
`Skipping ${functionName}: index.ts not found at ${indexPath}`,
);
continue;
}
}
try {
logger.info(`Deploying function: ${functionName}`);
if (validFunctions.length === 0) {
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,
functionName,
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) {
const errorMessage = `Failed to deploy ${functionName}: ${error.message}`;
const errorMessage = `Failed to bulk update functions: ${error.message}`;
logger.error(errorMessage, error);
errors.push(errorMessage);
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论