Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
65e0c59
Zip deploy fixes
annajowang Nov 22, 2025
ecbbe5e
Merge branch 'master' into zip-deploy
annajowang Dec 2, 2025
60adf94
use staging and specify uploading TAR
annajowang Dec 2, 2025
bcf0cd3
Improve docs
falahat Feb 2, 2026
4603750
Allow reading of simple env vars (not secrets) and passing them down …
falahat Feb 2, 2026
eb89f6b
Add some fixes for npm parsing issues
falahat Feb 3, 2026
9470c3d
Adding various local fixes/hacks to make local builds work. It includes:
falahat Feb 3, 2026
269da87
Follow-up fixes for ensuring we have account access for cloud storage…
falahat Feb 4, 2026
92a3f49
formatting fixes
falahat Feb 4, 2026
76ef53a
Merge branch 'main' of github.com:firebase/firebase-tools into zip-de…
falahat Feb 4, 2026
340020f
Get rid of env override
falahat Feb 11, 2026
d5343bd
Temporarily add a test app to easily test changes
falahat Feb 11, 2026
e17f55c
For local builds, explicitly check for apphosting.yaml files (and all…
falahat Feb 13, 2026
8341bc4
Add hono to packages
falahat Feb 13, 2026
4ed3b41
Merge branch 'main' of github.com:firebase/firebase-tools into zip-de…
falahat Feb 13, 2026
b76fa54
Merge branch 'main' of github.com:firebase/firebase-tools into zip-de…
falahat Feb 13, 2026
31fed5d
Merge branch 'zip-deploy' of github.com:firebase/firebase-tools into …
falahat Feb 13, 2026
2f653ca
Merge branch 'main' of github.com:firebase/firebase-tools into zip_de…
falahat Mar 9, 2026
b19b8bf
Get rid of test app
falahat Mar 9, 2026
611f258
Get rid of uniformBucketLevelAccess config which was not required
falahat Mar 9, 2026
37bb714
Fix tarball unit test
falahat Mar 9, 2026
1df73d7
Make local builds less hardcoded by checking if the build config is a…
falahat Mar 9, 2026
7d61148
Add a new experiment flag to control local builds (apphostinglocalbui…
falahat Mar 9, 2026
0ec1b92
Remove references to runtime flag
falahat Mar 9, 2026
2ea831d
Merge branch 'main' into zip_deploy_aryanf
falahat Mar 10, 2026
d20b46c
Merge branch 'main' of github.com:firebase/firebase-tools into zip_de…
falahat Mar 13, 2026
25262ac
Clean up the PR so that we gate by apphostinglocalbuilds experiment m…
falahat Mar 13, 2026
2f63f7a
Make the code for adding service account IAM permission more restrict…
falahat Mar 13, 2026
de4f95b
remove es2020 dependency
falahat Mar 13, 2026
7f122c2
feat: barebones local build implementation
falahat Mar 14, 2026
a4476a1
feat: reintroduce p4sa logic for local builds
falahat Mar 14, 2026
9c0d156
Merge branch 'main' of github.com:firebase/firebase-tools into zip_de…
falahat Mar 23, 2026
12dbb8d
reset unrelated changes
falahat Mar 23, 2026
0d06734
Manually reduce diffs
falahat Mar 23, 2026
8eb6065
format fix
falahat Mar 23, 2026
138bd3a
linter and test fixes
falahat Mar 23, 2026
ac0ac15
Fix test import
falahat Mar 23, 2026
bd43ff8
Move the p4sa flow to the prepare file where it's actually used
falahat Mar 23, 2026
1ac60b1
Linter fixes
falahat Mar 23, 2026
d33ca44
Move backends config call
falahat Mar 23, 2026
5718f91
Revert comment
falahat Mar 23, 2026
028e6a3
linter
falahat Mar 23, 2026
3e9546c
Merge branch 'main' into zip_deploy_p4sa
falahat Mar 24, 2026
1bd39ca
Merge branch 'main' into zip_deploy_p4sa
falahat Mar 25, 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
75 changes: 71 additions & 4 deletions src/deploy/apphosting/prepare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import * as devconnect from "../../gcp/devConnect";
import * as prompt from "../../prompt";
import { RC } from "../../rc";
import { Context } from "./args";
import { FirebaseError } from "../../error";
import prepare, { getBackendConfigs } from "./prepare";
import * as localbuilds from "../../apphosting/localbuilds";
import * as experiments from "../../experiments";
import * as getProjectNumber from "../../getProjectNumber";
import * as resourceManager from "../../gcp/resourceManager";

const BASE_OPTS = {
cwd: "/",
Expand Down Expand Up @@ -51,6 +54,8 @@ describe("apphosting", () => {
let doSetupSourceDeployStub: sinon.SinonStub;
let listBackendsStub: sinon.SinonStub;
let getGitRepositoryLinkStub: sinon.SinonStub;
let assertEnabledStub: sinon.SinonStub;
let addServiceAccountToRolesStub: sinon.SinonStub;

beforeEach(() => {
sinon.stub(opts.config, "writeProjectFile").returns();
Expand All @@ -67,6 +72,12 @@ describe("apphosting", () => {
getGitRepositoryLinkStub = sinon
.stub(devconnect, "getGitRepositoryLink")
.throws("Unexpected getGitRepositoryLink call");
assertEnabledStub = sinon.stub(experiments, "assertEnabled").returns();
sinon.stub(experiments, "isEnabled").returns(true);
sinon.stub(getProjectNumber, "getProjectNumber").resolves("123456789");
addServiceAccountToRolesStub = sinon
.stub(resourceManager, "addServiceAccountToRoles")
.resolves();
});

afterEach(() => {
Expand Down Expand Up @@ -102,7 +113,6 @@ describe("apphosting", () => {
buildConfig,
annotations,
});
sinon.stub(experiments, "assertEnabled").returns();
listBackendsStub.onFirstCall().resolves({
backends: [
{
Expand All @@ -125,6 +135,12 @@ describe("apphosting", () => {
buildConfig,
annotations,
});
expect(addServiceAccountToRolesStub).to.have.been.calledWith(
"my-project",
apphosting.serviceAgentEmail("123456789"),
["roles/storage.objectViewer"],
true,
);
});

it("should fail if localBuild is specified but experiment is disabled", async () => {
Expand All @@ -141,9 +157,7 @@ describe("apphosting", () => {
};
const context = initializeContext();

sinon
.stub(experiments, "assertEnabled")
.throws(new Error("Experiment 'apphostinglocalbuilds' is not enabled."));
assertEnabledStub.throws(new Error("Experiment 'apphostinglocalbuilds' is not enabled."));
listBackendsStub.onFirstCall().resolves({
backends: [
{
Expand Down Expand Up @@ -266,6 +280,59 @@ describe("apphosting", () => {
});
expect(context.backendLocalBuilds["foo"]).to.undefined;
});

it("throws an error for localBuild when experiment is not enabled", async () => {
const optsWithLocalBuild = {
...opts,
config: new Config({
apphosting: {
backendId: "foo",
rootDir: "/",
ignore: [],
localBuild: true,
},
}),
};

(experiments.isEnabled as sinon.SinonStub).withArgs("apphostinglocalbuilds").returns(false);
assertEnabledStub.throws(
new FirebaseError(
"Cannot perform a local build because the experiment apphostinglocalbuilds is not enabled.",
),
);

const context = initializeContext();
listBackendsStub.resolves({
backends: [
{
name: "projects/my-project/locations/us-central1/backends/foo",
},
],
});

await expect(prepare(context, optsWithLocalBuild)).to.be.rejectedWith(
FirebaseError,
"Cannot perform a local build",
);
expect(addServiceAccountToRolesStub).to.not.have.been.called;
});

it("should succeed for source deploys even if experiment is disabled", async () => {
const context = initializeContext();
listBackendsStub.resolves({
backends: [
{
name: "projects/my-project/locations/us-central1/backends/foo",
},
],
});

// No localBuild: true in config
(experiments.isEnabled as sinon.SinonStub).withArgs("apphostinglocalbuilds").returns(false);
await prepare(context, opts);

expect(assertEnabledStub).to.not.have.been.calledWith("apphostinglocalbuilds");
});
});

describe("getBackendConfigs", () => {
Expand Down
43 changes: 41 additions & 2 deletions src/deploy/apphosting/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ import {
ensureRequiredApisEnabled,
} from "../../apphosting/backend";
import { AppHostingMultiple, AppHostingSingle } from "../../firebaseConfig";
import { ensureApiEnabled, listBackends, parseBackendName } from "../../gcp/apphosting";
import {
ensureApiEnabled,
listBackends,
parseBackendName,
serviceAgentEmail,
} from "../../gcp/apphosting";
import { getGitRepositoryLink, parseGitRepositoryLinkName } from "../../gcp/devConnect";
import { addServiceAccountToRoles } from "../../gcp/resourceManager";

import { Options } from "../../options";
import { needProjectId } from "../../projectUtils";
import { getProjectNumber } from "../../getProjectNumber";
import { checkbox, confirm } from "../../prompt";
import { logLabeledBullet, logLabeledWarning } from "../../utils";
import { localBuild } from "../../apphosting/localbuilds";
import * as experiments from "../../experiments";
import { Context } from "./args";
import { FirebaseError } from "../../error";
import * as experiments from "../../experiments";
import { logger } from "../../logger";

/**
* Prepare backend targets to deploy from source. Checks that all required APIs are enabled,
Expand All @@ -32,6 +41,11 @@ export default async function (context: Context, options: Options): Promise<void
context.backendLocalBuilds = {};

const configs = getBackendConfigs(options);
if (configs.some((cfg) => cfg.localBuild) && experiments.isEnabled("apphostinglocalbuilds")) {
const projectNumber = await getProjectNumber(options);
await ensureAppHostingServiceAgentRoles(projectId, projectNumber);
}

const { backends } = await listBackends(projectId, "-");

const foundBackends: AppHostingSingle[] = [];
Expand Down Expand Up @@ -212,3 +226,28 @@ export function getBackendConfigs(options: Options): AppHostingMultiple {
}
return backendConfigs.filter((cfg) => backendIds.includes(cfg.backendId));
}

/**
* Ensures that the App Hosting service agent has the necessary roles to access
* project resources (e.g. storage) for a given project.
*/
async function ensureAppHostingServiceAgentRoles(
projectId: string,
projectNumber: string,
): Promise<void> {
const p4saEmail = serviceAgentEmail(projectNumber);
try {
await addServiceAccountToRoles(
projectId,
p4saEmail,
["roles/storage.objectViewer"],
/* skipAccountLookup= */ true,
);
} catch (err: unknown) {
logger.debug(`Failed to grant storage.objectViewer to ${p4saEmail}: ${String(err)}`);
logLabeledWarning(
"apphosting",
`Unable to verify App Hosting service agent permissions for ${p4saEmail}. If you encounter a PERMISSION_DENIED error during rollout, please ensure the service agent has the "Storage Object Viewer" role.`,
);
}
}
Loading