Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
292c48b
initial cloud run no build deploy implementation mvp
brittanycho Dec 19, 2025
57855da
manual testing files - will remove later
brittanycho Dec 19, 2025
b4f9633
fixes formatting issues
brittanycho Dec 20, 2025
f24830e
Fix runv2.spec.ts concurrency expectation
brittanycho Jan 6, 2026
6670e13
removing manual test files
brittanycho Jan 6, 2026
dc1978e
Merge branch 'master' into zip-deploy-4
brittanycho Jan 7, 2026
487ad04
Fix lint errors
brittanycho Jan 7, 2026
15a7b26
Update json schema for dart3
brittanycho Jan 7, 2026
23c6d27
Fix lint errors and remove verify script
brittanycho Jan 7, 2026
366fe34
Fix TS errors in fabricator.ts for vscode build
brittanycho Jan 7, 2026
e66d9de
Fixes minor formatting issues
brittanycho Jan 7, 2026
1975dec
Fixed linting
brittanycho Jan 7, 2026
6b53bf9
Merge branch 'master' into zip-deploy-4
brittanycho Jan 9, 2026
f385cc3
addresses feedback
brittanycho Jan 12, 2026
e98b149
Merge branch 'master' into zip-deploy-4
brittanycho Jan 12, 2026
55ee19f
remove runtime config for v2 and run
brittanycho Jan 13, 2026
92124c2
Merge branch 'master' into zip-deploy-4
brittanycho Jan 14, 2026
2e3ceec
adds relevant comments
brittanycho Jan 15, 2026
f767878
Merge branch 'master' into zip-deploy-4
brittanycho Jan 21, 2026
cc924c4
Merge branch 'master' into zip-deploy-4
brittanycho Jan 24, 2026
7a32e38
add additional fields and other minor fixes
brittanycho Jan 27, 2026
0d985f2
Merge branch 'master' into zip-deploy-4
brittanycho Jan 27, 2026
9a63a38
corrects test
brittanycho Jan 27, 2026
13fb338
fixes error
brittanycho Jan 27, 2026
4714005
fix prettier
brittanycho Jan 27, 2026
d6d22cb
Merge branch 'master' into zip-deploy-4
brittanycho Jan 27, 2026
220df7d
Merge branch 'master' into zip-deploy-4
brittanycho Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions schema/firebase-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"definitions": {
"ActiveRuntime": {
"enum": [
"dart3",
"nodejs18",
"nodejs20",
"nodejs22",
Expand Down Expand Up @@ -913,6 +914,7 @@
},
"runtime": {
"enum": [
"dart3",
"nodejs18",
"nodejs20",
"nodejs22",
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/functions/backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe("Backend", () => {
},
},
],
containerConcurrency: 80,
maxInstanceRequestConcurrency: 80,
},
generation: 1,
createTime: "2023-01-01T00:00:00Z",
Expand Down
5 changes: 5 additions & 0 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,11 @@ export type Endpoint = TargetIds &

// State of the endpoint.
state?: EndpointState;

// Fields for Cloud Run platform (for no-build path)
baseImageUri?: string;
command?: string[];
args?: string[];
};

export interface RequiredAPI {
Expand Down
17 changes: 12 additions & 5 deletions src/deploy/functions/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,8 @@ export interface SecretEnvVar {
export type MemoryOption = 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768;
const allMemoryOptions: MemoryOption[] = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];

// Run is an automatic migration from gcfv2 and is not used on the wire.
export type FunctionsPlatform = Exclude<backend.FunctionsPlatform, "run">;
export const AllFunctionsPlatforms: FunctionsPlatform[] = ["gcfv1", "gcfv2"];
export type FunctionsPlatform = backend.FunctionsPlatform;
export const AllFunctionsPlatforms: FunctionsPlatform[] = ["gcfv1", "gcfv2", "run"];
export type VpcEgressSetting = backend.VpcEgressSettings;
export const AllVpcEgressSettings: VpcEgressSetting[] = ["PRIVATE_RANGES_ONLY", "ALL_TRAFFIC"];
export type IngressSetting = backend.IngressSettings;
Expand All @@ -241,8 +240,8 @@ export type Endpoint = Triggered & {
// Defaults to false. If true, the function will be ignored during the deploy process.
omit?: Field<boolean>;

// Defaults to "gcfv2". "Run" will be an additional option defined later
platform?: "gcfv1" | "gcfv2";
// Defaults to "gcfv2".
platform?: "gcfv1" | "gcfv2" | "run";

// Necessary for the GCF API to determine what code to load with the Functions Framework.
// Will become optional once "run" is supported as a platform
Expand Down Expand Up @@ -287,6 +286,11 @@ export type Endpoint = Triggered & {
environmentVariables?: Record<string, string | Expression<string>> | null;
secretEnvironmentVariables?: SecretEnvVar[] | null;
labels?: Record<string, string | Expression<string>> | null;

// Fields for Cloud Run platform (for no-build path)
baseImageUri?: string;
command?: string[];
args?: string[];
};

type SecretParam = ReturnType<typeof defineSecret>;
Expand Down Expand Up @@ -503,6 +507,9 @@ export function toBackend(
"environmentVariables",
"labels",
"secretEnvironmentVariables",
"baseImageUri",
"command",
"args",
);
r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");

Expand Down
6 changes: 3 additions & 3 deletions src/deploy/functions/checkIam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export async function checkServiceAccountIam(projectId: string): Promise<void> {
/**
* Checks a functions deployment for HTTP function creation, and tests IAM
* permissions accordingly.
*
* @param context The deploy context.
* @param options The command-wide options object.
* @param payload The deploy payload.
Expand All @@ -77,8 +76,9 @@ export async function checkHttpIam(
const filters = context.filters || getEndpointFilters(options, context.config!);
const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend);
const httpEndpoints = [...flattenArray(wantBackends.map((b) => backend.allEndpoints(b)))]
.filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered)
.filter((f) => endpointMatchesAnyFilter(f, filters));
.filter((f) => backend.isHttpsTriggered(f) || backend.isDataConnectGraphqlTriggered(f))
.filter((f) => endpointMatchesAnyFilter(f, filters))
.filter((f) => f.platform !== "run");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why exclude run?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iiuc GCF and Run have different permissions (cloudfunctions.functions.setIamPolicy vs. run.services.setIamPolicy) and the specific check where it is excluded here is for GCF permission

GCF: https://docs.cloud.google.com/functions/docs/reference/iam/permissions#functions
Run: https://docs.cloud.google.com/run/docs/reference/iam/permissions#services

For current no build mvp implementation, I think the current approach is implemented so that we try to set the policy in setInvoker (within fabricator.ts) and if it fails iiuc there should be an IAM error from Cloud Run

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need similar check too, but we can leave that as todo for next PR.


const existing = await backend.existingBackend(context);
const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
Expand Down
13 changes: 11 additions & 2 deletions src/deploy/functions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as experiments from "../../experiments";
import { findEndpoint } from "./backend";
import { deploy as extDeploy } from "../extensions";
import { getProjectNumber } from "../../getProjectNumber";
import * as path from "path";

setGracefulCleanup();

Expand Down Expand Up @@ -51,10 +52,16 @@ async function uploadSourceV1(
}

// Trampoline to allow tests to mock out createStream.
/**
*
*/
export function createReadStream(filePath: string): NodeJS.ReadableStream {
return fs.createReadStream(filePath);
}

/**
*
*/
export async function uploadSourceV2(
projectId: string,
projectNumber: string,
Expand All @@ -80,7 +87,9 @@ export async function uploadSourceV2(
};

// Legacy behavior: use the GCF API
if (!experiments.isEnabled("runfunctions")) {
// We use BYO bucket if the "runfunctions" experiment is enabled OR if we have any platform: run endpoints.
// Otherwise, we use the GCF API.
if (!experiments.isEnabled("runfunctions") && !v2Endpoints.some((e) => e.platform === "run")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like we ahve 2 experiemtns being used here - suggest aligning on 1?

if (process.env.GOOGLE_CLOUD_QUOTA_PROJECT) {
logLabeledWarning(
"functions",
Expand Down Expand Up @@ -116,7 +125,7 @@ export async function uploadSourceV2(
},
},
});
const objectPath = `${source.functionsSourceV2Hash}.zip`;
const objectPath = `${source.functionsSourceV2Hash}${path.extname(source.functionsSourceV2!)}`;
await gcs.upload(
uploadOpts,
`${bucketName}/${objectPath}`,
Expand Down
8 changes: 7 additions & 1 deletion src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export async function prepare(
);
}

if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2" || e.platform === "run")) {
const schPathSet = new Set<string>();
for (const e of backend.allEndpoints(wantBackend)) {
if (
Expand All @@ -233,11 +233,17 @@ export async function prepare(
schPathSet.add(e.dataConnectGraphqlTrigger.schemaFilePath);
}
}
const configForUpload = shouldUseRuntimeConfig(localCfg) ? runtimeConfig : undefined;
const exportType = backend.someEndpoint(wantBackend, (e) => e.platform === "run")
? "tar.gz"
: "zip";
const packagedSource = await prepareFunctionsUpload(
options.config.projectDir,
sourceDir,
localCfg,
[...schPathSet],
configForUpload,
{ exportType },
);
source.functionsSourceV2 = packagedSource?.pathToSource;
source.functionsSourceV2Hash = packagedSource?.hash;
Expand Down
10 changes: 7 additions & 3 deletions src/deploy/functions/prepareFunctionsUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,16 @@ async function packageSource(
config: projectConfig.ValidatedSingle,
additionalSources: string[],
runtimeConfig: any,
options?: { exportType: "zip" | "tar.gz" },
): Promise<PackagedSourceInfo | undefined> {
const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix: ".zip" }).name;
const exportType = options?.exportType || "zip";
const postfix = exportType === "tar.gz" ? ".tar.gz" : ".zip";
const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix }).name;
const fileStream = fs.createWriteStream(tmpFile, {
flags: "w",
encoding: "binary",
});
const archive = archiver("zip");
const archive = exportType === "tar.gz" ? archiver("tar", { gzip: true }) : archiver("zip");
const hashes: string[] = [];

// We must ignore firebase-debug.log or weird things happen if
Expand Down Expand Up @@ -154,8 +157,9 @@ export async function prepareFunctionsUpload(
config: projectConfig.ValidatedSingle,
additionalSources: string[],
runtimeConfig?: backend.RuntimeConfigValues,
options?: { exportType: "zip" | "tar.gz" },
): Promise<PackagedSourceInfo | undefined> {
return packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig);
return packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig, options);
}

export function convertToSortedKeyValueArray(config: any): SortedConfig {
Expand Down
106 changes: 106 additions & 0 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as pollerNS from "../../../operation-poller";
import * as pubsubNS from "../../../gcp/pubsub";
import * as schedulerNS from "../../../gcp/cloudscheduler";
import * as runNS from "../../../gcp/run";
import * as runV2NS from "../../../gcp/runv2";
import * as cloudtasksNS from "../../../gcp/cloudtasks";
import * as backend from "../backend";
import * as scraper from "./sourceTokenScraper";
Expand All @@ -32,6 +33,7 @@ describe("Fabricator", () => {
let pubsub: sinon.SinonStubbedInstance<typeof pubsubNS>;
let scheduler: sinon.SinonStubbedInstance<typeof schedulerNS>;
let run: sinon.SinonStubbedInstance<typeof runNS>;
let runv2: sinon.SinonStubbedInstance<typeof runV2NS>;
let tasks: sinon.SinonStubbedInstance<typeof cloudtasksNS>;
let services: sinon.SinonStubbedInstance<typeof servicesNS>;
let identityPlatform: sinon.SinonStubbedInstance<typeof identityPlatformNS>;
Expand All @@ -44,6 +46,7 @@ describe("Fabricator", () => {
pubsub = sinon.stub(pubsubNS);
scheduler = sinon.stub(schedulerNS);
run = sinon.stub(runNS);
runv2 = sinon.stub(runV2NS);
tasks = sinon.stub(cloudtasksNS);
services = sinon.stub(servicesNS);
identityPlatform = sinon.stub(identityPlatformNS);
Expand Down Expand Up @@ -74,6 +77,10 @@ describe("Fabricator", () => {
run.setInvokerUpdate.rejects(new Error("unexpected run.setInvokerUpdate"));
run.replaceService.rejects(new Error("unexpected run.replaceService"));
run.updateService.rejects(new Error("Unexpected run.updateService"));
runv2.createService.rejects(new Error("unexpected runv2.createService"));
runv2.updateService.rejects(new Error("unexpected runv2.updateService"));
runv2.deleteService.rejects(new Error("unexpected runv2.deleteService"));
runv2.getService.rejects(new Error("unexpected runv2.getService"));
poller.pollOperation.rejects(new Error("unexpected poller.pollOperation"));
pubsub.createTopic.rejects(new Error("unexpected pubsub.createTopic"));
pubsub.deleteTopic.rejects(new Error("unexpected pubsub.deleteTopic"));
Expand Down Expand Up @@ -1704,4 +1711,103 @@ describe("Fabricator", () => {
expect(ep2Result?.error?.message).to.match(/delete function/);
});
});

describe("createRunService", () => {
it("creates a Cloud Run service with correct configuration", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);
run.setInvokerUpdate.resolves();

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
baseImageUri: "gcr.io/base",
command: ["cmd"],
args: ["arg"],
},
);
await fab.createRunService(ep);

expect(runv2.createService).to.have.been.calledWith(
ep.project,
ep.region,
ep.id,
sinon.match({
template: {
containers: [
sinon.match({
image: "scratch",
baseImageUri: "gcr.io/base",
command: ["cmd"],
args: ["arg"],
sourceCode: {
cloudStorageSource: {
bucket: "bucket",
object: "object",
generation: "42",
},
},
}),
],
},
}),
);
});
});

describe("updateRunService", () => {
it("updates a Cloud Run service with correct configuration", async () => {
runv2.updateService.resolves({ uri: "https://service", name: "service" } as any);
run.setInvokerUpdate.resolves();

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
baseImageUri: "gcr.io/base-v2",
},
);
// Mock update to include the endpoint
const update: planner.EndpointUpdate = {
endpoint: ep,
};

await fab.updateRunService(update);

expect(runv2.updateService).to.have.been.calledWith(
sinon.match({
name: `projects/${ep.project}/locations/${ep.region}/services/${ep.id}`,
template: {
containers: [
sinon.match({
baseImageUri: "gcr.io/base-v2",
}),
],
},
}),
);
});
});

describe("deleteRunService", () => {
it("deletes the Cloud Run service", async () => {
runv2.deleteService.resolves();
const ep = endpoint({ httpsTrigger: {} }, { platform: "run" });

await fab.deleteRunService(ep);

expect(runv2.deleteService).to.have.been.calledWith(ep.project, ep.region, ep.id);
});

it("ignores 404s", async () => {
const err = new Error("Not Found");
(err as any).status = 404;
runv2.deleteService.rejects(err);
const ep = endpoint({ httpsTrigger: {} }, { platform: "run" });

await fab.deleteRunService(ep);

expect(runv2.deleteService).to.have.been.called;
});
});
});
Loading
Loading