firebase-tools 13.3.0 → 13.4.0

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.
Files changed (51) hide show
  1. package/lib/api.js +2 -1
  2. package/lib/commands/apphosting-backends-create.js +6 -3
  3. package/lib/commands/apphosting-backends-delete.js +6 -44
  4. package/lib/commands/apphosting-backends-get.js +4 -35
  5. package/lib/commands/apphosting-backends-list.js +17 -9
  6. package/lib/commands/apphosting-builds-create.js +4 -4
  7. package/lib/commands/apphosting-builds-get.js +2 -2
  8. package/lib/commands/apphosting-rollouts-create.js +3 -3
  9. package/lib/commands/apphosting-rollouts-list.js +2 -2
  10. package/lib/commands/firestore-backups-delete.js +44 -0
  11. package/lib/commands/firestore-backups-get.js +25 -0
  12. package/lib/commands/firestore-backups-list.js +34 -0
  13. package/lib/commands/firestore-backups-schedules-create.js +67 -0
  14. package/lib/commands/firestore-backups-schedules-delete.js +46 -0
  15. package/lib/commands/firestore-backups-schedules-list.js +28 -0
  16. package/lib/commands/firestore-backups-schedules-update.js +33 -0
  17. package/lib/commands/firestore-databases-create.js +4 -2
  18. package/lib/commands/firestore-databases-delete.js +4 -2
  19. package/lib/commands/firestore-databases-get.js +3 -1
  20. package/lib/commands/firestore-databases-list.js +3 -1
  21. package/lib/commands/firestore-databases-restore.js +42 -0
  22. package/lib/commands/firestore-databases-update.js +4 -2
  23. package/lib/commands/firestore-indexes-list.js +5 -3
  24. package/lib/commands/firestore-locations.js +3 -1
  25. package/lib/commands/index.js +10 -0
  26. package/lib/deploy/extensions/v2FunctionHelper.js +2 -1
  27. package/lib/deploy/functions/ensure.js +2 -2
  28. package/lib/deploy/functions/prepare.js +5 -9
  29. package/lib/emulator/downloadableEmulators.js +3 -3
  30. package/lib/ensureApiEnabled.js +8 -6
  31. package/lib/extensions/extensionsHelper.js +2 -2
  32. package/lib/extensions/secretsUtils.js +2 -1
  33. package/lib/firestore/api-sort.js +23 -1
  34. package/lib/firestore/api-types.js +6 -1
  35. package/lib/firestore/api.js +18 -115
  36. package/lib/firestore/backupUtils.js +30 -0
  37. package/lib/firestore/pretty-print.js +184 -0
  38. package/lib/functions/secrets.js +1 -1
  39. package/lib/gcp/apphosting.js +4 -5
  40. package/lib/gcp/auth.js +5 -2
  41. package/lib/gcp/cloudbuild.js +7 -2
  42. package/lib/gcp/firestore.js +75 -1
  43. package/lib/gcp/storage.js +1 -1
  44. package/lib/init/features/apphosting/index.js +139 -78
  45. package/lib/init/features/apphosting/repo.js +83 -35
  46. package/lib/init/features/database.js +2 -1
  47. package/lib/init/features/extensions/index.js +2 -1
  48. package/lib/init/features/functions/index.js +3 -2
  49. package/lib/init/features/hosting/github.js +5 -0
  50. package/lib/prompt.js +1 -0
  51. package/package.json +5 -2
@@ -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,72 +8,60 @@ 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
- (0, ensureApiEnabled_1.ensure)(projectId, "cloudbuild.googleapis.com", "apphosting", true),
25
- (0, ensureApiEnabled_1.ensure)(projectId, "secretmanager.googleapis.com", "apphosting", true),
26
- (0, ensureApiEnabled_1.ensure)(projectId, "run.googleapis.com", "apphosting", true),
27
- (0, ensureApiEnabled_1.ensure)(projectId, "artifactregistry.googleapis.com", "apphosting", true),
27
+ (0, ensureApiEnabled_1.ensure)(projectId, api_1.cloudbuildOrigin, "apphosting", true),
28
+ (0, ensureApiEnabled_1.ensure)(projectId, api_1.secretManagerOrigin, "apphosting", true),
29
+ (0, ensureApiEnabled_1.ensure)(projectId, api_1.cloudRunApiOrigin, "apphosting", true),
30
+ (0, ensureApiEnabled_1.ensure)(projectId, api_1.artifactRegistryDomain, "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,141 @@ 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) {
126
+ if (err.message.includes(defaultServiceAccount)) {
127
+ await provisionDefaultComputeServiceAccount(projectId);
128
+ return await createBackendAndPoll();
129
+ }
130
+ else if (serviceAccount && err.message.includes(serviceAccount)) {
131
+ throw new error_1.FirebaseError(`Failed to create backend due to missing delegation permissions for ${serviceAccount}. Make sure you have the iam.serviceAccounts.actAs permission.`, { children: [err] });
132
+ }
133
+ }
134
+ throw err;
135
+ }
126
136
  }
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);
137
+ exports.createBackend = createBackend;
138
+ async function provisionDefaultComputeServiceAccount(projectId) {
139
+ try {
140
+ 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");
141
+ }
142
+ catch (err) {
143
+ if (err.status !== 409) {
144
+ throw err;
145
+ }
146
+ }
147
+ await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), [
148
+ "roles/firebaseapphosting.viewer",
149
+ "roles/artifactregistry.createOnPushWriter",
150
+ "roles/logging.logWriter",
151
+ "roles/storage.objectAdmin",
152
+ "roles/firebase.sdkAdminServiceAgent",
153
+ ], true);
131
154
  }
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;
155
+ async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseBranch) {
156
+ const traffic = {
157
+ rolloutPolicy: {
158
+ codebaseBranch: codebaseBranch,
159
+ stages: [
160
+ {
161
+ progression: "IMMEDIATE",
162
+ targetPercent: 100,
163
+ },
164
+ ],
165
+ },
166
+ };
167
+ const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
168
+ await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
137
169
  }
138
- exports.createBackend = createBackend;
139
- async function onboardRollout(projectId, location, backendId, buildInput) {
170
+ exports.setDefaultTrafficPolicy = setDefaultTrafficPolicy;
171
+ async function orchestrateRollout(projectId, location, backendId, buildInput) {
140
172
  (0, utils_1.logBullet)("Starting a new rollout... this may take a few minutes.");
141
173
  const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
142
174
  const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
143
- const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, {
175
+ const rolloutBody = {
144
176
  build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`,
145
- });
177
+ };
178
+ let tries = 0;
179
+ let done = false;
180
+ while (!done) {
181
+ tries++;
182
+ try {
183
+ const validateOnly = true;
184
+ await apphosting.createRollout(projectId, location, backendId, buildId, rolloutBody, validateOnly);
185
+ done = true;
186
+ }
187
+ catch (err) {
188
+ if (err instanceof error_1.FirebaseError && err.status === 400) {
189
+ if (tries >= 5) {
190
+ throw err;
191
+ }
192
+ await new Promise((resolve) => setTimeout(resolve, 1000));
193
+ }
194
+ else {
195
+ throw err;
196
+ }
197
+ }
198
+ }
199
+ const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, rolloutBody);
146
200
  const rolloutPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, operationResourceName: rolloutOp.name }));
147
201
  const buildPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`, operationResourceName: buildOp.name }));
148
202
  const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);
149
203
  (0, utils_1.logSuccess)("Rollout completed.");
204
+ if (build.state !== "READY") {
205
+ if (!build.buildLogsUri) {
206
+ throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
207
+ "This is an internal error and should be reported");
208
+ }
209
+ throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
210
+ }
150
211
  return { rollout, build };
151
212
  }
152
- exports.onboardRollout = onboardRollout;
213
+ 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;
@@ -12,6 +12,7 @@ const ensureApiEnabled_1 = require("../../ensureApiEnabled");
12
12
  const getDefaultDatabaseInstance_1 = require("../../getDefaultDatabaseInstance");
13
13
  const error_1 = require("../../error");
14
14
  const apiv2_1 = require("../../apiv2");
15
+ const api_1 = require("../../api");
15
16
  const DEFAULT_RULES = JSON.stringify({ rules: { ".read": "auth != null", ".write": "auth != null" } }, null, 2);
16
17
  async function getDBRules(instanceDetails) {
17
18
  if (!instanceDetails || !instanceDetails.name) {
@@ -70,7 +71,7 @@ async function doSetup(setup, config) {
70
71
  setup.config = setup.config || {};
71
72
  let instanceDetails;
72
73
  if (setup.projectId) {
73
- await (0, ensureApiEnabled_1.ensure)(setup.projectId, "firebasedatabase.googleapis.com", "database", false);
74
+ await (0, ensureApiEnabled_1.ensure)(setup.projectId, api_1.rtdbManagementOrigin, "database", false);
74
75
  logger_1.logger.info();
75
76
  setup.instance =
76
77
  setup.instance || (await (0, getDefaultDatabaseInstance_1.getDefaultDatabaseInstance)({ project: setup.projectId }));
@@ -4,12 +4,13 @@ exports.doSetup = void 0;
4
4
  const requirePermissions_1 = require("../../../requirePermissions");
5
5
  const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
6
6
  const manifest = require("../../../extensions/manifest");
7
+ const api_1 = require("../../../api");
7
8
  async function doSetup(setup, config, options) {
8
9
  var _a, _b;
9
10
  const projectId = (_b = (_a = setup === null || setup === void 0 ? void 0 : setup.rcfile) === null || _a === void 0 ? void 0 : _a.projects) === null || _b === void 0 ? void 0 : _b.default;
10
11
  if (projectId) {
11
12
  await (0, requirePermissions_1.requirePermissions)(Object.assign(Object.assign({}, options), { project: projectId }));
12
- await Promise.all([(0, ensureApiEnabled_1.ensure)(projectId, "firebaseextensions.googleapis.com", "unused", true)]);
13
+ await Promise.all([(0, ensureApiEnabled_1.ensure)(projectId, api_1.extensionsOrigin, "unused", true)]);
13
14
  }
14
15
  return manifest.writeEmptyManifest(config, options);
15
16
  }
@@ -8,6 +8,7 @@ const requirePermissions_1 = require("../../../requirePermissions");
8
8
  const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
9
9
  const projectConfig_1 = require("../../../functions/projectConfig");
10
10
  const error_1 = require("../../../error");
11
+ const api_1 = require("../../../api");
11
12
  const MAX_ATTEMPTS = 5;
12
13
  async function doSetup(setup, config, options) {
13
14
  var _a, _b;
@@ -15,8 +16,8 @@ async function doSetup(setup, config, options) {
15
16
  if (projectId) {
16
17
  await (0, requirePermissions_1.requirePermissions)(Object.assign(Object.assign({}, options), { project: projectId }));
17
18
  await Promise.all([
18
- (0, ensureApiEnabled_1.ensure)(projectId, "cloudfunctions.googleapis.com", "unused", true),
19
- (0, ensureApiEnabled_1.ensure)(projectId, "runtimeconfig.googleapis.com", "unused", true),
19
+ (0, ensureApiEnabled_1.ensure)(projectId, api_1.functionsOrigin, "unused", true),
20
+ (0, ensureApiEnabled_1.ensure)(projectId, api_1.runtimeconfigOrigin, "unused", true),
20
21
  ]);
21
22
  }
22
23
  setup.functions = {};
@@ -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 }}",
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.3.0",
3
+ "version": "13.4.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -74,15 +74,18 @@
74
74
  "cross-env": "^5.1.3",
75
75
  "cross-spawn": "^7.0.3",
76
76
  "csv-parse": "^5.0.4",
77
+ "deep-equal-in-any-order": "^2.0.6",
77
78
  "exegesis": "^4.1.0",
78
79
  "exegesis-express": "^4.0.0",
79
80
  "express": "^4.16.4",
80
81
  "filesize": "^6.1.0",
81
82
  "form-data": "^4.0.0",
82
83
  "fs-extra": "^10.1.0",
84
+ "fuzzy": "^0.1.3",
83
85
  "glob": "^7.1.2",
84
86
  "google-auth-library": "^7.11.0",
85
- "inquirer": "^8.2.0",
87
+ "inquirer": "^8.2.6",
88
+ "inquirer-autocomplete-prompt": "^2.0.1",
86
89
  "js-yaml": "^3.13.1",
87
90
  "jsonwebtoken": "^9.0.0",
88
91
  "leven": "^3.1.0",