Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a3d67c4
feat: add stdio-based function discovery as alternative to HTTP
taeold Jul 11, 2025
deac122
feat: implement file-based function discovery using FUNCTIONS_MANIFES…
taeold Jul 13, 2025
bb9f20a
test: update function discovery test fixtures
taeold Jul 13, 2025
1932fd3
extend cli helper for integration tests.
taeold Jul 13, 2025
1e85d91
Merge branch 'master' of https://github.com/firebase/firebase-tools i…
taeold Jul 14, 2025
f4a95f2
Set up tmpdir to save functions.yaml if missing
taeold Jul 14, 2025
cab0fe9
feat: Add experimental file-based discovery via env var
taeold Jul 14, 2025
eab25cf
fix: Remove nested .gemini directory from source control
taeold Jul 14, 2025
7ed66e8
refactor(tests): de-duplicate function discovery tests
taeold Jul 14, 2025
8bfc6f6
fix integration test
taeold Jul 15, 2025
67aef71
test: update firebase-functions to v6.4.0 in discovery test fixtures
taeold Jul 15, 2025
ccfc943
fix: address code review comments for function discovery
taeold Jul 15, 2025
ab50b5f
refactor: remove promisify usage and change discovery log level to debug
taeold Jul 15, 2025
5b70344
fix: remove file deletion from detectFromOutputPath
taeold Jul 15, 2025
53b6c56
cleanup impl
taeold Jul 15, 2025
0681b02
format.
taeold Jul 15, 2025
a643302
Update src/deploy/functions/runtimes/discovery/index.ts
taeold Jul 15, 2025
09ce508
respond to gemini code review.
taeold Jul 15, 2025
e09b2ef
Merge branch 'feat/stdio-discovery' of https://github.com/firebase/fi…
taeold Jul 15, 2025
412f1be
misc nits.
taeold Jul 15, 2025
e10f030
Merge branch 'master' into feat/stdio-discovery
taeold Jul 15, 2025
7dd3d05
remove accidentally commited file.
taeold Jul 15, 2025
30a8119
refactor: improve function discovery robustness and code quality
taeold Jul 16, 2025
db3fafa
revert: simplify detectFromOutputPath back to process exit approach
taeold Jul 16, 2025
a90b818
fix: address PR review feedback
taeold Jul 16, 2025
455ca15
Merge branch 'master' into feat/stdio-discovery
taeold Jul 16, 2025
5372810
fix: revert nullish coalescing for timeout to handle zero values
taeold Jul 16, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ scripts/*.json
lib/
dev/
clean/
.gemini/
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"name": "dist",
"version": "0.0.1",
"engines": {
"node": "20"
}
"version": "0.0.1"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"functions": {
"source": "dist"
"source": "dist",
"runtime": "nodejs22"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"name": "dist",
"version": "0.0.1",
"dependencies": {
"firebase-functions": "^5.1.0"
},
"engines": {
"node": "20"
"firebase-functions": "^6.4.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{
"source": "v1",
"codebase": "v1",
"runtime": "nodejs22",
"ignore": [
"node_modules",
".git",
Expand All @@ -13,6 +14,7 @@
{
"source": "v2",
"codebase": "v2",
"runtime": "nodejs22",
"ignore": [
"node_modules",
".git",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"name": "codebase-v1",
"dependencies": {
"firebase-functions": "^5.1.0"
},
"engines": {
"node": "20"
"firebase-functions": "^6.4.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"name": "codebase-v2",
"type": "module",
"dependencies": {
"firebase-functions": "^5.1.0"
},
"engines": {
"node": "20"
"firebase-functions": "^6.4.0"
}
}
5 changes: 4 additions & 1 deletion scripts/functions-discover-tests/fixtures/esm/firebase.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"functions": {}
"functions": {
"source": "functions",
"runtime": "nodejs22"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"name": "esm",
"type": "module",
"dependencies": {
"firebase-functions": "^5.1.0"
},
"engines": {
"node": "20"
"firebase-functions": "^6.4.0"
}
}
5 changes: 4 additions & 1 deletion scripts/functions-discover-tests/fixtures/pnpm/firebase.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"functions": {}
"functions": {
"source": "functions",
"runtime": "nodejs22"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"name": "pnpm",
"dependencies": {
"firebase-functions": "^5.1.0"
},
"engines": {
"node": "20"
"firebase-functions": "^6.4.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"functions": {}
"functions": {
"source": "functions",
"runtime": "nodejs22"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"name": "simple",
"dependencies": {
"firebase-functions": "^5.1.0"
},
"engines": {
"node": "20"
"firebase-functions": "^6.4.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"functions": {
"source": "functions",
"runtime": "nodejs22"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { onRequest } = require("firebase-functions/v2/https");

// Generate 20 functions for stress testing
for (let i = 1; i <= 20; i++) {
exports[`stressFunction${i}`] = onRequest((request, response) => {
response.send(`Hello from stress function ${i}!`);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "stress-test",
"dependencies": {
"firebase-functions": "^6.4.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
set -euxo pipefail # bash strict mode
IFS=$'\n\t'

cd functions && npm i
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"functions": {
"source": "packages/functions"
"source": "packages/functions",
"runtime": "nodejs22"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"name": "simple",
"version": "0.0.1",
"dependencies": {
"firebase-functions": "5.1.0",
"firebase-functions": "^6.4.0",
"firebase-admin": "^11.2.0",
"@firebase/a-test-pkg": "0.0.1"
},
"engines": {
"node": "20"
"node": ">=20"
},
"private": true
}
101 changes: 75 additions & 26 deletions scripts/functions-discover-tests/tests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as path from "path";
import * as path from "node:path";
import * as os from "node:os";
import * as fs from "node:fs/promises";

import { expect } from "chai";
import { CLIProcess } from "../integration-helpers/cli";
Expand All @@ -15,8 +17,45 @@
}[];
}

async function runDiscoveryTest(
projectDir: string,
testcase: Testcase,
env?: Record<string, string>,
): Promise<void> {
const cli = new CLIProcess("default", projectDir);

let outputBuffer = "";
let output: any;

Check warning on line 28 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
await cli.start(
"internaltesting:functions:discover",
FIREBASE_PROJECT,
["--json"],
(data: any) => {

Check warning on line 33 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
outputBuffer += data;
try {
output = JSON.parse(outputBuffer);

Check warning on line 36 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
return true;
} catch (e) {
// Not complete JSON yet, continue buffering
return false;
}
},
env,
);

expect(output.status).to.equal("success");

Check warning on line 46 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
for (const e of testcase.expects) {
const endpoints = output.result?.[e.codebase]?.endpoints;

Check warning on line 48 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .result on an `any` value

Check warning on line 48 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
expect(endpoints).to.be.an("object").that.is.not.empty;
expect(Object.keys(endpoints)).to.have.length(e.endpoints.length);

Check warning on line 50 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `{}`
expect(Object.keys(endpoints)).to.include.members(e.endpoints);

Check warning on line 51 in scripts/functions-discover-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `{}`
}

await cli.stop();
}

describe("Function discovery test", function (this) {
this.timeout(1000_000);
this.timeout(2000_000);

before(() => {
expect(FIREBASE_PROJECT).to.exist.and.not.be.empty;
Expand Down Expand Up @@ -87,31 +126,41 @@
},
],
},
{
name: "stress-test",
projectDir: "stress-test",
expects: [
{
codebase: "default",
endpoints: Array.from({ length: 20 }, (_, i) => `stressFunction${i + 1}`),
},
],
},
];

for (const tc of testCases) {
it(`discovers functions in a ${tc.name} project`, async () => {
const cli = new CLIProcess("default", path.join(FIXTURES, tc.projectDir));

let output: any;
await cli.start(
"internaltesting:functions:discover",
FIREBASE_PROJECT,
["--json"],
(data: any) => {
output = JSON.parse(data);
return true;
},
);
expect(output.status).to.equal("success");
for (const e of tc.expects) {
const endpoints = output.result?.[e.codebase]?.endpoints;
expect(endpoints).to.be.an("object").that.is.not.empty;
expect(Object.keys(endpoints)).to.have.length(e.endpoints.length);
expect(Object.keys(endpoints)).to.include.members(e.endpoints);
}
describe("detectFromPort", () => {
for (const tc of testCases) {
it(`discovers functions using HTTP in a ${tc.name} project`, async () => {
await runDiscoveryTest(path.join(FIXTURES, tc.projectDir), tc);
});
}
});

await cli.stop();
});
}
describe("detectFromOutputPath", () => {
for (const tc of testCases) {
it(`discovers functions using file-based discovery in a ${tc.name} project`, async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "firebase-test-"));
try {
await runDiscoveryTest(path.join(FIXTURES, tc.projectDir), tc, {
FIREBASE_FUNCTIONS_DISCOVERY_OUTPUT_PATH: tempDir,
});
} finally {
// Clean up the temp directory
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {
// Ignore cleanup errors
});
}
});
}
});
});
9 changes: 8 additions & 1 deletion scripts/integration-helpers/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,29 @@
project: string,
additionalArgs: string[],
logDoneFn?: (d: unknown) => unknown,
env?: Record<string, string>,
): Promise<void> {
const args = [cmd, "--project", project];

if (additionalArgs) {
args.push(...additionalArgs);
}

const p = spawn("firebase", args, { cwd: this.workdir });
const p = spawn("firebase", args, {
cwd: this.workdir,
env: env ? { ...process.env, ...env } : process.env,
});
if (!p) {
throw new Error("Failed to start firebase CLI");
}
this.process = p;

this.process.stdout?.on("data", (data: unknown) => {
process.stdout.write(`[${this.name} stdout] ` + data);

Check warning on line 35 in scripts/integration-helpers/cli.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Operands of '+' operation must either be both strings or both numbers. Consider using a template literal
});

this.process.stderr?.on("data", (data: unknown) => {
console.log(`[${this.name} stderr] ` + data);

Check warning on line 39 in scripts/integration-helpers/cli.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Operands of '+' operation must either be both strings or both numbers. Consider using a template literal
});

let started: Promise<void>;
Expand All @@ -51,6 +55,9 @@
};
p.stdout?.on("data", customCallback);
p.stdout?.on("close", customFailure);
p.stderr?.on("data", (data) => {
console.error(`[${this.name} stderr]`, data.toString());
});
});
} else {
started = new Promise((resolve) => {
Expand Down
9 changes: 5 additions & 4 deletions src/deploy/functions/runtimes/discovery/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from "chai";
import * as fs from "fs/promises";
import * as yaml from "yaml";
import * as sinon from "sinon";
import * as nock from "nock";
Expand Down Expand Up @@ -63,26 +64,26 @@ describe("yamlToBuild", () => {
});

describe("detectFromYaml", () => {
let readFileAsync: sinon.SinonStub;
let readFile: sinon.SinonStub;

beforeEach(() => {
readFileAsync = sinon.stub(discovery, "readFileAsync");
readFile = sinon.stub(fs, "readFile");
});

afterEach(() => {
sinon.verifyAndRestore();
});

it("succeeds when YAML can be found", async () => {
readFileAsync.resolves(YAML_TEXT);
readFile.resolves(YAML_TEXT);

await expect(
discovery.detectFromYaml("directory", "project", "nodejs16"),
).to.eventually.deep.equal(BUILD);
});

it("returns undefined when YAML cannot be found", async () => {
readFileAsync.rejects({ code: "ENOENT" });
readFile.rejects({ code: "ENOENT" });

await expect(discovery.detectFromYaml("directory", "project", "nodejs16")).to.eventually.equal(
undefined,
Expand Down
Loading
Loading