Skip to content

Commit 1fadb1e

Browse files
authored
Allow apphosting:backends:create to be called with --non-interactive (#9025)
1 parent 09e5df7 commit 1fadb1e

File tree

3 files changed

+129
-40
lines changed

3 files changed

+129
-40
lines changed

src/apphosting/backend.ts

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,44 +73,63 @@ async function awaitTlsReady(url: string): Promise<void> {
7373
*/
7474
export async function doSetup(
7575
projectId: string,
76-
webAppName: string | null,
77-
serviceAccount: string | null,
76+
nonInteractive: boolean,
77+
webAppName?: string,
78+
backendId?: string,
79+
serviceAccount?: string,
80+
primaryRegion?: string,
81+
rootDir?: string,
7882
): Promise<void> {
7983
await ensureRequiredApisEnabled(projectId);
8084

8185
// Hack: Because IAM can take ~45 seconds to propagate, we provision the service account as soon as
8286
// possible to reduce the likelihood that the subsequent Cloud Build fails. See b/336862200.
83-
await ensureAppHostingComputeServiceAccount(projectId, serviceAccount);
87+
await ensureAppHostingComputeServiceAccount(projectId, serviceAccount ? serviceAccount : null);
8488

8589
// TODO(https://github.com/firebase/firebase-tools/issues/8283): The "primary region"
8690
// is still "locations" in the V1 API. This will change in the V2 API and we may need to update
8791
// the variables and API methods we're calling under the hood when fetching "primary region".
88-
const location = await promptLocation(
89-
projectId,
90-
"Select a primary region to host your backend:\n",
91-
);
92+
let location = primaryRegion;
93+
let gitRepositoryLink: GitRepositoryLink | undefined;
94+
let branch: string | undefined;
95+
if (nonInteractive) {
96+
if (!backendId || !primaryRegion) {
97+
throw new FirebaseError("nonInteractive mode requires a backendId and primaryRegion");
98+
}
99+
} else {
100+
if (!location) {
101+
location = await promptLocation(projectId, "Select a primary region to host your backend:\n");
102+
}
103+
if (!backendId) {
104+
logBullet(`${clc.yellow("===")} Set up your backend`);
105+
backendId = await promptNewBackendId(projectId, location);
106+
logSuccess(`Name set to ${backendId}\n`);
107+
}
108+
if (!rootDir) {
109+
rootDir = await input({
110+
default: "/",
111+
message: "Specify your app's root directory relative to your repository",
112+
});
113+
}
114+
115+
gitRepositoryLink = await githubConnections.linkGitHubRepository(projectId, location);
116+
// TODO: Once tag patterns are implemented, prompt which method the user
117+
// prefers. We could reduce the number of questions asked by letting people
118+
// enter tag:<pattern>?
119+
branch = await githubConnections.promptGitHubBranch(gitRepositoryLink);
120+
logSuccess(`Repo linked successfully!\n`);
121+
}
122+
// Confirm both backendId and location are set at this point
123+
if (!location || !backendId) {
124+
// This should not happen based on the logic above, but it satisfies the type checker.
125+
throw new FirebaseError("Internal error: location or backendId is not defined.");
126+
}
92127

93-
const gitRepositoryLink: GitRepositoryLink = await githubConnections.linkGitHubRepository(
128+
const webApp = await webApps.getOrCreateWebApp(
94129
projectId,
95-
location,
130+
webAppName ? webAppName : null,
131+
backendId,
96132
);
97-
98-
const rootDir = await input({
99-
default: "/",
100-
message: "Specify your app's root directory relative to your repository",
101-
});
102-
103-
// TODO: Once tag patterns are implemented, prompt which method the user
104-
// prefers. We could reduce the number of questions asked by letting people
105-
// enter tag:<pattern>?
106-
const branch = await githubConnections.promptGitHubBranch(gitRepositoryLink);
107-
logSuccess(`Repo linked successfully!\n`);
108-
109-
logBullet(`${clc.yellow("===")} Set up your backend`);
110-
const backendId = await promptNewBackendId(projectId, location);
111-
logSuccess(`Name set to ${backendId}\n`);
112-
113-
const webApp = await webApps.getOrCreateWebApp(projectId, webAppName, backendId);
114133
if (!webApp) {
115134
logWarning(`Firebase web app not set`);
116135
}
@@ -120,13 +139,23 @@ export async function doSetup(
120139
projectId,
121140
location,
122141
backendId,
123-
serviceAccount,
142+
serviceAccount ? serviceAccount : null,
124143
gitRepositoryLink,
125144
webApp?.id,
126145
rootDir,
127146
);
128147
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
129148

149+
// In non-interactive mode, we never connected the backend to a github repo. Return
150+
// early and skip the rollout and setting default traffic policy.
151+
if (nonInteractive) {
152+
return;
153+
}
154+
155+
if (!branch) {
156+
throw new FirebaseError("Branch was not set while connecting to a github repo.");
157+
}
158+
130159
await setDefaultTrafficPolicy(projectId, location, backendId, branch);
131160

132161
const confirmRollout = await confirm({
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Command } from "../command";
2+
import { FirebaseError } from "../error";
23
import { Options } from "../options";
34
import { needProjectId } from "../projectUtils";
45
import { requireAuth } from "../requireAuth";
5-
import requireInteractive from "../requireInteractive";
66
import { doSetup } from "../apphosting/backend";
77
import { ensureApiEnabled } from "../gcp/apphosting";
88
import { APPHOSTING_TOS_ID } from "../gcp/firedata";
@@ -14,19 +14,36 @@ export const command = new Command("apphosting:backends:create")
1414
"-a, --app <webAppId>",
1515
"specify an existing Firebase web app's ID to associate your App Hosting backend with",
1616
)
17+
.option(
18+
"--backend <backend>",
19+
"specify the name of the new backend. Required with --non-interactive.",
20+
)
1721
.option(
1822
"-s, --service-account <serviceAccount>",
1923
"specify the service account used to run the server",
2024
"",
2125
)
26+
.option(
27+
"--primary-region <primaryRegion>",
28+
"specify the primary region for the backend. Required with --non-interactive.",
29+
)
30+
.option("--root-dir <rootDir>", "specify the root directory for the backend.")
2231
.before(requireAuth)
2332
.before(ensureApiEnabled)
24-
.before(requireInteractive)
2533
.before(requireTosAcceptance(APPHOSTING_TOS_ID))
2634
.action(async (options: Options) => {
2735
const projectId = needProjectId(options);
28-
const webAppId = options.app;
29-
const serviceAccount = options.serviceAccount;
36+
if (options.nonInteractive && (options.backend == null || options.primaryRegion == null)) {
37+
throw new FirebaseError(`--non-interactive option requires --backend and --primary-region`);
38+
}
3039

31-
await doSetup(projectId, webAppId as string | null, serviceAccount as string | null);
40+
await doSetup(
41+
projectId,
42+
options.nonInteractive,
43+
options.app as string | undefined,
44+
options.backend as string | undefined,
45+
options.serviceAccount as string | undefined,
46+
options.primaryRegion as string | undefined,
47+
options.rootDir as string | undefined,
48+
);
3249
});

src/mcp/prompts/core/deploy.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,57 @@ ${prompt || "<the user didn't supply specific instructions>"}
4343
Follow the steps below taking note of any user instructions provided above.
4444
4545
1. If there is no active user, prompt the user to run \`firebase login\` in an interactive terminal before continuing.
46-
2. If there is no \`firebase.json\` file and the current workspace is a static web application, manually create a \`firebase.json\` with \`"hosting"\` configuration based on the current directory's web app configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
47-
3. If there is no active project, ask the user if they want to use an existing project or create a new one.
48-
3a. If create a new one, use the \`firebase_create_project\` tool.
49-
3b. If they want to use an existing one, ask them for a project id (the \`firebase_list_projects\` tool may be helpful).
50-
4. Only after making sure Firebase has been initialized, run the \`firebase deploy\` shell command to perform the deploy. This may take a few minutes.
51-
5. If the deploy has errors, attempt to fix them and ask the user clarifying questions as needed.
52-
6. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`firebase deploy --force\`.
53-
7. If only one specific feature is failing, use command \`firebase deploy --only <feature>\` as you debug.
46+
2. Analyze the source code in the current working directory to determine if this is a web app. If it isn't, end this process and tell the user "The /deploy command only works with web apps."
47+
3. Analyze the source code in the current working directory to determine if the app requires a server for Server-Side Rendering (SSR). This will determine whether or not to use Firebase App Hosting. Here are instructions to determine if the app needs a server:
48+
Objective: Analyze the provided codebase files to determine if the web application requires a backend for Server-Side Rendering (SSR). Your final output must be a clear "Yes" or "No" followed by a brief justification.
49+
Primary Analysis: package.json
50+
This is the most critical step. If the package.json file is present, perform the following checks in order.
51+
Parse package.json: Locate and read the contents of the package.json file.
52+
Check Dependencies:
53+
Examine the dependencies and devDependencies objects.
54+
If any of the following packages are listed as keys, you can conclude the app uses SSR.
55+
next
56+
nuxt
57+
@sveltejs/kit
58+
@angular/ssr
59+
remix
60+
If a match is found, proceed directly to the Final Determination step.
61+
Check Scripts: If no framework dependency was found, examine the scripts object.
62+
Look for scripts (commonly start or serve) that execute a server process.
63+
Examples include: "start": "next start", "start": "nuxt start", or "dev": "ng serve --ssr".
64+
If such a script is found, conclude the app uses SSR and proceed to the Final Determination step.
65+
Secondary Analysis: Project File Structure
66+
Perform this analysis only if package.json is missing or inconclusive.
67+
Scan for Framework-Specific Files and Directories: Search the codebase for the following patterns:
68+
Next.js: A directory named app/ or pages/. Inside these, check for files containing the function name getServerSideProps.
69+
Nuxt.js: A directory named server/.
70+
SvelteKit: Any file ending with the .server.js suffix (e.g., +page.server.js, +layout.server.js).
71+
Angular: A file named server.ts.
72+
If any of these patterns are found, conclude the app uses SSR.
73+
Final Determination
74+
State Your Conclusion: Begin your response with a definitive "Yes" or "No".
75+
Yes: The application requires a backend for SSR.
76+
No: The application does not appear to require a backend for SSR and is likely a static or client-side rendered app.
77+
Provide Justification: Follow your conclusion with a single sentence explaining the evidence.
78+
Example (Yes): "Yes, the project requires SSR, as evidenced by the next dependency in package.json."
79+
Example (Yes): "Yes, the project requires SSR, as evidenced by the presence of a +page.server.js file."
80+
Example (No): "No, there are no dependencies or file structures that indicate the use of a server-side rendering framework."
81+
4. If there is no \`firebase.json\` file, manually create one based on whether the app requires SSR:
82+
4a. If the app requires SSR, configure Firebase App Hosting:
83+
Create \`firebase.json\ with an "apphosting" configuration, setting backendId to the app's name in package.json: \`{"apphosting": {"backendId": "<backendId>"}}\
84+
4b. If the app does NOT require SSR, configure Firebase Hosting:
85+
Create \`firebase.json\ with a "hosting" configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
86+
5. Check if there is an active Firebase project for this environment (the \`firebase_get_environment\` tool may be helpful). If there is, proceed using that project. If there is not an active project, give the user two options: Use an existing Firebase project or Create a new one. Wait for their response before proceeding.
87+
5a. If the user chooses to use an existing Firebase project, the \`firebase_list_projects\` tool may be helpful. Set the selected project as the active project (the \`firebase_update_environment\` tool may be helpful).
88+
5b. If the user chooses to create a new project, use the \`firebase_create_project \` tool. Then set the new project as the active project (the \`firebase_update_environment\` tool may be helpful).
89+
6. If firebase.json contains an "apphosting" configuration, check if a backend exists matching the provided backendId (the \`apphosting_list_backends\` tool may be helpful).
90+
If it doesn't exist, create one by running the \`firebase apphosting:backends:create --backend <backendId> --primary-region us-central1 --root-dir .\` shell.
91+
7. Only after making sure Firebase has been initialized, run the \`firebase deploy\` shell command to perform the deploy. This may take a few minutes.
92+
7a. If deploying to apphosting, tell the user the deployment will take a few minutes, and they can monitor deployment progress in the Firebase console: \`https://console.firebase.google.com/project/<projectId>/apphosting\`
93+
8. If the deploy has errors, attempt to fix them and ask the user clarifying questions as needed.
94+
9. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`firebase deploy --force\`.
95+
10. If only one specific feature is failing, use command \`firebase deploy --only <feature>\` as you debug.
96+
11. If the deploy succeeds, your job is finished.
5497
`.trim(),
5598
},
5699
},

0 commit comments

Comments
 (0)