Skip to content

Commit 9967636

Browse files
committed
feat: Add .env.<codebase> support for multi-instance functions
Implements environment variable isolation per codebase to support multi-instance function deployments where the same source can be deployed multiple times with different configurations. Environment file precedence order: 1. .env (global defaults for all functions in source) 2. .env.<codebase> (codebase-specific settings) 3. .env.<project|alias> (project-specific overrides) 4. .env.local (emulator-only, highest precedence) Changes: - Update UserEnvsOpts interface to require codebase parameter - Modify findEnvfiles() to include .env.<codebase> in search order - Update all function calls to pass codebase parameter - Add comprehensive unit tests for codebase environment variables - Fix compilation errors in test files Enables configuration isolation for multi-instance deployments like: - .env.profile-pics-resizer for profile picture functions - .env.gallery-pics-resizer for gallery picture functions
1 parent ecbbc91 commit 9967636

File tree

5 files changed

+114
-28
lines changed

5 files changed

+114
-28
lines changed

src/deploy/functions/prepare.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ export async function prepare(
123123
const config = configForCodebase(context.config, codebase);
124124
const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId);
125125
const userEnvOpt: functionsEnv.UserEnvsOpts = {
126+
projectId,
127+
codebase,
126128
functionsSource: options.config.path(config.source),
127-
projectId: projectId,
128129
projectAlias: options.projectAlias,
129130
};
130131
const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);

src/deploy/functions/runtimes/node/parseTriggers.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async function resolveBackend(bd: build.Build): Promise<backend.Backend> {
1717
storageBucket: "foo.appspot.com",
1818
databaseURL: "https://foo.firebaseio.com",
1919
},
20-
userEnvOpt: { functionsSource: "", projectId: "PROJECT" },
20+
userEnvOpt: { functionsSource: "", projectId: "PROJECT", codebase: "default" },
2121
userEnvs: {},
2222
})
2323
).backend;

src/emulator/functionsEmulator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ export class FunctionsEmulator implements EmulatorInstance {
553553
projectId: this.args.projectId,
554554
projectAlias: this.args.projectAlias,
555555
isEmulator: true,
556+
codebase: emulatableBackend.codebase,
556557
};
557558
const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
558559
const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment);
@@ -1378,6 +1379,7 @@ export class FunctionsEmulator implements EmulatorInstance {
13781379
projectId: this.args.projectId,
13791380
projectAlias: this.args.projectAlias,
13801381
isEmulator: true,
1382+
codebase: backend.codebase,
13811383
};
13821384

13831385
if (functionsEnv.hasUserEnvs(projectInfo)) {

src/functions/env.spec.ts

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -310,19 +310,19 @@ FOO=foo
310310
it("never affects the filesystem if the list of keys to write is empty", () => {
311311
env.writeUserEnvs(
312312
{},
313-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
313+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
314314
);
315315
env.writeUserEnvs(
316316
{},
317-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true },
317+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
318318
);
319319
expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw;
320320
expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw;
321321
expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw;
322322
});
323323

324324
it("touches .env.projectId if it doesn't already exist", () => {
325-
env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir });
325+
env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir, codebase: "default" });
326326
expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw;
327327
expect(!!fs.statSync(path.join(tmpdir, ".env.project"))).to.be.true;
328328
expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw;
@@ -331,7 +331,7 @@ FOO=foo
331331
it("touches .env.local if it doesn't already exist in emulator mode", () => {
332332
env.writeUserEnvs(
333333
{ FOO: "bar" },
334-
{ projectId: "project", functionsSource: tmpdir, isEmulator: true },
334+
{ projectId: "project", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
335335
);
336336
expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw;
337337
expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw;
@@ -343,7 +343,7 @@ FOO=foo
343343
[".env.project"]: "FOO=foo",
344344
});
345345
expect(() =>
346-
env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir }),
346+
env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir, codebase: "default" }),
347347
).to.throw(FirebaseError);
348348
});
349349

@@ -353,14 +353,15 @@ FOO=foo
353353
});
354354
env.writeUserEnvs(
355355
{ FOO: "bar" },
356-
{ projectId: "project", functionsSource: tmpdir, isEmulator: true },
356+
{ projectId: "project", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
357357
);
358358
expect(
359359
env.loadUserEnvs({
360360
projectId: "project",
361361
projectAlias: "alias",
362362
functionsSource: tmpdir,
363363
isEmulator: true,
364+
codebase: "default",
364365
})["FOO"],
365366
).to.equal("bar");
366367
});
@@ -372,7 +373,7 @@ FOO=foo
372373
expect(() =>
373374
env.writeUserEnvs(
374375
{ FOO: "baz" },
375-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
376+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
376377
),
377378
).to.throw(FirebaseError);
378379
});
@@ -383,14 +384,15 @@ FOO=foo
383384
});
384385
env.writeUserEnvs(
385386
{ FOO: "baz" },
386-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true },
387+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
387388
);
388389
expect(
389390
env.loadUserEnvs({
390391
projectId: "project",
391392
projectAlias: "alias",
392393
functionsSource: tmpdir,
393394
isEmulator: true,
395+
codebase: "default",
394396
})["FOO"],
395397
).to.equal("baz");
396398
});
@@ -402,7 +404,7 @@ FOO=foo
402404
expect(() =>
403405
env.writeUserEnvs(
404406
{ ASDF: "bar" },
405-
{ projectId: "project", functionsSource: tmpdir, isEmulator: true },
407+
{ projectId: "project", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
406408
),
407409
).to.throw(FirebaseError);
408410
});
@@ -411,30 +413,30 @@ FOO=foo
411413
expect(() =>
412414
env.writeUserEnvs(
413415
{ lowercase: "bar" },
414-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
416+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
415417
),
416418
).to.throw(env.KeyValidationError);
417419
expect(() =>
418420
env.writeUserEnvs(
419421
{ GCP_PROJECT: "bar" },
420-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
422+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
421423
),
422424
).to.throw(env.KeyValidationError);
423425
expect(() =>
424426
env.writeUserEnvs(
425427
{ FIREBASE_KEY: "bar" },
426-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
428+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
427429
),
428430
).to.throw(env.KeyValidationError);
429431
});
430432

431433
it("writes the specified key to a .env.projectId that it created", () => {
432434
env.writeUserEnvs(
433435
{ FOO: "bar" },
434-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
436+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
435437
);
436438
expect(
437-
env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir })[
439+
env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" })[
438440
"FOO"
439441
],
440442
).to.equal("bar");
@@ -446,10 +448,10 @@ FOO=foo
446448
});
447449
env.writeUserEnvs(
448450
{ FOO: "bar" },
449-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
451+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
450452
);
451453
expect(
452-
env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir })[
454+
env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" })[
453455
"FOO"
454456
],
455457
).to.equal("bar");
@@ -458,12 +460,13 @@ FOO=foo
458460
it("writes multiple keys at once", () => {
459461
env.writeUserEnvs(
460462
{ FOO: "foo", BAR: "bar" },
461-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
463+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
462464
);
463465
const envs = env.loadUserEnvs({
464466
projectId: "project",
465467
projectAlias: "alias",
466468
functionsSource: tmpdir,
469+
codebase: "default",
467470
});
468471
expect(envs["FOO"]).to.equal("foo");
469472
expect(envs["BAR"]).to.equal("bar");
@@ -476,12 +479,13 @@ FOO=foo
476479
WITH_SLASHES: "\n\\\r\\\t\\\v",
477480
QUOTES: "'\"'",
478481
},
479-
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
482+
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, codebase: "default" },
480483
);
481484
const envs = env.loadUserEnvs({
482485
projectId: "project",
483486
projectAlias: "alias",
484487
functionsSource: tmpdir,
488+
codebase: "default",
485489
});
486490
expect(envs["ESCAPES"]).to.equal("\n\r\t\v");
487491
expect(envs["WITH_SLASHES"]).to.equal("\n\\\r\\\t\\\v");
@@ -492,12 +496,12 @@ FOO=foo
492496
try {
493497
env.writeUserEnvs(
494498
{ FOO: "bar", lowercase: "bar" },
495-
{ projectId: "project", functionsSource: tmpdir },
499+
{ projectId: "project", functionsSource: tmpdir, codebase: "default" },
496500
);
497501
} catch (err: any) {
498502
// no-op
499503
}
500-
expect(env.loadUserEnvs({ projectId: "project", functionsSource: tmpdir })["FOO"]).to.be
504+
expect(env.loadUserEnvs({ projectId: "project", functionsSource: tmpdir, codebase: "default" })["FOO"]).to.be
501505
.undefined;
502506
});
503507
});
@@ -511,6 +515,7 @@ FOO=foo
511515
const projectInfo: Omit<env.UserEnvsOpts, "functionsSource"> = {
512516
projectId: "my-project",
513517
projectAlias: "dev",
518+
codebase: "default",
514519
};
515520
let tmpdir: string;
516521

@@ -695,6 +700,78 @@ FOO=foo
695700
env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir });
696701
}).to.throw("Failed to load");
697702
});
703+
704+
describe("codebase environment variables", () => {
705+
it("loads envs from .env.<codebase> file", () => {
706+
createEnvFiles(tmpdir, {
707+
[`.env.${projectInfo.codebase}`]: "FOO=codebase-foo\nBAR=codebase-bar",
708+
});
709+
710+
expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({
711+
FOO: "codebase-foo",
712+
BAR: "codebase-bar",
713+
});
714+
});
715+
716+
it("loads envs with correct precedence: .env < .env.<codebase> < .env.<project>", () => {
717+
createEnvFiles(tmpdir, {
718+
".env": "FOO=global\nBAR=global\nBAZ=global",
719+
[`.env.${projectInfo.codebase}`]: "FOO=codebase\nBAR=codebase",
720+
[`.env.${projectInfo.projectId}`]: "FOO=project",
721+
});
722+
723+
expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({
724+
FOO: "project", // project overrides codebase
725+
BAR: "codebase", // codebase overrides global
726+
BAZ: "global", // only defined in global
727+
});
728+
});
729+
730+
it("loads envs with correct precedence: .env < .env.<codebase> < .env.<alias>", () => {
731+
createEnvFiles(tmpdir, {
732+
".env": "FOO=global\nBAR=global\nBAZ=global",
733+
[`.env.${projectInfo.codebase}`]: "FOO=codebase\nBAR=codebase",
734+
[`.env.${projectInfo.projectAlias}`]: "FOO=alias",
735+
});
736+
737+
expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({
738+
FOO: "alias", // alias overrides codebase
739+
BAR: "codebase", // codebase overrides global
740+
BAZ: "global", // only defined in global
741+
});
742+
});
743+
744+
it("works with custom codebase names", () => {
745+
const customProjectInfo = { ...projectInfo, codebase: "profile-pics-resizer" };
746+
createEnvFiles(tmpdir, {
747+
".env": "FOO=global",
748+
".env.profile-pics-resizer": "FOO=custom-codebase\nCUSTOM=value",
749+
});
750+
751+
expect(env.loadUserEnvs({ ...customProjectInfo, functionsSource: tmpdir })).to.be.deep.equal({
752+
FOO: "custom-codebase",
753+
CUSTOM: "value",
754+
});
755+
});
756+
757+
it("loads envs correctly for emulator with .env.local precedence", () => {
758+
createEnvFiles(tmpdir, {
759+
".env": "FOO=global\nBAR=global\nBAZ=global",
760+
[`.env.${projectInfo.codebase}`]: "FOO=codebase\nBAR=codebase",
761+
[`.env.${projectInfo.projectId}`]: "FOO=project",
762+
".env.local": "FOO=local",
763+
});
764+
765+
expect(
766+
env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir, isEmulator: true }),
767+
).to.be.deep.equal({
768+
FOO: "local", // .env.local has highest precedence in emulator
769+
BAR: "codebase", // codebase overrides global
770+
BAZ: "global", // only defined in global
771+
});
772+
});
773+
774+
});
698775
});
699776

700777
describe("parseStrict", () => {

src/functions/env.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,12 @@ export function parseStrict(data: string): Record<string, string> {
220220
function findEnvfiles(
221221
functionsSource: string,
222222
projectId: string,
223+
codebase: string,
223224
projectAlias?: string,
224225
isEmulator?: boolean,
225226
): string[] {
226227
const files: string[] = [".env"];
228+
files.push(`.env.${codebase}`);
227229
files.push(`.env.${projectId}`);
228230
if (projectAlias) {
229231
files.push(`.env.${projectAlias}`);
@@ -243,6 +245,7 @@ export interface UserEnvsOpts {
243245
projectId: string;
244246
projectAlias?: string;
245247
isEmulator?: boolean;
248+
codebase: string;
246249
}
247250

248251
/**
@@ -255,8 +258,9 @@ export function hasUserEnvs({
255258
projectId,
256259
projectAlias,
257260
isEmulator,
261+
codebase,
258262
}: UserEnvsOpts): boolean {
259-
return findEnvfiles(functionsSource, projectId, projectAlias, isEmulator).length > 0;
263+
return findEnvfiles(functionsSource, projectId, codebase, projectAlias, isEmulator).length > 0;
260264
}
261265

262266
/**
@@ -269,10 +273,10 @@ export function writeUserEnvs(toWrite: Record<string, string>, envOpts: UserEnvs
269273
if (Object.keys(toWrite).length === 0) {
270274
return;
271275
}
272-
const { functionsSource, projectId, projectAlias, isEmulator } = envOpts;
276+
const { functionsSource, projectId, projectAlias, isEmulator, codebase } = envOpts;
273277

274278
// Determine which .env file to write to, and create it if it doesn't exist
275-
const allEnvFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
279+
const allEnvFiles = findEnvfiles(functionsSource, projectId, codebase, projectAlias, isEmulator);
276280
const targetEnvFile = envOpts.isEmulator
277281
? FUNCTIONS_EMULATOR_DOTENV
278282
: `.env.${envOpts.projectId}`;
@@ -356,8 +360,9 @@ function formatUserEnvForWrite(key: string, value: string): string {
356360
*
357361
* .env files are searched and merged in the following order:
358362
*
359-
* 1. .env
360-
* 2. .env.<project or alias>
363+
* 1. .env (global defaults for all functions in source)
364+
* 2. .env.<codebase> (codebase-specific settings)
365+
* 3. .env.<project or alias> (project-specific overrides)
361366
*
362367
* If both .env.<project> and .env.<alias> files are found, an error is thrown.
363368
*
@@ -368,8 +373,9 @@ export function loadUserEnvs({
368373
projectId,
369374
projectAlias,
370375
isEmulator,
376+
codebase,
371377
}: UserEnvsOpts): Record<string, string> {
372-
const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
378+
const envFiles = findEnvfiles(functionsSource, projectId, codebase, projectAlias, isEmulator);
373379
if (envFiles.length === 0) {
374380
return {};
375381
}

0 commit comments

Comments
 (0)