Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
4349832
Add support for BUILD-available secrets for Local Builds (#10229)
falahat Apr 16, 2026
b5cb204
Add a warning/confirmation when using BUILD-available secrets for loc…
falahat Apr 16, 2026
63a529e
Consolidate the ABIU prompts in the CLI so it matches the UI behavior…
falahat Apr 16, 2026
c573c6a
create a prototype version of local builds that uses the universal maker
falahat Apr 16, 2026
67cc1cc
Add some gross hacks to stop bundle.yaml from being wiped empty
falahat Apr 17, 2026
a5ae2b7
Run tests automatically against next (#10365)
joehan Apr 17, 2026
792502a
feat: add SSE mode support for MCP server (#10258)
joehan Apr 17, 2026
a7cf61e
Get rid of the watcher hack
falahat Apr 17, 2026
a2bce07
Address some review comments (unknown casts)
falahat Apr 17, 2026
80f5aa1
Address some other review comments
falahat Apr 17, 2026
0bb1d5c
Add a code comment
falahat Apr 17, 2026
0bccdc2
simplify local build code for tarballs
falahat Apr 17, 2026
152505d
simplify further
falahat Apr 17, 2026
d2185aa
Further simplify diffs
falahat Apr 17, 2026
9864f27
fix: prevent hosting deploy to site in wrong project (#10380)
joehan Apr 17, 2026
49ec34d
test: improve coverage for appdistribution distribution (#10353)
joehan Apr 20, 2026
98e3c4f
Add automatic downloading of the UM binary
falahat Apr 21, 2026
125676e
Address review comments:
falahat Apr 21, 2026
e6b0438
test: improve coverage for dataconnect names (#10350)
joehan Apr 21, 2026
13c3a47
update valid python runtimes (#10393)
aalej Apr 21, 2026
b8abe9b
Create a common utility for downloading. This allows emulaotr and uni…
falahat Apr 21, 2026
0cb9a89
Fix FIREBASE_OUTPUT_BUNDLE_DIR
falahat Apr 21, 2026
ebd7d70
formatting fix
falahat Apr 21, 2026
6469643
fix test
falahat Apr 21, 2026
b846dbb
test fix
falahat Apr 21, 2026
3b658b4
Silence punycode deprecation notice (#10395)
joehan Apr 21, 2026
6a01893
Automatically tag previous image during release (#10367)
joehan Apr 21, 2026
9c3a64b
make the diffs smaller
falahat Apr 21, 2026
2c40423
reduce diffs
falahat Apr 21, 2026
8a5ef6b
Make our hash validation more strict. ALWAYS validate, even on cache …
falahat Apr 21, 2026
c52080c
format fix
falahat Apr 21, 2026
064d9d1
format and test fix
falahat Apr 21, 2026
a8ebb98
fix tests
falahat Apr 21, 2026
2f13965
Update src/apphosting/localbuilds.ts
falahat Apr 21, 2026
9b2d82e
Update src/apphosting/localbuilds.ts
falahat Apr 21, 2026
db9f107
fix test
falahat Apr 21, 2026
6a427ea
chore: add .gitignore entries for the Dart/Flutter files (#10399)
kevmoo Apr 22, 2026
35ccb83
Merge branch 'next' of github.com:firebase/firebase-tools into univer…
falahat Apr 22, 2026
747b829
test: improve coverage for apphosting yaml configs (#10355)
joehan Apr 22, 2026
59a79f5
Prompt for ABIU runtime settings when creating a backend from the "de…
falahat Apr 22, 2026
e6d1796
Revert "Prompt for ABIU runtime settings when creating a backend from…
falahat Apr 22, 2026
e30a646
Making onCall Dart functions Public when deployed (#10390)
shettyvarun268 Apr 22, 2026
b66fa2d
Clean up 'any' typing in src/emulators (#10405)
joehan Apr 23, 2026
62e9948
test: improve coverage for firestore deploy (#10346)
joehan Apr 23, 2026
64d7380
test: improve test coverage for resourceManager (#10344)
joehan Apr 23, 2026
6852bf6
chore: address dependabot alerts in subprojects (#10413)
joehan Apr 23, 2026
9ae31c3
test: improve coverage for use command (#10345)
joehan Apr 24, 2026
ec56f9f
Revert "test: improve coverage for use command (#10345)" (#10418)
joehan Apr 24, 2026
aa1dc58
Make dartfunctions experiment public
wandamora Apr 24, 2026
17add3f
Cli prompts fixed to respect non interactive flag (#10401)
tammam-g Apr 25, 2026
57aee02
Merge branch 'next' of github.com:firebase/firebase-tools into univer…
falahat Apr 28, 2026
0564f1e
Merge branch 'main' of github.com:firebase/firebase-tools into univer…
falahat Apr 28, 2026
94f419e
address some linter warnings
falahat Apr 28, 2026
acc1d87
Address some code review comments
falahat Apr 28, 2026
8bcaf78
fix availability issue
falahat Apr 28, 2026
0dd50ec
Address final (ha!) review comments
falahat Apr 28, 2026
112c712
Merge branch 'main' of github.com:firebase/firebase-tools into univer…
falahat May 1, 2026
0848891
Merge branch 'main' of github.com:firebase/firebase-tools into univer…
falahat May 4, 2026
1812379
Address review comments on readability. Improve error handling and lo…
falahat May 4, 2026
e374b78
Merge branch 'main' of github.com:firebase/firebase-tools into univer…
falahat May 11, 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
64 changes: 0 additions & 64 deletions src/apphosting/backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ describe("apphosting setup functions", () => {
serviceAccount: "custom-service-account",
appId: webAppId,
runtime: { value: "" },
automaticBaseImageUpdatesDisabled: undefined,
};
expect(createBackendStub).to.be.calledWith(projectId, location, backendInput, backendId);
});
Expand Down Expand Up @@ -155,74 +154,11 @@ describe("apphosting setup functions", () => {
serviceAccount: "custom-service-account",
appId: webAppId,
runtime: { value: runtime ?? "" },
automaticBaseImageUpdatesDisabled: undefined,
};
expect(createBackendStub).to.be.calledWith(projectId, location, backendInput, backendId);
});
}

it("should create a new backend with automatic base image updates disabled", async () => {
createBackendStub.resolves(op);
pollOperationStub.resolves(completeBackend);

await createBackend(
projectId,
location,
backendId,
"custom-service-account",
cloudBuildConnRepo,
webAppId,
"/",
undefined,
true, // automaticBaseImageUpdatesDisabled
);

const backendInput: Omit<apphosting.Backend, apphosting.BackendOutputOnlyFields> = {
servingLocality: "GLOBAL_ACCESS",
codebase: {
repository: cloudBuildConnRepo.name,
rootDirectory: "/",
},
labels: deploymentTool.labels(),
serviceAccount: "custom-service-account",
appId: webAppId,
runtime: { value: "" },
automaticBaseImageUpdatesDisabled: true,
};
expect(createBackendStub).to.be.calledWith(projectId, location, backendInput, backendId);
});

it("should create a new backend with automatic base image updates enabled", async () => {
createBackendStub.resolves(op);
pollOperationStub.resolves(completeBackend);

await createBackend(
projectId,
location,
backendId,
"custom-service-account",
cloudBuildConnRepo,
webAppId,
"/",
undefined,
false, // automaticBaseImageUpdatesDisabled
);

const backendInput: Omit<apphosting.Backend, apphosting.BackendOutputOnlyFields> = {
servingLocality: "GLOBAL_ACCESS",
codebase: {
repository: cloudBuildConnRepo.name,
rootDirectory: "/",
},
labels: deploymentTool.labels(),
serviceAccount: "custom-service-account",
appId: webAppId,
runtime: { value: "" },
automaticBaseImageUpdatesDisabled: false,
};
expect(createBackendStub).to.be.calledWith(projectId, location, backendInput, backendId);
});

it("should set default rollout policy to 100% all at once", async () => {
const completeTraffic: apphosting.Traffic = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
Expand Down
12 changes: 1 addition & 11 deletions src/apphosting/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import fetch from "node-fetch";
import { orchestrateRollout } from "./rollout";
import * as fuzzy from "fuzzy";
import { isEnabled } from "../experiments";
import { DEFAULT_RUNTIME, promptRuntime, promptAutomaticBaseImageUpdates } from "./prompts";
import { DEFAULT_RUNTIME, promptRuntime } from "./prompts";

const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";

Expand Down Expand Up @@ -82,7 +82,6 @@ export async function doSetup(
primaryRegion?: string,
rootDir?: string,
runtime?: string,
automaticBaseImageUpdatesDisabled?: boolean,
): Promise<void> {
await ensureRequiredApisEnabled(projectId);

Expand Down Expand Up @@ -137,12 +136,6 @@ export async function doSetup(
}
}

if (automaticBaseImageUpdatesDisabled === undefined && isEnabled("abiu")) {
if (!nonInteractive) {
automaticBaseImageUpdatesDisabled = !(await promptAutomaticBaseImageUpdates());
}
}

const webApp = await webApps.getOrCreateWebApp(
projectId,
webAppName ? webAppName : null,
Expand All @@ -162,7 +155,6 @@ export async function doSetup(
webApp?.id,
rootDir,
runtime,
automaticBaseImageUpdatesDisabled,
);
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);

Expand Down Expand Up @@ -373,7 +365,6 @@ export async function createBackend(
webAppId: string | undefined,
rootDir = "/",
runtime?: string,
automaticBaseImageUpdatesDisabled?: boolean,
): Promise<Backend> {
const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
const backendReqBody: Omit<Backend, BackendOutputOnlyFields> = {
Expand All @@ -392,7 +383,6 @@ export async function createBackend(
// this is to be extra careful that we do not set the ABIU fields if the experiment is disabled
if (isEnabled("abiu")) {
backendReqBody.runtime = { value: runtime ?? "" };
backendReqBody.automaticBaseImageUpdatesDisabled = automaticBaseImageUpdatesDisabled;
}

async function createBackendAndPoll(): Promise<apphosting.Backend> {
Expand Down
230 changes: 228 additions & 2 deletions src/apphosting/localbuilds.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as sinon from "sinon";
import { expect } from "chai";
import * as localBuildModule from "@apphosting/build";
import { localBuild } from "./localbuilds";
import { localBuild, runUniversalMaker } from "./localbuilds";
import * as secrets from "./secrets";
import { EnvMap } from "./yaml";
import * as childProcess from "child_process";
import * as fs from "fs";

describe("localBuild", () => {
afterEach(() => {
Expand All @@ -11,6 +15,7 @@ describe("localBuild", () => {
it("returns the expected output", async () => {
const bundleConfig = {
version: "v1" as const,

runConfig: {
runCommand: "npm run build:prod",
},
Expand Down Expand Up @@ -38,10 +43,231 @@ describe("localBuild", () => {
const localApphostingBuildStub: sinon.SinonStub = sinon
.stub(localBuildModule, "localBuild")
.resolves(bundleConfig);
const { outputFiles, annotations, buildConfig } = await localBuild("./", "nextjs");
const { outputFiles, annotations, buildConfig } = await localBuild(
"test-project",
"./",
"nextjs",
);
expect(annotations).to.deep.equal(expectedAnnotations);
expect(buildConfig).to.deep.equal(expectedBuildConfig);
expect(outputFiles).to.deep.equal(expectedOutputFiles);
sinon.assert.calledWith(localApphostingBuildStub, "./", "nextjs");
});

it("resolves BUILD-available secrets passed in the environment map and ignores RUNTIME-only ones", async () => {
const bundleConfig = {
version: "v1" as const,
runConfig: { runCommand: "npm run build:prod" },
metadata: {
adapterPackageName: "@apphosting/angular-adapter",
adapterVersion: "14.1",
framework: "nextjs",
},
outputFiles: { serverApp: { include: ["./next/standalone"] } },
};
sinon.stub(localBuildModule, "localBuild").callsFake(async () => {
expect(process.env.MY_BUILD_SECRET).to.equal("secret-value");
expect(process.env.MY_RUNTIME_SECRET).to.be.undefined;
expect(process.env.MY_PLAIN_VAR).to.equal("plain-value");
return bundleConfig;
});
const loadSecretStub = sinon.stub(secrets, "loadSecret").resolves("secret-value");

const envMap: EnvMap = {
MY_BUILD_SECRET: { secret: "my-secret-id", availability: ["BUILD"] },
MY_RUNTIME_SECRET: { secret: "runtime-only-id", availability: ["RUNTIME"] },
MY_PLAIN_VAR: { value: "plain-value" },
};

await localBuild("test-project", "./", "nextjs", envMap, {
nonInteractive: true,
allowLocalBuildSecrets: true,
});

expect(loadSecretStub).to.have.been.calledWith("test-project", "my-secret-id");
// Confirm RUNTIME-only secret was ignored
expect(loadSecretStub).to.have.been.calledOnce;
// Confirm injected envs were cleaned up from the global scope after the build finishes
expect(process.env.MY_BUILD_SECRET).to.be.undefined;
expect(process.env.MY_RUNTIME_SECRET).to.be.undefined;
});

it("handles environment variables that do not contain secrets", async () => {
const bundleConfig = {
version: "v1" as const,
runConfig: { runCommand: "npm run build:prod" },
metadata: {
adapterPackageName: "@apphosting/angular-adapter",
adapterVersion: "14.1",
framework: "nextjs",
},
outputFiles: { serverApp: { include: ["./next/standalone"] } },
};
sinon.stub(localBuildModule, "localBuild").callsFake(async () => {
expect(process.env.MY_PLAIN_VAR).to.equal("plain-value");
expect(process.env.ANOTHER_VAR).to.equal("another-value");
return bundleConfig;
});
const loadSecretStub = sinon.stub(secrets, "loadSecret").resolves("secret-value");

const envMap: EnvMap = {
MY_PLAIN_VAR: { value: "plain-value" },
ANOTHER_VAR: { value: "another-value" },
};

await localBuild("test-project", "./", "nextjs", envMap);

expect(loadSecretStub).to.not.have.been.called;
// We expect the original process.env to not have these injected globally after run completes,
// as localBuild cleans up.
expect(process.env.MY_PLAIN_VAR).to.be.undefined;
expect(process.env.ANOTHER_VAR).to.be.undefined;
});

describe("localBuild secret confirmations", () => {
let confirmStub: sinon.SinonStub;

beforeEach(() => {
confirmStub = sinon.stub(require("../prompt"), "confirm");
});

it("throws an error in non-interactive mode if build-available secrets are used without the bypass flag", async () => {
const envMap: EnvMap = {
MY_BUILD_SECRET: { secret: "my-secret-id", availability: ["BUILD"] },
};

await expect(
localBuild("test-project", "./", "nextjs", envMap, { nonInteractive: true }),
).to.be.rejectedWith(
"Using build-available secrets during a local build in non-interactive mode requires the --allow-local-build-secrets flag.",
);
});

it("allows build-available secrets in non-interactive mode if bypass flag is provided", async () => {
const bundleConfig = {
version: "v1" as const,
runConfig: { runCommand: "npm run build:prod" },
metadata: {
adapterPackageName: "@apphosting/angular-adapter",
adapterVersion: "14.1",
framework: "nextjs",
},
outputFiles: { serverApp: { include: ["./next/standalone"] } },
};
sinon.stub(localBuildModule, "localBuild").resolves(bundleConfig);
sinon.stub(secrets, "loadSecret").resolves("secret-value");

const envMap: EnvMap = {
MY_BUILD_SECRET: { secret: "my-secret-id", availability: ["BUILD"] },
};

await localBuild("test-project", "./", "nextjs", envMap, {
nonInteractive: true,
allowLocalBuildSecrets: true,
});

expect(confirmStub).to.not.have.been.called;
});

it("cancels the build if the user declines the secrets confirmation prompt", async () => {
confirmStub.resolves(false);

const envMap: EnvMap = {
MY_BUILD_SECRET: { secret: "my-secret-id", availability: ["BUILD"] },
};

await expect(
localBuild("test-project", "./", "nextjs", envMap, { nonInteractive: false }),
).to.be.rejectedWith("Cancelled local build due to BUILD-available secrets.");
expect(confirmStub).to.have.been.calledOnce;
});

it("proceeds with the build if the user accepts the secrets confirmation prompt", async () => {
confirmStub.resolves(true);
const bundleConfig = {
version: "v1" as const,
runConfig: { runCommand: "npm run build:prod" },
metadata: {
adapterPackageName: "@apphosting/angular-adapter",
adapterVersion: "14.1",
framework: "nextjs",
},
outputFiles: { serverApp: { include: ["./next/standalone"] } },
};
sinon.stub(localBuildModule, "localBuild").resolves(bundleConfig);
sinon.stub(secrets, "loadSecret").resolves("secret-value");

const envMap: EnvMap = {
MY_BUILD_SECRET: { secret: "my-secret-id", availability: ["BUILD"] },
};

await localBuild("test-project", "./", "nextjs", envMap, { nonInteractive: false });
expect(confirmStub).to.have.been.calledOnce;
});
});

describe("runUniversalMaker", () => {
it("should successfully execute Universal Maker and parse output", () => {
process.env.UNIVERSAL_MAKER_BINARY = "/path/to/universal_maker";
const spawnStub = sinon
.stub(childProcess, "spawnSync")
.returns({} as unknown as childProcess.SpawnSyncReturns<string>);
Comment thread
falahat marked this conversation as resolved.
Outdated
sinon.stub(fs, "existsSync").returns(true);
const readFileSyncStub = sinon.stub(fs, "readFileSync").returns(
JSON.stringify({
command: "npm",
args: ["run", "start"],
language: "nodejs",
runtime: "nodejs22",
envVars: { PORT: 3000 },
}),
);

const output = runUniversalMaker("./", "nextjs");

expect(output).to.deep.equal({
metadata: {
language: "nodejs",
runtime: "nodejs22",
framework: "nextjs",
},
runConfig: {
runCommand: "npm run start",
environmentVariables: [{ variable: "PORT", value: "3000", availability: ["RUNTIME"] }],
},
outputFiles: {
serverApp: {
include: [".apphosting"],
},
},
});

sinon.assert.calledOnce(spawnStub);
sinon.assert.calledOnce(readFileSyncStub);
delete process.env.UNIVERSAL_MAKER_BINARY;
});

it("should raise clear FirebaseError when UNIVERSAL_MAKER_BINARY is undefined", () => {
delete process.env.UNIVERSAL_MAKER_BINARY;

expect(() => runUniversalMaker("./")).to.throw(
"Please specify the path to your Universal Maker binary by establishing the UNIVERSAL_MAKER_BINARY environment variable.",
);
});

it("should raise clear FirebaseError on permission errors within child execution", () => {
process.env.UNIVERSAL_MAKER_BINARY = "/path/to/universal_maker";
sinon.stub(childProcess, "spawnSync").callsFake(() => {
const err = new Error("EACCES exception") as NodeJS.ErrnoException;
err.code = "EACCES";

throw err;
});

expect(() => runUniversalMaker("./")).to.throw(
"Failed to execute the Universal Maker binary due to permission constraints. Please assure you have set chmod +x on your file.",
);
delete process.env.UNIVERSAL_MAKER_BINARY;
});
});
});
Loading
Loading