Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Add JSON format support for Cloud Functions secrets with `--format json` flag and auto-detection from file extensions (#1745)
- `firebase dataconnect:sdk:generate` will run `init dataconnect:sdk` automatically if no SDKs are configured (#9325).
- Tighten --only filter resolution for functions deployment to prefer codebase names (#9353)
- Add `disallowLegacyRuntimeConfig` option to `firebase.json` to optionally skip fetching legacy Runtime Config during function deploys (#9354)
6 changes: 6 additions & 0 deletions schema/firebase-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,9 @@
"configDir": {
"type": "string"
},
"disallowLegacyRuntimeConfig": {
"type": "boolean"
},
"ignore": {
"items": {
"type": "string"
Expand Down Expand Up @@ -979,6 +982,9 @@
"configDir": {
"type": "string"
},
"disallowLegacyRuntimeConfig": {
"type": "boolean"
},
"ignore": {
"items": {
"type": "string"
Expand Down
6 changes: 2 additions & 4 deletions src/commands/internaltesting-functions-discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { Command } from "../command";
import { Options } from "../options";
import { logger } from "../logger";
import { loadCodebases } from "../deploy/functions/prepare";
import { normalizeAndValidate } from "../functions/projectConfig";
import { normalizeAndValidate, shouldUseRuntimeConfig } from "../functions/projectConfig";
import { getProjectAdminSdkConfigOrCached } from "../emulator/adminSdkConfig";
import { needProjectId } from "../projectUtils";
import { FirebaseError } from "../error";
import * as ensureApiEnabled from "../ensureApiEnabled";
import { runtimeconfigOrigin } from "../api";
import * as experiments from "../experiments";
import { getFunctionsConfig } from "../deploy/functions/prepareFunctionsUpload";

export const command = new Command("internaltesting:functions:discover")
Expand All @@ -24,9 +23,8 @@ export const command = new Command("internaltesting:functions:discover")
}

let runtimeConfig: Record<string, unknown> = { firebase: firebaseConfig };
const allowFunctionsConfig = experiments.isEnabled("dangerouslyAllowFunctionsConfig");

if (allowFunctionsConfig) {
if (fnConfig.some(shouldUseRuntimeConfig)) {
try {
const runtimeConfigApiEnabled = await ensureApiEnabled.check(
projectId,
Expand Down
52 changes: 52 additions & 0 deletions src/deploy/functions/prepare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,58 @@

expect(builds.codebase.runtime).to.equal("nodejs20");
});

it("should pass only firebase config when disallowLegacyRuntimeConfig is true", async () => {
const config: ValidatedConfig = [
{
source: "source",
codebase: "codebase",
disallowLegacyRuntimeConfig: true,
runtime: "nodejs22",
},
];
const options = {
config: {
path: (p: string) => p,
},
projectId: "project",
} as unknown as Options;
const firebaseConfig = { projectId: "project" };
const runtimeConfig = { firebase: firebaseConfig, customKey: "customValue" };

await prepare.loadCodebases(config, options, firebaseConfig, runtimeConfig);

expect(discoverBuildStub.calledOnce).to.be.true;
const callArgs = discoverBuildStub.firstCall.args;
expect(callArgs[0]).to.deep.equal({ firebase: firebaseConfig });
expect(callArgs[0]).to.not.have.property("customKey");
});

it("should pass full runtime config when disallowLegacyRuntimeConfig is false", async () => {
const config: ValidatedConfig = [
{
source: "source",
codebase: "codebase",
disallowLegacyRuntimeConfig: false,
runtime: "nodejs22",
},
];
const options = {
config: {
path: (p: string) => p,
},
projectId: "project",
} as unknown as Options;
const firebaseConfig = { projectId: "project" };
const runtimeConfig = { firebase: firebaseConfig, customKey: "customValue" };

await prepare.loadCodebases(config, options, firebaseConfig, runtimeConfig);

expect(discoverBuildStub.calledOnce).to.be.true;
const callArgs = discoverBuildStub.firstCall.args;
expect(callArgs[0]).to.deep.equal(runtimeConfig);
expect(callArgs[0]).to.have.property("customKey", "customValue");
});
});

describe("inferDetailsFromExisting", () => {
Expand Down Expand Up @@ -433,7 +485,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.empty(),
backend.of(nonGenkitEndpoint),
{} as any,

Check warning on line 488 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 488 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand All @@ -442,7 +494,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.empty(),
backend.of(genkitEndpointWithSecrets),
{} as any,

Check warning on line 497 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 497 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand All @@ -451,7 +503,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.of(genkitEndpointWithoutSecrets),
backend.of(genkitEndpointWithoutSecrets),
{} as any,

Check warning on line 506 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 506 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand All @@ -460,7 +512,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.empty(),
backend.of(genkitEndpointWithoutSecrets),
{ force: true } as any,

Check warning on line 515 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 515 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand All @@ -471,7 +523,7 @@
prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.empty(),
backend.of(genkitEndpointWithoutSecrets),
{ nonInteractive: true } as any,

Check warning on line 526 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 526 in src/deploy/functions/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
),
).to.be.rejectedWith(FirebaseError);
expect(confirm).to.have.been.calledWithMatch({ nonInteractive: true });
Expand Down
19 changes: 13 additions & 6 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import * as runtimes from "./runtimes";
import * as supported from "./runtimes/supported";
import * as validate from "./validate";
import * as ensure from "./ensure";
import * as experiments from "../../experiments";
import {
functionsOrigin,
artifactRegistryDomain,
Expand Down Expand Up @@ -43,6 +42,7 @@ import {
normalizeAndValidate,
ValidatedConfig,
requireLocal,
shouldUseRuntimeConfig,
} from "../../functions/projectConfig";
import { AUTH_BLOCKING_EVENTS } from "../../functions/events/v1";
import { generateServiceIdentity } from "../../gcp/serviceusage";
Expand Down Expand Up @@ -94,10 +94,11 @@ export async function prepare(

// ===Phase 1. Load codebases from source with optional runtime config.
let runtimeConfig: Record<string, unknown> = { firebase: firebaseConfig };
const allowFunctionsConfig = experiments.isEnabled("dangerouslyAllowFunctionsConfig");

// Load runtime config if experiment allows it and API is enabled
if (allowFunctionsConfig && checkAPIsEnabled[1]) {
const targetedCodebaseConfigs = context.config!.filter((cfg) => codebases.includes(cfg.codebase));

// Load runtime config if API is enabled and at least one targeted codebase uses it
if (checkAPIsEnabled[1] && targetedCodebaseConfigs.some(shouldUseRuntimeConfig)) {
runtimeConfig = { ...runtimeConfig, ...(await getFunctionsConfig(projectId)) };
}

Expand Down Expand Up @@ -228,7 +229,8 @@ export async function prepare(
source.functionsSourceV2Hash = packagedSource?.hash;
}
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) {
const packagedSource = await prepareFunctionsUpload(sourceDir, localCfg, runtimeConfig);
const configForUpload = shouldUseRuntimeConfig(localCfg) ? runtimeConfig : undefined;
const packagedSource = await prepareFunctionsUpload(sourceDir, localCfg, configForUpload);
source.functionsSourceV1 = packagedSource?.pathToSource;
source.functionsSourceV1Hash = packagedSource?.hash;
}
Expand Down Expand Up @@ -486,7 +488,12 @@ export async function loadCodebases(
"functions",
`Loading and analyzing source code for codebase ${codebase} to determine what to deploy`,
);
const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, {

const codebaseRuntimeConfig = shouldUseRuntimeConfig(codebaseConfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

worth logging here? maybe if true, log a warning that runtime config is going away? tempting to log if false, but that would probably get too noisy and might confuse.

Copy link
Contributor

@jhuleatt jhuleatt Oct 21, 2025

Choose a reason for hiding this comment

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

Will functions.config() just silently fail if someone on v6 has runtime config turned off in their firebase.json but tries to use it in their functions code? the env var the firebase-functions SDK reads for functions.config will just be undefined, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes it would be equivalent to functions.config() returning an empty object

? runtimeConfig
: { firebase: firebaseConfig };

const discoveredBuild = await runtimeDelegate.discoverBuild(codebaseRuntimeConfig, {
...firebaseEnvs,
// Quota project is required when using GCP's Client-based APIs
// Some GCP client SDKs, like Vertex AI, requires appropriate quota project setup
Expand Down
8 changes: 0 additions & 8 deletions src/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ export const ALL_EXPERIMENTS = experiments({
"of how that image was created.",
public: true,
},
dangerouslyAllowFunctionsConfig: {
shortDescription: "Allows the use of deprecated functions.config() API",
fullDescription:
"The functions.config() API is deprecated and will be removed on December 31, 2025. " +
"This experiment allows continued use of the API during the migration period.",
default: true,
public: true,
},
runfunctions: {
shortDescription:
"Functions created using the V2 API target Cloud Run Functions (not production ready)",
Expand Down
4 changes: 4 additions & 0 deletions src/firebaseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ type FunctionConfigBase = {
// Must start with a lowercase letter; may contain lowercase letters, numbers, and dashes;
// cannot start or end with a dash; maximum length 30 characters.
prefix?: string;
// Optional: When true, prevents the Firebase CLI from fetching and including legacy
// Runtime Config values for this codebase during deployment. This has no effect on
// remote sources, which never use runtime config. Defaults to false for backward compatibility.
disallowLegacyRuntimeConfig?: boolean;
} & Deployable;

export type LocalFunctionConfig = FunctionConfigBase & {
Expand Down
45 changes: 45 additions & 0 deletions src/functions/projectConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,49 @@ describe("projectConfig", () => {
expect(projectConfig.resolveConfigDir(cfg)).to.be.undefined;
});
});

describe("shouldUseRuntimeConfig", () => {
const testCases = [
{
description:
"returns true for local codebase without disallowLegacyRuntimeConfig (default)",
config: { source: "functions" },
expected: true,
},
{
description: "returns true for local codebase with disallowLegacyRuntimeConfig=false",
config: { source: "functions", disallowLegacyRuntimeConfig: false },
expected: true,
},
{
description: "returns false for local codebase with disallowLegacyRuntimeConfig=true",
config: { source: "functions", disallowLegacyRuntimeConfig: true },
expected: false,
},
{
description: "returns false for remote source",
config: {
remoteSource: { repository: "repo", ref: "main" },
runtime: "nodejs20",
},
expected: false,
},
{
description: "returns false for remote source even with disallowLegacyRuntimeConfig=false",
config: {
remoteSource: { repository: "repo", ref: "main" },
runtime: "nodejs20",
disallowLegacyRuntimeConfig: false,
},
expected: false,
},
];

for (const tc of testCases) {
it(tc.description, () => {
const config = projectConfig.validate([tc.config as any])[0];
expect(projectConfig.shouldUseRuntimeConfig(config)).to.equal(tc.expected);
});
}
});
});
13 changes: 13 additions & 0 deletions src/functions/projectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,16 @@ export function requireLocal(c: ValidatedSingle, purpose?: string): ValidatedLoc
export function resolveConfigDir(c: ValidatedSingle): string | undefined {
return c.configDir || c.source;
}

/**
* Determines if a codebase should use runtime config.
*
* Only local sources that haven't opted out via disallowLegacyRuntimeConfig use runtime config.
* Remote sources never use runtime config.
*
* @param cfg The codebase configuration to check
* @returns true if this codebase should use runtime config, false otherwise
*/
export function shouldUseRuntimeConfig(cfg: ValidatedSingle): boolean {
return isLocalConfig(cfg) && cfg.disallowLegacyRuntimeConfig !== true;
}
3 changes: 3 additions & 0 deletions src/init/features/functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe("functions", () => {
codebase: TEST_CODEBASE_DEFAULT,
ignore: ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"],
predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'],
disallowLegacyRuntimeConfig: true,
});
expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([
`${TEST_SOURCE_DEFAULT}/package.json`,
Expand Down Expand Up @@ -114,6 +115,7 @@ describe("functions", () => {
'npm --prefix "$RESOURCE_DIR" run lint',
'npm --prefix "$RESOURCE_DIR" run build',
],
disallowLegacyRuntimeConfig: true,
});
expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([
`${TEST_SOURCE_DEFAULT}/package.json`,
Expand Down Expand Up @@ -168,6 +170,7 @@ describe("functions", () => {
"*.local",
],
predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'],
disallowLegacyRuntimeConfig: true,
},
]);
expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([
Expand Down
2 changes: 2 additions & 0 deletions src/init/features/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ async function initNewCodebase(setup: any, config: Config): Promise<any> {
setup.config.functions.push({
source,
codebase,
// Disable legacy runtime config for new codebases by default
disallowLegacyRuntimeConfig: true,
});
setup.functions.source = source;
setup.functions.codebase = codebase;
Expand Down
Loading