Skip to content

Add support for deploying local source to App Hosting #8516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
944ab1f
deploy from local source
blidd-google May 7, 2025
a0f00ff
minor fixes & use fuzzy search for backends
blidd-google May 9, 2025
1588252
add storage.objectViewer role on compute SA
blidd-google May 9, 2025
f7be953
respond to first round of feedback
blidd-google May 12, 2025
b5519ac
second round of feedback
blidd-google May 12, 2025
e344fae
Fix VSCode import error (#8546)
joehan May 12, 2025
68caf08
Add GCP API client functions to support App Hosting deploy from sourc…
blidd-google May 12, 2025
588b11f
fix(mcp): Make all input schemas valid for Gemini. (#8537)
mbleigh May 12, 2025
8cc675f
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 12, 2025
219b901
log source code upload path
blidd-google May 12, 2025
cbe7ffe
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 12, 2025
908eb34
simplify gitignore parsing
blidd-google May 12, 2025
d771ce8
fix bug not deploying with no --only flag
blidd-google May 12, 2025
affa033
support negation rules in .gitignore
blidd-google May 13, 2025
bc0cfbf
clean up readdirrecursive logic
blidd-google May 13, 2025
41ca746
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 13, 2025
f0eefcc
polish & cleanup
blidd-google May 13, 2025
f84ff77
refactor to use 'ignore' pkg to apply .gitignore rules
blidd-google May 14, 2025
9fe4133
fix gitignore unit test
blidd-google May 14, 2025
d019b57
rework prepare flow and polish copy
blidd-google May 15, 2025
93db1a6
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 15, 2025
e72954e
fix merge issues
blidd-google May 15, 2025
57b9d4c
more polish & cleanup
blidd-google May 15, 2025
23c3e2f
Merge branch 'master' into bl-fah-deploy-from-source
Yuangwang May 15, 2025
d6e37bc
skip not found backends with force
blidd-google May 15, 2025
611dc97
return empty array if git ignore doesn't exist
blidd-google May 15, 2025
1526d3d
return empty array if no gitignore found
blidd-google May 15, 2025
a999b7a
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 15, 2025
09bcac9
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 15, 2025
df7042d
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 15, 2025
f8cce50
Merge branch 'master' into bl-fah-deploy-from-source
blidd-google May 15, 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
77 changes: 69 additions & 8 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"gaxios": "^6.7.0",
"glob": "^10.4.1",
"google-auth-library": "^9.11.0",
"ignore": "^7.0.4",
"js-yaml": "^3.14.1",
"jsonwebtoken": "^9.0.0",
"leven": "^3.1.0",
Expand Down
51 changes: 50 additions & 1 deletion src/apphosting/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
secretManagerOrigin,
} from "../api";
import { Backend, BackendOutputOnlyFields, API_VERSION } from "../gcp/apphosting";
import { addServiceAccountToRoles } from "../gcp/resourceManager";
import { addServiceAccountToRoles, getIamPolicy } from "../gcp/resourceManager";
import * as iam from "../gcp/iam";
import { FirebaseError, getErrStatus, getError } from "../error";
import { input, confirm, select, checkbox, search, Choice } from "../prompt";
Expand Down Expand Up @@ -49,7 +49,7 @@
// SSL.
const maybeNodeError = err as { cause: { code: string }; code: string };
if (
/HANDSHAKE_FAILURE/.test(maybeNodeError?.cause?.code) ||

Check warning on line 52 in src/apphosting/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Use `String#includes()` method with a string instead
"EPROTO" === maybeNodeError?.code
) {
return false;
Expand Down Expand Up @@ -172,6 +172,30 @@
logSuccess(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
}

/**
* Setup up a new App Hosting backend to deploy from source.
*/
export async function doSetupSourceDeploy(
projectId: string,
backendId: string,
): Promise<{ backend: Backend; location: string }> {
const location = await promptLocation(
projectId,
"Select a primary region to host your backend:\n",
);
const webApp = await webApps.getOrCreateWebApp(projectId, null, backendId);
if (!webApp) {
logWarning(`Firebase web app not set`);
}
const createBackendSpinner = ora("Creating your new backend...").start();
const backend = await createBackend(projectId, location, backendId, null, undefined, webApp?.id);
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
return {
backend,
location,
};
}

/**
* Check that all GCP APIs required for App Hosting are enabled.
*/
Expand Down Expand Up @@ -225,6 +249,7 @@
export async function ensureAppHostingComputeServiceAccount(
projectId: string,
serviceAccount: string | null,
deployFromSource = false,
): Promise<void> {
const sa = serviceAccount || defaultComputeServiceAccountEmail(projectId);
const name = `projects/${projectId}/serviceAccounts/${sa}`;
Expand All @@ -249,13 +274,36 @@
);
}
}
// N.B. To deploy from source, the App Hosting Compute Service Account must have
// the storage.objectViewer IAM role. For firebase-tools <= 14.3.0, the CLI does
// not add the objectViewer role, which means all existing customers will need to
// add it before deploying from source.
if (deployFromSource) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a comment in the code explaining why this new roles/storage.objectViewer stuff is necessary

Copy link
Contributor

Choose a reason for hiding this comment

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

^ for better code comprehension e.g. letting reader know that deploy from source has the source code bundled in storage buckets so we need access to that bucket.

but also, now I see below that roles/storage.objectViewer is also added in provisionDefaultComputeServiceAccount() below, which is called in this ensureAppHostingComputeServiceAccount()

and I'm not sure why?

either there's a better way to do it to make things more clear natively or let's document what we're doing and explain any weird/confusing stuff.

const policy = await getIamPolicy(projectId);
const objectViewerBinding = policy.bindings.find(
(binding) => binding.role === "roles/storage.objectViewer",
);
if (
!objectViewerBinding ||
!objectViewerBinding.members.includes(
`serviceAccount:${defaultComputeServiceAccountEmail(projectId)}`,
)
) {
await addServiceAccountToRoles(
projectId,
defaultComputeServiceAccountEmail(projectId),
["roles/storage.objectViewer"],
/* skipAccountLookup= */ true,
);
}
}
}

/**
* Prompts the user for a backend id and verifies that it doesn't match a pre-existing backend.
*/
export async function promptNewBackendId(projectId: string, location: string): Promise<string> {
while (true) {

Check warning on line 306 in src/apphosting/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected constant condition
const backendId = await input({
default: "my-web-app",
message: "Provide a name for your backend [1-30 characters]",
Expand Down Expand Up @@ -340,6 +388,7 @@
"roles/firebaseapphosting.computeRunner",
"roles/firebase.sdkAdminServiceAgent",
"roles/developerconnect.readTokenAccessor",
"roles/storage.objectViewer",
],
/* skipAccountLookup= */ true,
);
Expand Down Expand Up @@ -553,7 +602,7 @@
message: locationDisambugationPrompt,
choices: [...backendsByLocation.keys()],
});
return backendsByLocation.get(location)!;

Check warning on line 605 in src/apphosting/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/checkValidTargetFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const FILTERABLE_TARGETS = new Set([
"storage",
"database",
"dataconnect",
"apphosting",
]);

/**
Expand Down
1 change: 1 addition & 0 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"remoteconfig",
"extensions",
"dataconnect",
"apphosting",
];
export const TARGET_PERMISSIONS: Record<(typeof VALID_DEPLOY_TARGETS)[number], string[]> = {
Copy link
Contributor

Choose a reason for hiding this comment

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

does apphosting require any permissions we need to check for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added calls with ensureApiEnabled to check if App Hosting API is enabled. Double checking with team which specific IAM permissions are required

database: ["firebasedatabase.instances.update"],
Expand Down Expand Up @@ -98,14 +99,14 @@
)
.before(requireConfig)
.before((options) => {
options.filteredTargets = filterTargets(options, VALID_DEPLOY_TARGETS);

Check warning on line 102 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .filteredTargets on an `any` value

Check warning on line 102 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Options`
const permissions = options.filteredTargets.reduce((perms: string[], target: string) => {

Check warning on line 103 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

Check warning on line 103 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .filteredTargets on an `any` value

Check warning on line 103 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value
return perms.concat(TARGET_PERMISSIONS[target]);
}, []);
return requirePermissions(options, permissions);

Check warning on line 106 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `string[]`
})
.before((options) => {
if (options.filteredTargets.includes("functions")) {

Check warning on line 109 in src/commands/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .filteredTargets on an `any` value
return checkServiceAccountIam(options.project);
}
})
Expand Down
7 changes: 7 additions & 0 deletions src/deploy/apphosting/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AppHostingSingle } from "../../firebaseConfig";

export interface Context {
backendConfigs: Map<string, AppHostingSingle>;
backendLocations: Map<string, string>;
backendStorageUris: Map<string, string>;
}
Loading
Loading