firebase-tools 13.2.1 → 13.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/auth.js CHANGED
@@ -307,7 +307,7 @@ async function loginRemotely() {
307
307
  const tokens = await getTokensFromAuthorizationCode(code, `${api_1.authProxyOrigin}/complete`, codeVerifier);
308
308
  void (0, track_1.trackGA4)("login", { method: "google_remote" });
309
309
  return {
310
- user: jwt.decode(tokens.id_token),
310
+ user: jwt.decode(tokens.id_token, { json: true }),
311
311
  tokens: tokens,
312
312
  scopes: SCOPES,
313
313
  };
@@ -323,7 +323,7 @@ async function loginWithLocalhostGoogle(port, userHint) {
323
323
  const tokens = await loginWithLocalhost(port, callbackUrl, authUrl, successTemplate, getTokensFromAuthorizationCode);
324
324
  void (0, track_1.trackGA4)("login", { method: "google_localhost" });
325
325
  return {
326
- user: jwt.decode(tokens.id_token),
326
+ user: jwt.decode(tokens.id_token, { json: true }),
327
327
  tokens: tokens,
328
328
  scopes: tokens.scopes,
329
329
  };
@@ -477,6 +477,15 @@ async function refreshTokens(refreshToken, authScopes) {
477
477
  skipLog: { body: true, queryParams: true, resBody: true },
478
478
  resolveOnHTTPError: true,
479
479
  });
480
+ const forceReauthErrs = [
481
+ { error: "invalid_grant", error_subtype: "invalid_rapt" },
482
+ ];
483
+ const matches = (a, b) => {
484
+ return a.error === b.error && a.error_subtype === b.error_subtype;
485
+ };
486
+ if (forceReauthErrs.some((a) => matches(a, res.body))) {
487
+ throw invalidCredentialError();
488
+ }
480
489
  if (res.status === 401 || res.status === 400) {
481
490
  return { access_token: refreshToken };
482
491
  }
@@ -9,9 +9,12 @@ const apphosting_2 = require("../gcp/apphosting");
9
9
  exports.command = new command_1.Command("apphosting:backends:create")
10
10
  .description("Create a backend in a Firebase project")
11
11
  .option("-l, --location <location>", "Specify the region of the backend", "")
12
+ .option("-s, --service-account <serviceAccount>", "Specify the service account used to run the server", "")
12
13
  .before(apphosting_2.ensureApiEnabled)
13
14
  .before(requireInteractive_1.default)
14
15
  .action(async (options) => {
15
16
  const projectId = (0, projectUtils_1.needProjectId)(options);
16
- await (0, apphosting_1.doSetup)(options, projectId);
17
+ const location = options.location;
18
+ const serviceAccount = options.serviceAccount;
19
+ await (0, apphosting_1.doSetup)(projectId, location, serviceAccount);
17
20
  });
@@ -19,16 +19,14 @@ const TABLE_HEAD = [
19
19
  "Created Date",
20
20
  "Updated Date",
21
21
  ];
22
- exports.command = new command_1.Command("apphosting:backends:delete")
23
- .description("Delete a backend from a Firebase project")
22
+ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
23
+ .description("delete a backend from a Firebase project")
24
24
  .option("-l, --location <location>", "App Backend location", "")
25
- .option("-s, --backend <backend>", "Backend Id", "")
26
25
  .withForce()
27
26
  .before(apphosting.ensureApiEnabled)
28
- .action(async (options) => {
27
+ .action(async (backendId, options) => {
29
28
  const projectId = (0, projectUtils_1.needProjectId)(options);
30
29
  let location = options.location;
31
- const backendId = options.backend;
32
30
  if (!backendId) {
33
31
  throw new error_1.FirebaseError("Backend id can't be empty.");
34
32
  }
@@ -14,7 +14,7 @@ exports.command = new command_1.Command("apphosting:backends:list")
14
14
  .option("-l, --location <location>", "App Backend location", "-")
15
15
  .before(apphosting.ensureApiEnabled)
16
16
  .action(async (options) => {
17
- var _a, _b, _c;
17
+ var _a, _b, _c, _d;
18
18
  const projectId = (0, projectUtils_1.needProjectId)(options);
19
19
  const location = options.location;
20
20
  const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
@@ -25,12 +25,12 @@ exports.command = new command_1.Command("apphosting:backends:list")
25
25
  catch (err) {
26
26
  throw new error_1.FirebaseError(`Unable to list backends present for project: ${projectId}. Please check the parameters you have provided.`, { original: err });
27
27
  }
28
- const backends = backendRes === null || backendRes === void 0 ? void 0 : backendRes.backends;
28
+ const backends = (_a = backendRes.backends) !== null && _a !== void 0 ? _a : [];
29
29
  for (const backend of backends) {
30
30
  const [backendLocation, , backendId] = backend.name.split("/").slice(3, 6);
31
31
  table.push([
32
32
  backendId,
33
- (_c = (_b = (_a = backend.codebase) === null || _a === void 0 ? void 0 : _a.repository) === null || _b === void 0 ? void 0 : _b.split("/").pop()) !== null && _c !== void 0 ? _c : "",
33
+ (_d = (_c = (_b = backend.codebase) === null || _b === void 0 ? void 0 : _b.repository) === null || _c === void 0 ? void 0 : _c.split("/").pop()) !== null && _d !== void 0 ? _d : "",
34
34
  backendLocation,
35
35
  backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
36
36
  (0, utils_1.datetimeString)(new Date(backend.createTime)),
@@ -12,15 +12,16 @@ exports.command = new command_1.Command("apphosting:builds:create <backendId>")
12
12
  .option("-b, --branch <branch>", "Repository branch to deploy. Defaults to 'main'", "main")
13
13
  .before(apphosting.ensureApiEnabled)
14
14
  .action(async (backendId, options) => {
15
+ var _a;
15
16
  const projectId = (0, projectUtils_1.needProjectId)(options);
16
17
  const location = options.location;
17
18
  const buildId = options.buildId ||
18
19
  (await apphosting.getNextRolloutId(projectId, location, backendId));
19
- const branch = options.branch;
20
+ const branch = (_a = options.branch) !== null && _a !== void 0 ? _a : "main";
20
21
  const op = await apphosting.createBuild(projectId, location, backendId, buildId, {
21
22
  source: {
22
23
  codebase: {
23
- branch: "main",
24
+ branch,
24
25
  },
25
26
  },
26
27
  });
@@ -144,7 +144,7 @@ function load(client) {
144
144
  client.internaltesting.functions = {};
145
145
  client.internaltesting.functions.discover = loadCommand("internaltesting-functions-discover");
146
146
  }
147
- if (experiments.isEnabled("internalframeworks")) {
147
+ if (experiments.isEnabled("apphosting")) {
148
148
  client.apphosting = {};
149
149
  client.apphosting.backends = {};
150
150
  client.apphosting.backends.list = loadCommand("apphosting-backends-list");
@@ -69,10 +69,10 @@ const choices = [
69
69
  checked: false,
70
70
  },
71
71
  ];
72
- if ((0, experiments_1.isEnabled)("internalframeworks")) {
72
+ if ((0, experiments_1.isEnabled)("apphosting")) {
73
73
  choices.push({
74
- value: "internalframeworks",
75
- name: "Frameworks: Get started with Frameworks projects.",
74
+ value: "apphosting",
75
+ name: "App Hosting: Get started with App Hosting projects.",
76
76
  checked: false,
77
77
  });
78
78
  }
@@ -14,6 +14,7 @@ const RUNTIMES = [
14
14
  "nodejs20",
15
15
  "python310",
16
16
  "python311",
17
+ "python312",
17
18
  ];
18
19
  const EXPERIMENTAL_RUNTIMES = [];
19
20
  const DEPRECATED_RUNTIMES = ["nodejs6", "nodejs8"];
@@ -36,6 +37,7 @@ const MESSAGE_FRIENDLY_RUNTIMES = {
36
37
  nodejs20: "Node.js 20",
37
38
  python310: "Python 3.10",
38
39
  python311: "Python 3.11",
40
+ python312: "Python 3.12",
39
41
  };
40
42
  function getHumanFriendlyRuntimeName(runtime) {
41
43
  return MESSAGE_FRIENDLY_RUNTIMES[runtime] || runtime;
@@ -11,7 +11,7 @@ const discovery = require("../discovery");
11
11
  const logger_1 = require("../../../../logger");
12
12
  const python_1 = require("../../../../functions/python");
13
13
  const error_1 = require("../../../../error");
14
- exports.LATEST_VERSION = "python311";
14
+ exports.LATEST_VERSION = "python312";
15
15
  async function tryCreateDelegate(context) {
16
16
  const requirementsTextPath = path.join(context.sourceDir, "requirements.txt");
17
17
  if (!(await (0, util_1.promisify)(fs.exists)(requirementsTextPath))) {
@@ -35,6 +35,9 @@ function getPythonBinary(runtime) {
35
35
  else if (runtime === "python311") {
36
36
  return "python3.11";
37
37
  }
38
+ else if (runtime === "python312") {
39
+ return "python3.12";
40
+ }
38
41
  return "python";
39
42
  }
40
43
  exports.getPythonBinary = getPythonBinary;
@@ -9,7 +9,7 @@ async function getDatabase(project, databaseId) {
9
9
  if (dbCache.has(key)) {
10
10
  return dbCache.get(key);
11
11
  }
12
- const db = await firestore.getDatabase(project, databaseId);
12
+ const db = await firestore.getDatabase(project, databaseId, false);
13
13
  dbCache.set(key, db);
14
14
  return db;
15
15
  }
@@ -23,9 +23,9 @@ const EMULATOR_UPDATE_DETAILS = {
23
23
  expectedChecksum: "2fd771101c0e1f7898c04c9204f2ce63",
24
24
  },
25
25
  firestore: {
26
- version: "1.18.2",
27
- expectedSize: 63929486,
28
- expectedChecksum: "7b066cd684baf9bcd4a56a258be344a5",
26
+ version: "1.19.1",
27
+ expectedSize: 67187672,
28
+ expectedChecksum: "859b1ac2a6040cccddd993c43586347c",
29
29
  },
30
30
  storage: {
31
31
  version: "1.1.3",
@@ -331,7 +331,7 @@ function createAuthExpressionValue(opts) {
331
331
  return toExpressionValue(null);
332
332
  }
333
333
  else {
334
- const tokenPayload = jwt.decode(opts.token);
334
+ const tokenPayload = jwt.decode(opts.token, { json: true });
335
335
  const jsonValue = {
336
336
  uid: tokenPayload.user_id,
337
337
  token: tokenPayload,
@@ -72,9 +72,9 @@ exports.ALL_EXPERIMENTS = experiments({
72
72
  "These commands are not meant for public consumption and may break or disappear " +
73
73
  "without a notice.",
74
74
  },
75
- internalframeworks: {
75
+ apphosting: {
76
76
  shortDescription: "Allow CLI option for Frameworks",
77
- default: true,
77
+ default: false,
78
78
  public: false,
79
79
  },
80
80
  });
@@ -219,7 +219,7 @@ class FirestoreDelete {
219
219
  }
220
220
  numPendingDeletes++;
221
221
  firestore
222
- .deleteDocuments(this.project, toDelete)
222
+ .deleteDocuments(this.project, toDelete, true)
223
223
  .then((numDeleted) => {
224
224
  FirestoreDelete.progressBar.tick(numDeleted);
225
225
  numDocsDeleted += numDeleted;
@@ -280,7 +280,7 @@ class FirestoreDelete {
280
280
  let initialDelete;
281
281
  if (this.isDocumentPath) {
282
282
  const doc = { name: this.root + "/" + this.path };
283
- initialDelete = firestore.deleteDocument(doc).catch((err) => {
283
+ initialDelete = firestore.deleteDocument(doc, true).catch((err) => {
284
284
  logger_1.logger.debug("deletePath:initialDelete:error", err);
285
285
  if (this.allDescendants) {
286
286
  return Promise.resolve();
@@ -297,7 +297,7 @@ class FirestoreDelete {
297
297
  }
298
298
  deleteDatabase() {
299
299
  return firestore
300
- .listCollectionIds(this.project)
300
+ .listCollectionIds(this.project, true)
301
301
  .catch((err) => {
302
302
  logger_1.logger.debug("deleteDatabase:listCollectionIds:error", err);
303
303
  return utils.reject("Unable to list collection IDs");
@@ -8,6 +8,7 @@ const api_1 = require("../api");
8
8
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
9
9
  const deploymentTool = require("../deploymentTool");
10
10
  const error_1 = require("../error");
11
+ const metaprogramming_1 = require("../metaprogramming");
11
12
  exports.API_HOST = new URL(api_1.apphostingOrigin).host;
12
13
  exports.API_VERSION = "v1alpha";
13
14
  exports.client = new apiv2_1.Client({
@@ -15,6 +16,10 @@ exports.client = new apiv2_1.Client({
15
16
  auth: true,
16
17
  apiVersion: exports.API_VERSION,
17
18
  });
19
+ (0, metaprogramming_1.assertImplements)();
20
+ (0, metaprogramming_1.assertImplements)();
21
+ (0, metaprogramming_1.assertImplements)();
22
+ (0, metaprogramming_1.assertImplements)();
18
23
  async function createBackend(projectId, location, backendReqBoby, backendId) {
19
24
  const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends`, Object.assign(Object.assign({}, backendReqBoby), { labels: Object.assign(Object.assign({}, backendReqBoby.labels), deploymentTool.labels()) }), { queryParams: { backendId } });
20
25
  return res.body;
@@ -68,8 +73,8 @@ async function createBuild(projectId, location, backendId, buildId, buildInput)
68
73
  return res.body;
69
74
  }
70
75
  exports.createBuild = createBuild;
71
- async function createRollout(projectId, location, backendId, rolloutId, rollout) {
72
- const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`, Object.assign(Object.assign({}, rollout), { labels: Object.assign(Object.assign({}, rollout.labels), deploymentTool.labels()) }), { queryParams: { rolloutId } });
76
+ async function createRollout(projectId, location, backendId, rolloutId, rollout, validateOnly = false) {
77
+ const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`, Object.assign(Object.assign({}, rollout), { labels: Object.assign(Object.assign({}, rollout.labels), deploymentTool.labels()) }), { queryParams: { rolloutId, validateOnly: validateOnly ? "true" : "false" } });
73
78
  return res.body;
74
79
  }
75
80
  exports.createRollout = createRollout;
@@ -92,11 +97,7 @@ async function listRollouts(projectId, location, backendId) {
92
97
  }
93
98
  exports.listRollouts = listRollouts;
94
99
  async function updateTraffic(projectId, location, backendId, traffic) {
95
- const trafficCopy = Object.assign({}, traffic);
96
- if ("rolloutPolicy" in traffic) {
97
- trafficCopy.rolloutPolicy = {};
98
- }
99
- const fieldMasks = proto.fieldMasks(trafficCopy);
100
+ const fieldMasks = proto.fieldMasks(traffic, "rolloutPolicy");
100
101
  const queryParams = {
101
102
  updateMask: fieldMasks.join(","),
102
103
  };
@@ -125,7 +126,7 @@ async function listLocations(projectId) {
125
126
  exports.listLocations = listLocations;
126
127
  async function ensureApiEnabled(options) {
127
128
  const projectId = (0, projectUtils_1.needProjectId)(options);
128
- return await (0, ensureApiEnabled_1.ensure)(projectId, exports.API_HOST, "frameworks", true);
129
+ return await (0, ensureApiEnabled_1.ensure)(projectId, exports.API_HOST, "app hosting", true);
129
130
  }
130
131
  exports.ensureApiEnabled = ensureApiEnabled;
131
132
  async function getNextRolloutId(projectId, location, backendId, counter) {
@@ -133,7 +134,7 @@ async function getNextRolloutId(projectId, location, backendId, counter) {
133
134
  const date = new Date();
134
135
  const year = date.getUTCFullYear();
135
136
  const month = String(date.getUTCMonth() + 1).padStart(2, "0");
136
- const day = String(date.getUTCDay()).padStart(2, "0");
137
+ const day = String(date.getUTCDate()).padStart(2, "0");
137
138
  if (counter) {
138
139
  return `build-${year}-${month}-${day}-${String(counter).padStart(3, "0")}`;
139
140
  }
@@ -46,9 +46,14 @@ async function deleteConnection(projectId, location, connectionId) {
46
46
  return res.body;
47
47
  }
48
48
  exports.deleteConnection = deleteConnection;
49
- async function fetchLinkableRepositories(projectId, location, connectionId) {
49
+ async function fetchLinkableRepositories(projectId, location, connectionId, pageToken = "", pageSize = 1000) {
50
50
  const name = `projects/${projectId}/locations/${location}/connections/${connectionId}:fetchLinkableRepositories`;
51
- const res = await client.get(name);
51
+ const res = await client.get(name, {
52
+ queryParams: {
53
+ pageSize,
54
+ pageToken,
55
+ },
56
+ });
52
57
  return res.body;
53
58
  }
54
59
  exports.fetchLinkableRepositories = fetchLinkableRepositories;
@@ -4,12 +4,18 @@ exports.deleteDocuments = exports.deleteDocument = exports.listCollectionIds = e
4
4
  const api_1 = require("../api");
5
5
  const apiv2_1 = require("../apiv2");
6
6
  const logger_1 = require("../logger");
7
- const apiClient = new apiv2_1.Client({
7
+ const prodOnlyClient = new apiv2_1.Client({
8
8
  auth: true,
9
9
  apiVersion: "v1",
10
10
  urlPrefix: api_1.firestoreOrigin,
11
11
  });
12
- async function getDatabase(project, database) {
12
+ const emuOrProdClient = new apiv2_1.Client({
13
+ auth: true,
14
+ apiVersion: "v1",
15
+ urlPrefix: api_1.firestoreOriginOrEmulator,
16
+ });
17
+ async function getDatabase(project, database, allowEmulator = false) {
18
+ const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
13
19
  const url = `projects/${project}/databases/${database}`;
14
20
  try {
15
21
  const resp = await apiClient.get(url);
@@ -21,7 +27,8 @@ async function getDatabase(project, database) {
21
27
  }
22
28
  }
23
29
  exports.getDatabase = getDatabase;
24
- function listCollectionIds(project) {
30
+ function listCollectionIds(project, allowEmulator = false) {
31
+ const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
25
32
  const url = "projects/" + project + "/databases/(default)/documents:listCollectionIds";
26
33
  const data = {
27
34
  pageSize: 2147483647,
@@ -31,11 +38,13 @@ function listCollectionIds(project) {
31
38
  });
32
39
  }
33
40
  exports.listCollectionIds = listCollectionIds;
34
- async function deleteDocument(doc) {
41
+ async function deleteDocument(doc, allowEmulator = false) {
42
+ const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
35
43
  return apiClient.delete(doc.name);
36
44
  }
37
45
  exports.deleteDocument = deleteDocument;
38
- async function deleteDocuments(project, docs) {
46
+ async function deleteDocuments(project, docs, allowEmulator = false) {
47
+ const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
39
48
  const url = "projects/" + project + "/databases/(default)/documents:commit";
40
49
  const writes = docs.map((doc) => {
41
50
  return { delete: doc.name };
@@ -1,3 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const optionsAreHostingOptions = true;
3
+ const metaprogramming_1 = require("../metaprogramming");
4
+ (0, metaprogramming_1.assertImplements)();
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.onboardRollout = exports.createBackend = exports.onboardBackend = exports.doSetup = void 0;
3
+ exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
4
4
  const clc = require("colorette");
5
5
  const repo = require("./repo");
6
6
  const poller = require("../../../operation-poller");
@@ -8,18 +8,21 @@ const apphosting = require("../../../gcp/apphosting");
8
8
  const utils_1 = require("../../../utils");
9
9
  const api_1 = require("../../../api");
10
10
  const apphosting_1 = require("../../../gcp/apphosting");
11
+ const resourceManager_1 = require("../../../gcp/resourceManager");
12
+ const iam_1 = require("../../../gcp/iam");
11
13
  const error_1 = require("../../../error");
12
14
  const prompt_1 = require("../../../prompt");
13
15
  const constants_1 = require("./constants");
14
16
  const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
15
17
  const deploymentTool = require("../../../deploymentTool");
18
+ const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
16
19
  const apphostingPollerOptions = {
17
20
  apiOrigin: api_1.apphostingOrigin,
18
21
  apiVersion: apphosting_1.API_VERSION,
19
22
  masterTimeout: 25 * 60 * 1000,
20
23
  maxBackoff: 10000,
21
24
  };
22
- async function doSetup(setup, projectId) {
25
+ async function doSetup(projectId, location, serviceAccount) {
23
26
  await Promise.all([
24
27
  (0, ensureApiEnabled_1.ensure)(projectId, "cloudbuild.googleapis.com", "apphosting", true),
25
28
  (0, ensureApiEnabled_1.ensure)(projectId, "secretmanager.googleapis.com", "apphosting", true),
@@ -27,53 +30,38 @@ async function doSetup(setup, projectId) {
27
30
  (0, ensureApiEnabled_1.ensure)(projectId, "artifactregistry.googleapis.com", "apphosting", true),
28
31
  ]);
29
32
  const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
30
- if (setup.location) {
31
- if (!allowedLocations.includes(setup.location)) {
32
- throw new error_1.FirebaseError(`Invalid location ${setup.location}. Valid choices are ${allowedLocations.join(", ")}`);
33
+ if (location) {
34
+ if (!allowedLocations.includes(location)) {
35
+ throw new error_1.FirebaseError(`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`);
33
36
  }
34
37
  }
35
38
  (0, utils_1.logBullet)("First we need a few details to create your backend.");
36
- const location = setup.location || (await promptLocation(projectId, allowedLocations));
39
+ location =
40
+ location ||
41
+ (await (0, prompt_1.promptOnce)({
42
+ name: "region",
43
+ type: "list",
44
+ default: constants_1.DEFAULT_REGION,
45
+ message: "Please select a region " +
46
+ `(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
47
+ choices: allowedLocations.map((loc) => ({ value: loc })),
48
+ }));
37
49
  (0, utils_1.logSuccess)(`Region set to ${location}.\n`);
38
- let backendId;
39
- while (true) {
40
- backendId = await (0, prompt_1.promptOnce)({
41
- name: "backendId",
42
- type: "input",
43
- default: "my-web-app",
44
- message: "Create a name for your backend [1-30 characters]",
45
- });
46
- try {
47
- await apphosting.getBackend(projectId, location, backendId);
48
- }
49
- catch (err) {
50
- if (err.status === 404) {
51
- break;
52
- }
53
- throw new error_1.FirebaseError(`Failed to check if backend with id ${backendId} already exists in ${location}`, { original: err });
54
- }
55
- (0, utils_1.logWarning)(`Backend with id ${backendId} already exists in ${location}`);
56
- }
57
- const backend = await onboardBackend(projectId, location, backendId);
50
+ const backendId = await promptNewBackendId(projectId, location, {
51
+ name: "backendId",
52
+ type: "input",
53
+ default: "my-web-app",
54
+ message: "Create a name for your backend [1-30 characters]",
55
+ });
56
+ const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
57
+ const backend = await createBackend(projectId, location, backendId, cloudBuildConnRepo, serviceAccount);
58
58
  const branch = await (0, prompt_1.promptOnce)({
59
59
  name: "branch",
60
60
  type: "input",
61
61
  default: "main",
62
62
  message: "Pick a branch for continuous deployment",
63
63
  });
64
- const traffic = {
65
- rolloutPolicy: {
66
- codebaseBranch: branch,
67
- stages: [
68
- {
69
- progression: "IMMEDIATE",
70
- targetPercent: 100,
71
- },
72
- ],
73
- },
74
- };
75
- const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
76
- await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
64
+ await setDefaultTrafficPolicy(projectId, location, backendId, branch);
77
65
  const confirmRollout = await (0, prompt_1.promptOnce)({
78
66
  type: "confirm",
79
67
  name: "rollout",
@@ -85,68 +73,136 @@ async function doSetup(setup, projectId) {
85
73
  (0, utils_1.logSuccess)(`Your site will be deployed at:\n\thttps://${backend.uri}`);
86
74
  return;
87
75
  }
88
- const { build } = await onboardRollout(projectId, location, backendId, {
76
+ await orchestrateRollout(projectId, location, backendId, {
89
77
  source: {
90
78
  codebase: {
91
79
  branch,
92
80
  },
93
81
  },
94
82
  });
95
- if (build.state !== "READY") {
96
- if (!build.buildLogsUri) {
97
- throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
98
- "This is an internal error and should be reported");
99
- }
100
- throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
101
- }
102
83
  (0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
103
84
  (0, utils_1.logSuccess)(`Your site is now deployed at:\n\thttps://${backend.uri}`);
104
- (0, utils_1.logSuccess)(`View the rollout status by running:\n\tfirebase apphosting:backends:get ${backendId} --project ${projectId}`);
105
85
  }
106
86
  exports.doSetup = doSetup;
107
- async function promptLocation(projectId, locations) {
108
- return (await (0, prompt_1.promptOnce)({
109
- name: "region",
110
- type: "list",
111
- default: constants_1.DEFAULT_REGION,
112
- message: "Please select a region " +
113
- `(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
114
- choices: locations.map((loc) => ({ value: loc })),
115
- }));
87
+ async function promptNewBackendId(projectId, location, prompt) {
88
+ while (true) {
89
+ const backendId = await (0, prompt_1.promptOnce)(prompt);
90
+ try {
91
+ await apphosting.getBackend(projectId, location, backendId);
92
+ }
93
+ catch (err) {
94
+ if (err.status === 404) {
95
+ return backendId;
96
+ }
97
+ throw new error_1.FirebaseError(`Failed to check if backend with id ${backendId} already exists in ${location}`, { original: err });
98
+ }
99
+ (0, utils_1.logWarning)(`Backend with id ${backendId} already exists in ${location}`);
100
+ }
101
+ }
102
+ function defaultComputeServiceAccountEmail(projectId) {
103
+ return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
116
104
  }
117
- function toBackend(cloudBuildConnRepo) {
118
- return {
105
+ async function createBackend(projectId, location, backendId, repository, serviceAccount) {
106
+ const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
107
+ const backendReqBody = {
119
108
  servingLocality: "GLOBAL_ACCESS",
120
109
  codebase: {
121
- repository: `${cloudBuildConnRepo.name}`,
110
+ repository: `${repository.name}`,
122
111
  rootDirectory: "/",
123
112
  },
124
113
  labels: deploymentTool.labels(),
114
+ computeServiceAccount: serviceAccount || defaultServiceAccount,
125
115
  };
116
+ delete backendReqBody.computeServiceAccount;
117
+ async function createBackendAndPoll() {
118
+ const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
119
+ return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
120
+ }
121
+ try {
122
+ return await createBackendAndPoll();
123
+ }
124
+ catch (err) {
125
+ if (err.status === 403 && err.message.includes(defaultServiceAccount)) {
126
+ await provisionDefaultComputeServiceAccount(projectId);
127
+ return await createBackendAndPoll();
128
+ }
129
+ throw err;
130
+ }
126
131
  }
127
- async function onboardBackend(projectId, location, backendId) {
128
- const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
129
- const backendDetails = toBackend(cloudBuildConnRepo);
130
- return await createBackend(projectId, location, backendDetails, backendId);
132
+ exports.createBackend = createBackend;
133
+ async function provisionDefaultComputeServiceAccount(projectId) {
134
+ try {
135
+ await (0, iam_1.createServiceAccount)(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
136
+ }
137
+ catch (err) {
138
+ if (err.status !== 409) {
139
+ throw err;
140
+ }
141
+ }
142
+ await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), [
143
+ "roles/firebaseapphosting.viewer",
144
+ "roles/artifactregistry.createOnPushWriter",
145
+ "roles/logging.logWriter",
146
+ "roles/storage.objectAdmin",
147
+ "roles/firebase.sdkAdminServiceAgent",
148
+ ], true);
131
149
  }
132
- exports.onboardBackend = onboardBackend;
133
- async function createBackend(projectId, location, backendReqBoby, backendId) {
134
- const op = await apphosting.createBackend(projectId, location, backendReqBoby, backendId);
135
- const backend = await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
136
- return backend;
150
+ async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseBranch) {
151
+ const traffic = {
152
+ rolloutPolicy: {
153
+ codebaseBranch: codebaseBranch,
154
+ stages: [
155
+ {
156
+ progression: "IMMEDIATE",
157
+ targetPercent: 100,
158
+ },
159
+ ],
160
+ },
161
+ };
162
+ const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
163
+ await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
137
164
  }
138
- exports.createBackend = createBackend;
139
- async function onboardRollout(projectId, location, backendId, buildInput) {
165
+ exports.setDefaultTrafficPolicy = setDefaultTrafficPolicy;
166
+ async function orchestrateRollout(projectId, location, backendId, buildInput) {
140
167
  (0, utils_1.logBullet)("Starting a new rollout... this may take a few minutes.");
141
168
  const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
142
169
  const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
143
- const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, {
170
+ const rolloutBody = {
144
171
  build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`,
145
- });
172
+ };
173
+ let tries = 0;
174
+ let done = false;
175
+ while (!done) {
176
+ tries++;
177
+ try {
178
+ const validateOnly = true;
179
+ await apphosting.createRollout(projectId, location, backendId, buildId, rolloutBody, validateOnly);
180
+ done = true;
181
+ }
182
+ catch (err) {
183
+ if (err instanceof error_1.FirebaseError && err.status === 400) {
184
+ if (tries >= 5) {
185
+ throw err;
186
+ }
187
+ await new Promise((resolve) => setTimeout(resolve, 1000));
188
+ }
189
+ else {
190
+ throw err;
191
+ }
192
+ }
193
+ }
194
+ const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, rolloutBody);
146
195
  const rolloutPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, operationResourceName: rolloutOp.name }));
147
196
  const buildPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`, operationResourceName: buildOp.name }));
148
197
  const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);
149
198
  (0, utils_1.logSuccess)("Rollout completed.");
199
+ if (build.state !== "READY") {
200
+ if (!build.buildLogsUri) {
201
+ throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
202
+ "This is an internal error and should be reported");
203
+ }
204
+ throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
205
+ }
150
206
  return { rollout, build };
151
207
  }
152
- exports.onboardRollout = onboardRollout;
208
+ exports.orchestrateRollout = orchestrateRollout;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
3
+ exports.fetchAllRepositories = exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
4
4
  const clc = require("colorette");
5
5
  const gcb = require("../../../gcp/cloudbuild");
6
6
  const rm = require("../../../gcp/resourceManager");
@@ -10,11 +10,13 @@ const api_1 = require("../../../api");
10
10
  const error_1 = require("../../../error");
11
11
  const prompt_1 = require("../../../prompt");
12
12
  const getProjectNumber_1 = require("../../../getProjectNumber");
13
+ const fuzzy = require("fuzzy");
14
+ const inquirer = require("inquirer");
13
15
  const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/;
14
16
  const APPHOSTING_OAUTH_CONN_NAME = "apphosting-github-oauth";
15
17
  const CONNECTION_NAME_REGEX = /^projects\/(?<projectId>[^\/]+)\/locations\/(?<location>[^\/]+)\/connections\/(?<id>[^\/]+)$/;
16
18
  function parseConnectionName(name) {
17
- const match = name.match(CONNECTION_NAME_REGEX);
19
+ const match = CONNECTION_NAME_REGEX.exec(name);
18
20
  if (!match || typeof match.groups === undefined) {
19
21
  return;
20
22
  }
@@ -47,19 +49,22 @@ function generateConnectionId() {
47
49
  const randomHash = Math.random().toString(36).slice(6);
48
50
  return `apphosting-github-conn-${randomHash}`;
49
51
  }
52
+ const ADD_REPO_CHOICE = "@ADD_REPO";
53
+ const ADD_CONN_CHOICE = "@ADD_CONN";
54
+ const CONFIGURE_REMOTE_URI_CHOICES = [ADD_REPO_CHOICE, ADD_CONN_CHOICE];
50
55
  async function linkGitHubRepository(projectId, location) {
51
- var _a, _b, _c;
56
+ var _a, _b, _c, _d;
52
57
  utils.logBullet(clc.bold(`${clc.yellow("===")} Set up a GitHub connection`));
58
+ let oauthConn = await getOrCreateConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
59
+ while (oauthConn.installationState.stage === "PENDING_USER_OAUTH") {
60
+ oauthConn = await promptConnectionAuth(oauthConn);
61
+ }
53
62
  const existingConns = await listAppHostingConnections(projectId);
54
63
  if (existingConns.length < 1) {
55
64
  const grantSuccess = await promptSecretManagerAdminGrant(projectId);
56
65
  if (!grantSuccess) {
57
66
  throw new error_1.FirebaseError("Insufficient IAM permissions to create a new connection to GitHub");
58
67
  }
59
- let oauthConn = await getOrCreateConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
60
- while (oauthConn.installationState.stage === "PENDING_USER_OAUTH") {
61
- oauthConn = await promptConnectionAuth(oauthConn);
62
- }
63
68
  const connectionId = generateConnectionId();
64
69
  const conn = await createConnection(projectId, location, connectionId, {
65
70
  authorizerCredential: (_a = oauthConn.githubConfig) === null || _a === void 0 ? void 0 : _a.authorizerCredential,
@@ -71,20 +76,33 @@ async function linkGitHubRepository(projectId, location) {
71
76
  existingConns.push(refreshedConn);
72
77
  }
73
78
  let { remoteUri, connection } = await promptRepositoryUri(projectId, existingConns);
74
- while (remoteUri === "") {
75
- await utils.openInBrowser("https://github.com/apps/google-cloud-build/installations/new");
76
- await (0, prompt_1.promptOnce)({
77
- type: "input",
78
- message: "Press ENTER once you have finished configuring your installation's access settings.",
79
- });
79
+ while (CONFIGURE_REMOTE_URI_CHOICES.includes(remoteUri)) {
80
+ if (remoteUri === ADD_REPO_CHOICE) {
81
+ await utils.openInBrowser("https://github.com/apps/google-cloud-build/installations/new");
82
+ await (0, prompt_1.promptOnce)({
83
+ type: "input",
84
+ message: "Press ENTER once you have provided the GitHub app installation with access to your desired repository.",
85
+ });
86
+ }
87
+ else if (remoteUri === ADD_CONN_CHOICE) {
88
+ const connectionId = generateConnectionId();
89
+ const conn = await createConnection(projectId, location, connectionId, {
90
+ authorizerCredential: (_b = oauthConn.githubConfig) === null || _b === void 0 ? void 0 : _b.authorizerCredential,
91
+ });
92
+ let refreshedConn = conn;
93
+ while (refreshedConn.installationState.stage !== "COMPLETE") {
94
+ refreshedConn = await promptAppInstall(conn);
95
+ }
96
+ existingConns.push(refreshedConn);
97
+ }
80
98
  const selection = await promptRepositoryUri(projectId, existingConns);
81
99
  remoteUri = selection.remoteUri;
82
100
  connection = selection.connection;
83
101
  }
84
102
  const { id: connectionId } = parseConnectionName(connection.name);
85
103
  await getOrCreateConnection(projectId, location, connectionId, {
86
- authorizerCredential: (_b = connection.githubConfig) === null || _b === void 0 ? void 0 : _b.authorizerCredential,
87
- appInstallationId: (_c = connection.githubConfig) === null || _c === void 0 ? void 0 : _c.appInstallationId,
104
+ authorizerCredential: (_c = connection.githubConfig) === null || _c === void 0 ? void 0 : _c.authorizerCredential,
105
+ appInstallationId: (_d = connection.githubConfig) === null || _d === void 0 ? void 0 : _d.appInstallationId,
88
106
  });
89
107
  const repo = await getOrCreateRepository(projectId, location, connectionId, remoteUri);
90
108
  utils.logSuccess(`Successfully linked GitHub repository at remote URI`);
@@ -93,28 +111,36 @@ async function linkGitHubRepository(projectId, location) {
93
111
  }
94
112
  exports.linkGitHubRepository = linkGitHubRepository;
95
113
  async function promptRepositoryUri(projectId, connections) {
96
- const remoteUriToConnection = {};
97
- for (const conn of connections) {
98
- const { location, id } = parseConnectionName(conn.name);
99
- const resp = await gcb.fetchLinkableRepositories(projectId, location, id);
100
- if (resp.repositories && resp.repositories.length > 0) {
101
- for (const repo of resp.repositories) {
102
- remoteUriToConnection[repo.remoteUri] = conn;
103
- }
104
- }
105
- }
106
- const choices = Object.keys(remoteUriToConnection).map((remoteUri) => ({
107
- name: extractRepoSlugFromUri(remoteUri) || remoteUri,
108
- value: remoteUri,
109
- }));
110
- choices.push({
111
- name: "Missing a repo? Select this option to configure your installation's access settings",
112
- value: "",
113
- });
114
+ const { repos, remoteUriToConnection } = await fetchAllRepositories(projectId, connections);
115
+ const searchRepos = (repos) => async (_, input = "") => {
116
+ return [
117
+ new inquirer.Separator(),
118
+ {
119
+ name: "Missing a repo? Select this option to configure your installation's access settings",
120
+ value: ADD_REPO_CHOICE,
121
+ },
122
+ {
123
+ name: "Missing an account or org? Select this option to create a new connection",
124
+ value: ADD_CONN_CHOICE,
125
+ },
126
+ new inquirer.Separator(),
127
+ ...fuzzy
128
+ .filter(input, repos, {
129
+ extract: (repo) => extractRepoSlugFromUri(repo.remoteUri) || "",
130
+ })
131
+ .map((result) => {
132
+ return {
133
+ name: extractRepoSlugFromUri(result.original.remoteUri) || "",
134
+ value: result.original.remoteUri,
135
+ };
136
+ }),
137
+ ];
138
+ };
114
139
  const remoteUri = await (0, prompt_1.promptOnce)({
115
- type: "list",
140
+ type: "autocomplete",
141
+ name: "remoteUri",
116
142
  message: "Which of the following repositories would you like to deploy?",
117
- choices,
143
+ source: searchRepos(repos),
118
144
  });
119
145
  return { remoteUri, connection: remoteUriToConnection[remoteUri] };
120
146
  }
@@ -217,3 +243,25 @@ async function listAppHostingConnections(projectId) {
217
243
  !conn.disabled);
218
244
  }
219
245
  exports.listAppHostingConnections = listAppHostingConnections;
246
+ async function fetchAllRepositories(projectId, connections) {
247
+ const repos = [];
248
+ const remoteUriToConnection = {};
249
+ const getNextPage = async (conn, pageToken = "") => {
250
+ const { location, id } = parseConnectionName(conn.name);
251
+ const resp = await gcb.fetchLinkableRepositories(projectId, location, id, pageToken);
252
+ if (resp.repositories && resp.repositories.length > 0) {
253
+ for (const repo of resp.repositories) {
254
+ repos.push(repo);
255
+ remoteUriToConnection[repo.remoteUri] = conn;
256
+ }
257
+ }
258
+ if (resp.nextPageToken) {
259
+ await getNextPage(conn, resp.nextPageToken);
260
+ }
261
+ };
262
+ for (const conn of connections) {
263
+ await getNextPage(conn);
264
+ }
265
+ return { repos, remoteUriToConnection };
266
+ }
267
+ exports.fetchAllRepositories = fetchAllRepositories;
@@ -160,6 +160,11 @@ function writeChannelActionYMLFile(ymlPath, secretName, projectId, script) {
160
160
  const workflowConfig = {
161
161
  name: "Deploy to Firebase Hosting on PR",
162
162
  on: "pull_request",
163
+ permissions: {
164
+ checks: "write",
165
+ contents: "read",
166
+ "pull-requests": "write",
167
+ },
163
168
  jobs: {
164
169
  ["build_and_preview"]: {
165
170
  if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}",
@@ -1,2 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertImplements = void 0;
4
+ function assertImplements() { }
5
+ exports.assertImplements = assertImplements;
package/lib/prompt.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.confirm = exports.promptOnce = exports.prompt = void 0;
4
4
  const inquirer = require("inquirer");
5
5
  const error_1 = require("./error");
6
+ inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
6
7
  async function prompt(options, questions) {
7
8
  const prompts = [];
8
9
  for (const question of questions) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.2.1",
3
+ "version": "13.3.1",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -80,9 +80,11 @@
80
80
  "filesize": "^6.1.0",
81
81
  "form-data": "^4.0.0",
82
82
  "fs-extra": "^10.1.0",
83
+ "fuzzy": "^0.1.3",
83
84
  "glob": "^7.1.2",
84
85
  "google-auth-library": "^7.11.0",
85
- "inquirer": "^8.2.0",
86
+ "inquirer": "^8.2.6",
87
+ "inquirer-autocomplete-prompt": "^2.0.1",
86
88
  "js-yaml": "^3.13.1",
87
89
  "jsonwebtoken": "^9.0.0",
88
90
  "leven": "^3.1.0",