Skip to content

Commit 702c1c0

Browse files
authored
Enhance firebase init apphosting to support local source deploys (#8479)
* expand init apphosting to modify firebase.json, optionally create backend * regen schema * minor fixes & use fuzzy search for backends
1 parent b9835ac commit 702c1c0

File tree

10 files changed

+302
-19
lines changed

10 files changed

+302
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
- Changed artifact registry cleanup policy error to warn for CI/CD workloads #8513
2+
- Enhance firebase init apphosting to support local source deploys. (#8479)

schema/firebase-config.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,65 @@
983983
"format": "uri",
984984
"type": "string"
985985
},
986+
"apphosting": {
987+
"anyOf": [
988+
{
989+
"additionalProperties": false,
990+
"properties": {
991+
"alwaysDeployFromSource": {
992+
"type": "boolean"
993+
},
994+
"backendId": {
995+
"type": "string"
996+
},
997+
"ignore": {
998+
"items": {
999+
"type": "string"
1000+
},
1001+
"type": "array"
1002+
},
1003+
"rootDir": {
1004+
"type": "string"
1005+
}
1006+
},
1007+
"required": [
1008+
"backendId",
1009+
"ignore",
1010+
"rootDir"
1011+
],
1012+
"type": "object"
1013+
},
1014+
{
1015+
"items": {
1016+
"additionalProperties": false,
1017+
"properties": {
1018+
"alwaysDeployFromSource": {
1019+
"type": "boolean"
1020+
},
1021+
"backendId": {
1022+
"type": "string"
1023+
},
1024+
"ignore": {
1025+
"items": {
1026+
"type": "string"
1027+
},
1028+
"type": "array"
1029+
},
1030+
"rootDir": {
1031+
"type": "string"
1032+
}
1033+
},
1034+
"required": [
1035+
"backendId",
1036+
"ignore",
1037+
"rootDir"
1038+
],
1039+
"type": "object"
1040+
},
1041+
"type": "array"
1042+
}
1043+
]
1044+
},
9861045
"database": {
9871046
"anyOf": [
9881047
{

src/apphosting/backend.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ describe("apphosting setup functions", () => {
106106
projectId,
107107
location,
108108
backendId,
109-
cloudBuildConnRepo,
110109
"custom-service-account",
110+
cloudBuildConnRepo,
111111
webAppId,
112112
);
113113

src/apphosting/backend.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Backend, BackendOutputOnlyFields, API_VERSION } from "../gcp/apphosting
1717
import { addServiceAccountToRoles } from "../gcp/resourceManager";
1818
import * as iam from "../gcp/iam";
1919
import { FirebaseError, getErrStatus, getError } from "../error";
20-
import { input, confirm, select, checkbox } from "../prompt";
20+
import { input, confirm, select, checkbox, search, Choice } from "../prompt";
2121
import { DEFAULT_LOCATION } from "./constants";
2222
import { ensure } from "../ensureApiEnabled";
2323
import * as deploymentTool from "../deploymentTool";
@@ -27,6 +27,7 @@ import { GitRepositoryLink } from "../gcp/devConnect";
2727
import * as ora from "ora";
2828
import fetch from "node-fetch";
2929
import { orchestrateRollout } from "./rollout";
30+
import * as fuzzy from "fuzzy";
3031

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

@@ -126,8 +127,8 @@ export async function doSetup(
126127
projectId,
127128
location,
128129
backendId,
129-
gitRepositoryLink,
130130
serviceAccount,
131+
gitRepositoryLink,
131132
webApp?.id,
132133
rootDir,
133134
);
@@ -246,7 +247,7 @@ export async function ensureAppHostingComputeServiceAccount(
246247
/**
247248
* Prompts the user for a backend id and verifies that it doesn't match a pre-existing backend.
248249
*/
249-
async function promptNewBackendId(projectId: string, location: string): Promise<string> {
250+
export async function promptNewBackendId(projectId: string, location: string): Promise<string> {
250251
while (true) {
251252
const backendId = await input({
252253
default: "my-web-app",
@@ -280,18 +281,20 @@ export async function createBackend(
280281
projectId: string,
281282
location: string,
282283
backendId: string,
283-
repository: GitRepositoryLink,
284284
serviceAccount: string | null,
285+
repository: GitRepositoryLink | undefined,
285286
webAppId: string | undefined,
286287
rootDir = "/",
287288
): Promise<Backend> {
288289
const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
289290
const backendReqBody: Omit<Backend, BackendOutputOnlyFields> = {
290291
servingLocality: "GLOBAL_ACCESS",
291-
codebase: {
292-
repository: `${repository.name}`,
293-
rootDirectory: rootDir,
294-
},
292+
codebase: repository
293+
? {
294+
repository: `${repository.name}`,
295+
rootDirectory: rootDir,
296+
}
297+
: undefined,
295298
labels: deploymentTool.labels(),
296299
serviceAccount: serviceAccount || defaultServiceAccount,
297300
appId: webAppId,
@@ -385,11 +388,11 @@ export async function promptLocation(
385388
return allowedLocations[0];
386389
}
387390

388-
const location = (await select<string>({
391+
const location = await select<string>({
389392
default: DEFAULT_LOCATION,
390393
message: prompt,
391394
choices: allowedLocations,
392-
})) as string;
395+
});
393396

394397
logSuccess(`Location set to ${location}.\n`);
395398

@@ -413,6 +416,39 @@ export async function getBackendForLocation(
413416
}
414417
}
415418

419+
/**
420+
* Prompts users to select an existing backend.
421+
* @param projectId the user's project ID
422+
* @param promptMessage prompt message to display to the user
423+
* @return the selected backend ID
424+
*/
425+
export async function promptExistingBackend(
426+
projectId: string,
427+
promptMessage: string,
428+
): Promise<string> {
429+
const { backends } = await apphosting.listBackends(projectId, "-");
430+
const backendId: string = await search({
431+
message: promptMessage,
432+
source: (input = ""): Promise<Choice<string>[]> => {
433+
return new Promise((resolve) =>
434+
resolve([
435+
...fuzzy
436+
.filter(input, backends, {
437+
extract: (backend) => apphosting.parseBackendName(backend.name).id,
438+
})
439+
.map((result) => {
440+
return {
441+
name: apphosting.parseBackendName(result.original.name).id,
442+
value: apphosting.parseBackendName(result.original.name).id,
443+
};
444+
}),
445+
]),
446+
);
447+
},
448+
});
449+
return backendId;
450+
}
451+
416452
/**
417453
* Fetches backends of the given backendId and lets the user choose if more than one is found.
418454
*/

src/apphosting/rollout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export async function createRollout(
3737
): Promise<void> {
3838
const backend = await getBackend(projectId, backendId);
3939

40-
if (!backend.codebase.repository) {
40+
if (!backend.codebase || !backend.codebase.repository) {
4141
throw new FirebaseError(
42-
`Backend ${backendId} is misconfigured due to missing a connected repository. You can delete and recreate your backend using 'firebase apphosting:backends:delete' and 'firebase apphosting:backends:create'.`,
42+
`Backend ${backendId} is missing a connected repository. If you would like to deploy from a branch or commit of a GitHub repository, you can connect one through the Firebase Console. If you would like to deploy from local source, run 'firebase deploy'.`,
4343
);
4444
}
4545

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class Config {
3333
"storage",
3434
"remoteconfig",
3535
"dataconnect",
36+
"apphosting",
3637
];
3738

3839
public options: any;

src/firebaseConfig.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,17 @@ export type DataConnectMultiple = DataConnectSingle[];
264264

265265
export type DataConnectConfig = DataConnectSingle | DataConnectMultiple;
266266

267+
export type AppHostingSingle = {
268+
backendId: string;
269+
rootDir: string;
270+
ignore: string[];
271+
alwaysDeployFromSource?: boolean;
272+
};
273+
274+
export type AppHostingMultiple = AppHostingSingle[];
275+
276+
export type AppHostingConfig = AppHostingSingle | AppHostingMultiple;
277+
267278
export type FirebaseConfig = {
268279
/**
269280
* @TJS-format uri
@@ -278,4 +289,5 @@ export type FirebaseConfig = {
278289
emulators?: EmulatorsConfig;
279290
extensions?: ExtensionsConfig;
280291
dataconnect?: DataConnectConfig;
292+
apphosting?: AppHostingConfig;
281293
};

src/gcp/apphosting.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type ServingLocality = "GLOBAL_ACCESS" | "REGIONAL_STRICT";
3434
export interface Backend {
3535
name: string;
3636
mode?: string;
37-
codebase: Codebase;
37+
codebase?: Codebase;
3838
servingLocality: ServingLocality;
3939
labels: Record<string, string>;
4040
createTime: string;

src/init/features/apphosting.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { Config } from "../../config";
4+
import { upsertAppHostingConfig } from "./apphosting";
5+
6+
describe("apphosting", () => {
7+
afterEach(() => {
8+
sinon.verifyAndRestore();
9+
});
10+
11+
describe("upsertAppHostingConfig", () => {
12+
it("creates App Hosting section in firebase.json if no previous config exists", () => {
13+
const config = new Config({}, { projectDir: "test", cwd: "test" });
14+
const backendConfig = {
15+
backendId: "my-backend",
16+
rootDir: "/",
17+
ignore: [],
18+
};
19+
20+
upsertAppHostingConfig(backendConfig, config);
21+
22+
expect(config.src.apphosting).to.deep.equal(backendConfig);
23+
});
24+
25+
it("converts App Hosting config into array when going from one backend to two", () => {
26+
const existingBackendConfig = {
27+
backendId: "my-backend",
28+
rootDir: "/",
29+
ignore: [],
30+
};
31+
const config = new Config(
32+
{ apphosting: existingBackendConfig },
33+
{ projectDir: "test", cwd: "test" },
34+
);
35+
const newBackendConfig = {
36+
backendId: "my-backend-1",
37+
rootDir: "/",
38+
ignore: [],
39+
};
40+
41+
upsertAppHostingConfig(newBackendConfig, config);
42+
43+
expect(config.src.apphosting).to.deep.equal([existingBackendConfig, newBackendConfig]);
44+
});
45+
46+
it("appends backend config to array if there is already an array", () => {
47+
const appHostingConfig = [
48+
{
49+
backendId: "my-backend-0",
50+
rootDir: "/",
51+
ignore: [],
52+
},
53+
{
54+
backendId: "my-backend-1",
55+
rootDir: "/",
56+
ignore: [],
57+
},
58+
];
59+
const config = new Config(
60+
{ apphosting: appHostingConfig },
61+
{ projectDir: "test", cwd: "test" },
62+
);
63+
const newBackendConfig = {
64+
backendId: "my-backend-2",
65+
rootDir: "/",
66+
ignore: [],
67+
};
68+
69+
upsertAppHostingConfig(newBackendConfig, config);
70+
71+
expect(config.src.apphosting).to.deep.equal([...appHostingConfig, newBackendConfig]);
72+
});
73+
});
74+
}).timeout(5000);

0 commit comments

Comments
 (0)