Skip to content

Commit 2b72ad5

Browse files
committed
Merge branch 'main' of github.com:firebase/firebase-tools into local_builds_ignore_files
2 parents 4589304 + 7ce2bd5 commit 2b72ad5

12 files changed

Lines changed: 232 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +0,0 @@
1-
- Updated Pub/Sub emulator to version 0.8.31
2-
- Resolves undefined regions earlier, during the build to backend resolution phase (#10471)

npm-shrinkwrap.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firebase-tools",
3-
"version": "15.17.0",
3+
"version": "15.18.0",
44
"description": "Command-Line Interface for Firebase",
55
"main": "./lib/index.js",
66
"mcpName": "io.github.firebase/firebase-mcp",

src/deploy/functions/backend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export function memoryToGen2Cpu(memory: MemoryOptions): number {
256256

257257
export const DEFAULT_CONCURRENCY = 80;
258258
export const DEFAULT_MEMORY: MemoryOptions = 256;
259+
export const DEFAULT_TIMEOUT_SECONDS = 60;
259260
export const MIN_CPU_FOR_CONCURRENCY = 1;
260261
export const SCHEDULED_FUNCTION_LABEL = Object.freeze({ deployment: "firebase-schedule" });
261262

src/deploy/functions/prepare.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,18 @@ describe("prepare", () => {
593593
expect(want.availableMemoryMb).to.equal(512);
594594
});
595595

596+
it("fills in timeout from last deploy", () => {
597+
const want: backend.Endpoint = {
598+
...ENDPOINT_BASE,
599+
httpsTrigger: {},
600+
};
601+
const have: backend.Endpoint = JSON.parse(JSON.stringify(want));
602+
have.timeoutSeconds = 120;
603+
604+
prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false);
605+
expect(want.timeoutSeconds).to.equal(120);
606+
});
607+
596608
it("downgrades concurrency if necessary (explicit)", () => {
597609
const have: backend.Endpoint = {
598610
...ENDPOINT_BASE,
@@ -645,6 +657,28 @@ describe("prepare", () => {
645657
prepare.resolveCpuAndConcurrency(backend.of(want));
646658
expect(want.concurrency).to.equal(1);
647659
});
660+
661+
it("defaults timeout to 60 for run platform functions", () => {
662+
const want: backend.Endpoint = {
663+
...ENDPOINT_BASE,
664+
platform: "run",
665+
httpsTrigger: {},
666+
};
667+
668+
prepare.resolveDefaultTimeout(backend.of(want));
669+
expect(want.timeoutSeconds).to.equal(60);
670+
});
671+
672+
it("does not default timeout for gcfv2 platform functions", () => {
673+
const want: backend.Endpoint = {
674+
...ENDPOINT_BASE,
675+
platform: "gcfv2",
676+
httpsTrigger: {},
677+
};
678+
679+
prepare.resolveDefaultTimeout(backend.of(want));
680+
expect(want.timeoutSeconds).to.be.undefined;
681+
});
648682
});
649683

650684
describe("inferBlockingDetails", () => {

src/deploy/functions/prepare.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ export async function prepare(
302302
inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase));
303303
await ensureTriggerRegions(wantBackend);
304304
resolveCpuAndConcurrency(wantBackend);
305+
resolveDefaultTimeout(wantBackend);
305306
validate.endpointsAreValid(wantBackend);
306307
inferBlockingDetails(wantBackend);
307308
}
@@ -552,6 +553,10 @@ export function inferDetailsFromExisting(
552553
wantE.cpu = haveE.cpu;
553554
}
554555

556+
if (typeof wantE.timeoutSeconds === "undefined" && haveE.timeoutSeconds) {
557+
wantE.timeoutSeconds = haveE.timeoutSeconds;
558+
}
559+
555560
// N.B. concurrency has different defaults based on CPU. If the customer
556561
// only specifies CPU and they change that specification to < 1, we should
557562
// turn off concurrency.
@@ -650,6 +655,18 @@ export function resolveCpuAndConcurrency(want: backend.Backend): void {
650655
}
651656
}
652657

658+
/**
659+
* Assigns the default timeout to a function if it is deployed to Cloud Run
660+
* and no timeout was specified.
661+
*/
662+
export function resolveDefaultTimeout(want: backend.Backend): void {
663+
for (const e of backend.allEndpoints(want)) {
664+
if (e.platform === "run" && e.timeoutSeconds === undefined) {
665+
e.timeoutSeconds = backend.DEFAULT_TIMEOUT_SECONDS;
666+
}
667+
}
668+
}
669+
653670
/**
654671
* Exported for use by an internal command (internaltesting:functions:discover) only.
655672
* @internal
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { Delegate } from "./index";
4+
import * as discovery from "../discovery";
5+
import * as build from "../../build";
6+
import * as supported from "../supported";
7+
8+
describe("Dart Runtime Delegate", () => {
9+
describe("discoverBuild", () => {
10+
let detectFromYamlStub: sinon.SinonStub;
11+
12+
beforeEach(() => {
13+
detectFromYamlStub = sinon.stub(discovery, "detectFromYaml");
14+
});
15+
16+
afterEach(() => {
17+
sinon.restore();
18+
});
19+
20+
it("should not set default timeout", async () => {
21+
const delegate = new Delegate("project", "sourceDir", supported.latest("dart"));
22+
23+
const mockBuild: build.Build = {
24+
endpoints: {
25+
func1: {
26+
platform: "gcfv2",
27+
entryPoint: "func1",
28+
project: "project",
29+
runtime: supported.latest("dart"),
30+
httpsTrigger: {},
31+
},
32+
},
33+
params: [],
34+
requiredAPIs: [],
35+
};
36+
37+
detectFromYamlStub.resolves(mockBuild);
38+
39+
const result = await delegate.discoverBuild({}, {});
40+
41+
expect(result.endpoints.func1.timeoutSeconds).to.be.undefined;
42+
expect(result.endpoints.func1.platform).to.equal("run");
43+
});
44+
45+
it("should preserve user-defined timeout", async () => {
46+
const delegate = new Delegate("project", "sourceDir", supported.latest("dart"));
47+
48+
const mockBuild: build.Build = {
49+
endpoints: {
50+
func1: {
51+
platform: "gcfv2",
52+
entryPoint: "func1",
53+
project: "project",
54+
runtime: supported.latest("dart"),
55+
httpsTrigger: {},
56+
timeoutSeconds: 120,
57+
},
58+
},
59+
params: [],
60+
requiredAPIs: [],
61+
};
62+
63+
detectFromYamlStub.resolves(mockBuild);
64+
65+
const result = await delegate.discoverBuild({}, {});
66+
67+
expect(result.endpoints.func1.timeoutSeconds).to.equal(120);
68+
expect(result.endpoints.func1.platform).to.equal("run");
69+
});
70+
71+
it("should not apply default timeout in emulator mode", async () => {
72+
const delegate = new Delegate("project", "sourceDir", supported.latest("dart"));
73+
74+
const mockBuild: build.Build = {
75+
endpoints: {
76+
func1: {
77+
platform: "gcfv2",
78+
entryPoint: "func1",
79+
project: "project",
80+
runtime: supported.latest("dart"),
81+
httpsTrigger: {},
82+
},
83+
},
84+
params: [],
85+
requiredAPIs: [],
86+
};
87+
88+
detectFromYamlStub.resolves(mockBuild);
89+
90+
const result = await delegate.discoverBuild({}, { FUNCTIONS_EMULATOR: "true" });
91+
92+
expect(result.endpoints.func1.timeoutSeconds).to.be.undefined;
93+
// Platform should not be converted to "run" in emulator mode either
94+
expect(result.endpoints.func1.platform).to.equal("gcfv2");
95+
});
96+
});
97+
});

src/deploy/functions/services/ailogic.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ describe("AILogicService", () => {
3030
sinon.restore();
3131
});
3232

33+
describe("requiredProjectBindings", () => {
34+
it("should bind the AI logic service account with the Cloud Run invoker role", async () => {
35+
const bindings = await service.requiredProjectBindings("123456789");
36+
expect(bindings).to.deep.equal([
37+
{
38+
role: "roles/run.invoker",
39+
members: [
40+
"serviceAccount:service-123456789@gcp-sa-firebasevertexai.iam.gserviceaccount.com",
41+
],
42+
},
43+
]);
44+
});
45+
});
46+
3347
describe("validateTrigger", () => {
3448
it("should throw if two regional triggers of same type in same region", () => {
3549
const ep1: backend.Endpoint = {

src/deploy/functions/services/ailogic.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as backend from "../backend";
22
import { FirebaseError, getErrStatus } from "../../../error";
33
import { Name, Service } from "./index";
44
import * as ailogicApi from "../../../gcp/ailogic";
5+
import * as iam from "../../../gcp/iam";
56
import {
67
AI_LOGIC_BEFORE_GENERATE_CONTENT,
78
AI_LOGIC_AFTER_GENERATE_CONTENT,
@@ -51,6 +52,22 @@ export class AILogicService implements Service {
5152
ensureTriggerRegion: (ep: backend.Endpoint & backend.EventTriggered) => Promise<void> = () =>
5253
Promise.resolve();
5354

55+
/**
56+
* The AI logic proxy server uses a service account to invoke functions.
57+
* Setting requiredProjectBindings here causes the ensureServiceAgentRoles
58+
* call during prepare phase to upsert the corresponding IAM binding.
59+
*/
60+
async requiredProjectBindings(projectNumber: string): Promise<Array<iam.Binding>> {
61+
return [
62+
{
63+
role: "roles/run.invoker",
64+
members: [
65+
`serviceAccount:service-${projectNumber}@gcp-sa-firebasevertexai.iam.gserviceaccount.com`,
66+
],
67+
},
68+
];
69+
}
70+
5471
/**
5572
* Validate that there are no duplicate AI Logic triggers of the same type.
5673
* Regional triggers are grouped by region; Global triggers are checked globally.

src/deploy/functions/validate.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,29 @@ describe("validate", () => {
692692
expect(backend.allEndpoints(b)[0].secretEnvironmentVariables![0].version).to.equal("2");
693693
}
694694
});
695+
696+
it("passes validation for Cloud Run (platform=run) functions with secrets", async () => {
697+
secretVersionStub.withArgs(project, secret.name, "latest").resolves({
698+
secret,
699+
versionId: "1",
700+
state: "ENABLED",
701+
});
702+
703+
const b = backend.of({
704+
...ENDPOINT,
705+
platform: "run",
706+
secretEnvironmentVariables: [
707+
{
708+
projectId: project,
709+
secret: "MY_SECRET",
710+
key: "MY_SECRET",
711+
},
712+
],
713+
});
714+
715+
await validate.secretsAreValid(project, b);
716+
expect(backend.allEndpoints(b)[0].secretEnvironmentVariables![0].version).to.equal("1");
717+
});
695718
});
696719

697720
describe("validateTimeoutConfig", () => {

0 commit comments

Comments
 (0)