firebase-tools 13.0.3 → 13.2.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.
@@ -1,37 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppDistributionClient = exports.UploadReleaseResult = exports.IntegrationState = void 0;
3
+ exports.AppDistributionClient = void 0;
4
4
  const utils = require("../utils");
5
5
  const operationPoller = require("../operation-poller");
6
6
  const error_1 = require("../error");
7
7
  const apiv2_1 = require("../apiv2");
8
8
  const api_1 = require("../api");
9
- var IntegrationState;
10
- (function (IntegrationState) {
11
- IntegrationState["AAB_INTEGRATION_STATE_UNSPECIFIED"] = "AAB_INTEGRATION_STATE_UNSPECIFIED";
12
- IntegrationState["INTEGRATED"] = "INTEGRATED";
13
- IntegrationState["PLAY_ACCOUNT_NOT_LINKED"] = "PLAY_ACCOUNT_NOT_LINKED";
14
- IntegrationState["NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT"] = "NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT";
15
- IntegrationState["APP_NOT_PUBLISHED"] = "APP_NOT_PUBLISHED";
16
- IntegrationState["AAB_STATE_UNAVAILABLE"] = "AAB_STATE_UNAVAILABLE";
17
- IntegrationState["PLAY_IAS_TERMS_NOT_ACCEPTED"] = "PLAY_IAS_TERMS_NOT_ACCEPTED";
18
- })(IntegrationState = exports.IntegrationState || (exports.IntegrationState = {}));
19
- var UploadReleaseResult;
20
- (function (UploadReleaseResult) {
21
- UploadReleaseResult["UPLOAD_RELEASE_RESULT_UNSPECIFIED"] = "UPLOAD_RELEASE_RESULT_UNSPECIFIED";
22
- UploadReleaseResult["RELEASE_CREATED"] = "RELEASE_CREATED";
23
- UploadReleaseResult["RELEASE_UPDATED"] = "RELEASE_UPDATED";
24
- UploadReleaseResult["RELEASE_UNMODIFIED"] = "RELEASE_UNMODIFIED";
25
- })(UploadReleaseResult = exports.UploadReleaseResult || (exports.UploadReleaseResult = {}));
9
+ const types_1 = require("./types");
26
10
  class AppDistributionClient {
27
11
  constructor() {
28
- this.appDistroV2Client = new apiv2_1.Client({
12
+ this.appDistroV1Client = new apiv2_1.Client({
29
13
  urlPrefix: api_1.appDistributionOrigin,
30
14
  apiVersion: "v1",
31
15
  });
16
+ this.appDistroV1AlphaClient = new apiv2_1.Client({
17
+ urlPrefix: api_1.appDistributionOrigin,
18
+ apiVersion: "v1alpha",
19
+ });
32
20
  }
33
21
  async getAabInfo(appName) {
34
- const apiResponse = await this.appDistroV2Client.get(`/${appName}/aabInfo`);
22
+ const apiResponse = await this.appDistroV1Client.get(`/${appName}/aabInfo`);
35
23
  return apiResponse.body;
36
24
  }
37
25
  async uploadRelease(appName, distribution) {
@@ -74,7 +62,7 @@ class AppDistributionClient {
74
62
  };
75
63
  const queryParams = { updateMask: "release_notes.text" };
76
64
  try {
77
- await this.appDistroV2Client.patch(`/${releaseName}`, data, { queryParams });
65
+ await this.appDistroV1Client.patch(`/${releaseName}`, data, { queryParams });
78
66
  }
79
67
  catch (err) {
80
68
  throw new error_1.FirebaseError(`failed to update release notes with ${err === null || err === void 0 ? void 0 : err.message}`);
@@ -93,7 +81,7 @@ class AppDistributionClient {
93
81
  groupAliases,
94
82
  };
95
83
  try {
96
- await this.appDistroV2Client.post(`/${releaseName}:distribute`, data);
84
+ await this.appDistroV1Client.post(`/${releaseName}:distribute`, data);
97
85
  }
98
86
  catch (err) {
99
87
  let errorMessage = err.message;
@@ -112,7 +100,7 @@ class AppDistributionClient {
112
100
  }
113
101
  async addTesters(projectName, emails) {
114
102
  try {
115
- await this.appDistroV2Client.request({
103
+ await this.appDistroV1Client.request({
116
104
  method: "POST",
117
105
  path: `${projectName}/testers:batchAdd`,
118
106
  body: { emails: emails },
@@ -126,7 +114,7 @@ class AppDistributionClient {
126
114
  async removeTesters(projectName, emails) {
127
115
  let apiResponse;
128
116
  try {
129
- apiResponse = await this.appDistroV2Client.request({
117
+ apiResponse = await this.appDistroV1Client.request({
130
118
  method: "POST",
131
119
  path: `${projectName}/testers:batchRemove`,
132
120
  body: { emails: emails },
@@ -140,7 +128,7 @@ class AppDistributionClient {
140
128
  async createGroup(projectName, displayName, alias) {
141
129
  let apiResponse;
142
130
  try {
143
- apiResponse = await this.appDistroV2Client.request({
131
+ apiResponse = await this.appDistroV1Client.request({
144
132
  method: "POST",
145
133
  path: alias === undefined ? `${projectName}/groups` : `${projectName}/groups?groupId=${alias}`,
146
134
  body: { displayName: displayName },
@@ -153,7 +141,7 @@ class AppDistributionClient {
153
141
  }
154
142
  async deleteGroup(groupName) {
155
143
  try {
156
- await this.appDistroV2Client.request({
144
+ await this.appDistroV1Client.request({
157
145
  method: "DELETE",
158
146
  path: groupName,
159
147
  });
@@ -165,7 +153,7 @@ class AppDistributionClient {
165
153
  }
166
154
  async addTestersToGroup(groupName, emails) {
167
155
  try {
168
- await this.appDistroV2Client.request({
156
+ await this.appDistroV1Client.request({
169
157
  method: "POST",
170
158
  path: `${groupName}:batchJoin`,
171
159
  body: { emails: emails },
@@ -178,7 +166,7 @@ class AppDistributionClient {
178
166
  }
179
167
  async removeTestersFromGroup(groupName, emails) {
180
168
  try {
181
- await this.appDistroV2Client.request({
169
+ await this.appDistroV1Client.request({
182
170
  method: "POST",
183
171
  path: `${groupName}:batchLeave`,
184
172
  body: { emails: emails },
@@ -189,5 +177,25 @@ class AppDistributionClient {
189
177
  }
190
178
  utils.logSuccess(`Testers removed from group successfully`);
191
179
  }
180
+ async createReleaseTest(releaseName, devices, loginCredential) {
181
+ try {
182
+ const response = await this.appDistroV1AlphaClient.request({
183
+ method: "POST",
184
+ path: `${releaseName}/tests`,
185
+ body: {
186
+ deviceExecutions: devices.map(types_1.mapDeviceToExecution),
187
+ loginCredential,
188
+ },
189
+ });
190
+ return response.body;
191
+ }
192
+ catch (err) {
193
+ throw new error_1.FirebaseError(`Failed to create release test ${err}`);
194
+ }
195
+ }
196
+ async getReleaseTest(releaseTestName) {
197
+ const response = await this.appDistroV1AlphaClient.get(releaseTestName);
198
+ return response.body;
199
+ }
192
200
  }
193
201
  exports.AppDistributionClient = AppDistributionClient;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getAppName = exports.getProjectName = exports.ensureFileExists = exports.getEmails = exports.getTestersOrGroups = void 0;
3
+ exports.getLoginCredential = exports.getTestDevices = exports.getAppName = exports.getProjectName = exports.ensureFileExists = exports.getEmails = exports.getTestersOrGroups = void 0;
4
4
  const fs = require("fs-extra");
5
5
  const error_1 = require("../error");
6
6
  const projectUtils_1 = require("../projectUtils");
@@ -49,3 +49,82 @@ function getAppName(options) {
49
49
  return `projects/${appId.split(":")[1]}/apps/${appId}`;
50
50
  }
51
51
  exports.getAppName = getAppName;
52
+ function getTestDevices(value, file) {
53
+ if (!value && file) {
54
+ ensureFileExists(file);
55
+ value = fs.readFileSync(file, "utf8");
56
+ }
57
+ if (!value) {
58
+ return [];
59
+ }
60
+ return value
61
+ .split(/[;\n]/)
62
+ .map((entry) => entry.trim())
63
+ .filter((entry) => !!entry)
64
+ .map((str) => parseTestDevice(str));
65
+ }
66
+ exports.getTestDevices = getTestDevices;
67
+ function parseTestDevice(testDeviceString) {
68
+ const entries = testDeviceString.split(",");
69
+ const allowedKeys = new Set(["model", "version", "orientation", "locale"]);
70
+ let model;
71
+ let version;
72
+ let orientation;
73
+ let locale;
74
+ for (const entry of entries) {
75
+ const keyAndValue = entry.split("=");
76
+ switch (keyAndValue[0]) {
77
+ case "model":
78
+ model = keyAndValue[1];
79
+ break;
80
+ case "version":
81
+ version = keyAndValue[1];
82
+ break;
83
+ case "orientation":
84
+ orientation = keyAndValue[1];
85
+ break;
86
+ case "locale":
87
+ locale = keyAndValue[1];
88
+ break;
89
+ default:
90
+ throw new error_1.FirebaseError(`Unrecognized key in test devices. Can only contain ${Array.from(allowedKeys).join(", ")}`);
91
+ }
92
+ }
93
+ if (!model || !version || !orientation || !locale) {
94
+ throw new error_1.FirebaseError("Test devices must be in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'");
95
+ }
96
+ return { model, version, locale, orientation };
97
+ }
98
+ function getLoginCredential(args) {
99
+ const { username, passwordFile, usernameResourceName, passwordResourceName } = args;
100
+ let password = args.password;
101
+ if (!password && passwordFile) {
102
+ ensureFileExists(passwordFile);
103
+ password = fs.readFileSync(passwordFile, "utf8").trim();
104
+ }
105
+ if (isPresenceMismatched(usernameResourceName, passwordResourceName)) {
106
+ throw new error_1.FirebaseError("Username and password resource names for automated tests need to be specified together.");
107
+ }
108
+ let fieldHints;
109
+ if (usernameResourceName && passwordResourceName) {
110
+ fieldHints = {
111
+ usernameResourceName: usernameResourceName,
112
+ passwordResourceName: passwordResourceName,
113
+ };
114
+ }
115
+ if (isPresenceMismatched(username, password)) {
116
+ throw new error_1.FirebaseError("Username and password for automated tests need to be specified together.");
117
+ }
118
+ let loginCredential;
119
+ if (username && password) {
120
+ loginCredential = { username, password, fieldHints };
121
+ }
122
+ else if (fieldHints) {
123
+ throw new error_1.FirebaseError("Must specify username and password for automated tests if resource names are set");
124
+ }
125
+ return loginCredential;
126
+ }
127
+ exports.getLoginCredential = getLoginCredential;
128
+ function isPresenceMismatched(value1, value2) {
129
+ return (value1 && !value2) || (!value1 && value2);
130
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapDeviceToExecution = exports.UploadReleaseResult = exports.IntegrationState = void 0;
4
+ var IntegrationState;
5
+ (function (IntegrationState) {
6
+ IntegrationState["AAB_INTEGRATION_STATE_UNSPECIFIED"] = "AAB_INTEGRATION_STATE_UNSPECIFIED";
7
+ IntegrationState["INTEGRATED"] = "INTEGRATED";
8
+ IntegrationState["PLAY_ACCOUNT_NOT_LINKED"] = "PLAY_ACCOUNT_NOT_LINKED";
9
+ IntegrationState["NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT"] = "NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT";
10
+ IntegrationState["APP_NOT_PUBLISHED"] = "APP_NOT_PUBLISHED";
11
+ IntegrationState["AAB_STATE_UNAVAILABLE"] = "AAB_STATE_UNAVAILABLE";
12
+ IntegrationState["PLAY_IAS_TERMS_NOT_ACCEPTED"] = "PLAY_IAS_TERMS_NOT_ACCEPTED";
13
+ })(IntegrationState = exports.IntegrationState || (exports.IntegrationState = {}));
14
+ var UploadReleaseResult;
15
+ (function (UploadReleaseResult) {
16
+ UploadReleaseResult["UPLOAD_RELEASE_RESULT_UNSPECIFIED"] = "UPLOAD_RELEASE_RESULT_UNSPECIFIED";
17
+ UploadReleaseResult["RELEASE_CREATED"] = "RELEASE_CREATED";
18
+ UploadReleaseResult["RELEASE_UPDATED"] = "RELEASE_UPDATED";
19
+ UploadReleaseResult["RELEASE_UNMODIFIED"] = "RELEASE_UNMODIFIED";
20
+ })(UploadReleaseResult = exports.UploadReleaseResult || (exports.UploadReleaseResult = {}));
21
+ function mapDeviceToExecution(device) {
22
+ return {
23
+ device: {
24
+ model: device.model,
25
+ version: device.version,
26
+ orientation: device.orientation,
27
+ locale: device.locale,
28
+ },
29
+ };
30
+ }
31
+ exports.mapDeviceToExecution = mapDeviceToExecution;
@@ -6,9 +6,12 @@ const command_1 = require("../command");
6
6
  const utils = require("../utils");
7
7
  const requireAuth_1 = require("../requireAuth");
8
8
  const client_1 = require("../appdistribution/client");
9
+ const types_1 = require("../appdistribution/types");
9
10
  const error_1 = require("../error");
10
11
  const distribution_1 = require("../appdistribution/distribution");
11
12
  const options_parser_util_1 = require("../appdistribution/options-parser-util");
13
+ const TEST_MAX_POLLING_RETRIES = 40;
14
+ const TEST_POLLING_INTERVAL_MILLIS = 30000;
12
15
  function getReleaseNotes(releaseNotes, releaseNotesFile) {
13
16
  if (releaseNotes) {
14
17
  return releaseNotes.replace(/\\n/g, "\n");
@@ -28,6 +31,14 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
28
31
  .option("--testers-file <file>", "path to file with a comma separated list of tester emails to distribute to")
29
32
  .option("--groups <string>", "a comma separated list of group aliases to distribute to")
30
33
  .option("--groups-file <file>", "path to file with a comma separated list of group aliases to distribute to")
34
+ .option("--test-devices <string>", "semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
35
+ .option("--test-devices-file <string>", "path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
36
+ .option("--test-username <string>", "username for automatic login")
37
+ .option("--test-password <string>", "password for automatic login. If using a real password, use --test-password-file instead to avoid putting sensitive info in history and logs.")
38
+ .option("--test-password-file <string>", "path to file containing password for automatic login")
39
+ .option("--test-username-resource <string>", "resource name for the username field for automatic login")
40
+ .option("--test-password-resource <string>", "resource name for the password field for automatic login")
41
+ .option("--test-non-blocking", "run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
31
42
  .before(requireAuth_1.requireAuth)
32
43
  .action(async (file, options) => {
33
44
  const appName = (0, options_parser_util_1.getAppName)(options);
@@ -35,6 +46,14 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
35
46
  const releaseNotes = getReleaseNotes(options.releaseNotes, options.releaseNotesFile);
36
47
  const testers = (0, options_parser_util_1.getTestersOrGroups)(options.testers, options.testersFile);
37
48
  const groups = (0, options_parser_util_1.getTestersOrGroups)(options.groups, options.groupsFile);
49
+ const testDevices = (0, options_parser_util_1.getTestDevices)(options.testDevices, options.testDevicesFile);
50
+ const loginCredential = (0, options_parser_util_1.getLoginCredential)({
51
+ username: options.testUsername,
52
+ password: options.testPassword,
53
+ passwordFile: options.testPasswordFile,
54
+ usernameResourceName: options.testUsernameResource,
55
+ passwordResourceName: options.testPasswordResource,
56
+ });
38
57
  const requests = new client_1.AppDistributionClient();
39
58
  let aabInfo;
40
59
  if (distribution.distributionFileType() === distribution_1.DistributionFileType.AAB) {
@@ -50,19 +69,19 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
50
69
  }
51
70
  throw new error_1.FirebaseError(`failed to determine AAB info. ${err.message}`, { exit: 1 });
52
71
  }
53
- if (aabInfo.integrationState !== client_1.IntegrationState.INTEGRATED &&
54
- aabInfo.integrationState !== client_1.IntegrationState.AAB_STATE_UNAVAILABLE) {
72
+ if (aabInfo.integrationState !== types_1.IntegrationState.INTEGRATED &&
73
+ aabInfo.integrationState !== types_1.IntegrationState.AAB_STATE_UNAVAILABLE) {
55
74
  switch (aabInfo.integrationState) {
56
- case client_1.IntegrationState.PLAY_ACCOUNT_NOT_LINKED: {
75
+ case types_1.IntegrationState.PLAY_ACCOUNT_NOT_LINKED: {
57
76
  throw new error_1.FirebaseError("This project is not linked to a Google Play account.");
58
77
  }
59
- case client_1.IntegrationState.APP_NOT_PUBLISHED: {
78
+ case types_1.IntegrationState.APP_NOT_PUBLISHED: {
60
79
  throw new error_1.FirebaseError('"This app is not published in the Google Play console.');
61
80
  }
62
- case client_1.IntegrationState.NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT: {
81
+ case types_1.IntegrationState.NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT: {
63
82
  throw new error_1.FirebaseError("App with matching package name does not exist in Google Play.");
64
83
  }
65
- case client_1.IntegrationState.PLAY_IAS_TERMS_NOT_ACCEPTED: {
84
+ case types_1.IntegrationState.PLAY_IAS_TERMS_NOT_ACCEPTED: {
66
85
  throw new error_1.FirebaseError("You must accept the Play Internal App Sharing (IAS) terms to upload AABs.");
67
86
  }
68
87
  default: {
@@ -78,13 +97,13 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
78
97
  const uploadResponse = await requests.pollUploadStatus(operationName);
79
98
  const release = uploadResponse.release;
80
99
  switch (uploadResponse.result) {
81
- case client_1.UploadReleaseResult.RELEASE_CREATED:
100
+ case types_1.UploadReleaseResult.RELEASE_CREATED:
82
101
  utils.logSuccess(`uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!`);
83
102
  break;
84
- case client_1.UploadReleaseResult.RELEASE_UPDATED:
103
+ case types_1.UploadReleaseResult.RELEASE_UPDATED:
85
104
  utils.logSuccess(`uploaded update to existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
86
105
  break;
87
- case client_1.UploadReleaseResult.RELEASE_UNMODIFIED:
106
+ case types_1.UploadReleaseResult.RELEASE_UNMODIFIED:
88
107
  utils.logSuccess(`re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
89
108
  break;
90
109
  default:
@@ -102,7 +121,7 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
102
121
  "button on the App Distribution page in the Firebase console: " +
103
122
  "https://console.firebase.google.com/project/_/appdistribution", { exit: 1 });
104
123
  }
105
- throw new error_1.FirebaseError(`failed to upload release. ${err.message}`, { exit: 1 });
124
+ throw new error_1.FirebaseError(`Failed to upload release. ${err.message}`, { exit: 1 });
106
125
  }
107
126
  if (aabInfo && !aabInfo.testCertificate) {
108
127
  aabInfo = await requests.getAabInfo(appName);
@@ -118,4 +137,45 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
118
137
  }
119
138
  await requests.updateReleaseNotes(releaseName, releaseNotes);
120
139
  await requests.distribute(releaseName, testers, groups);
140
+ if (testDevices) {
141
+ utils.logBullet("starting automated tests (note: this feature is in beta)");
142
+ const releaseTest = await requests.createReleaseTest(releaseName, testDevices, loginCredential);
143
+ utils.logSuccess(`Release test created successfully`);
144
+ if (!options.testNonBlocking) {
145
+ await awaitTestResults(releaseTest.name, requests);
146
+ }
147
+ }
121
148
  });
149
+ async function awaitTestResults(releaseTestName, requests) {
150
+ for (let i = 0; i < TEST_MAX_POLLING_RETRIES; i++) {
151
+ utils.logBullet("the automated tests results are pending");
152
+ await delay(TEST_POLLING_INTERVAL_MILLIS);
153
+ const releaseTest = await requests.getReleaseTest(releaseTestName);
154
+ if (releaseTest.deviceExecutions.every((e) => e.state === "PASSED")) {
155
+ utils.logSuccess("automated test(s) passed!");
156
+ return;
157
+ }
158
+ for (const execution of releaseTest.deviceExecutions) {
159
+ switch (execution.state) {
160
+ case "PASSED":
161
+ case "IN_PROGRESS":
162
+ continue;
163
+ case "FAILED":
164
+ throw new error_1.FirebaseError(`Automated test failed for ${deviceToString(execution.device)}: ${execution.failedReason}`, { exit: 1 });
165
+ case "INCONCLUSIVE":
166
+ throw new error_1.FirebaseError(`Automated test inconclusive for ${deviceToString(execution.device)}: ${execution.inconclusiveReason}`, { exit: 1 });
167
+ default:
168
+ throw new error_1.FirebaseError(`Unsupported automated test state for ${deviceToString(execution.device)}: ${execution.state}`, { exit: 1 });
169
+ }
170
+ }
171
+ }
172
+ throw new error_1.FirebaseError("It took longer than expected to process your test, please try again.", {
173
+ exit: 1,
174
+ });
175
+ }
176
+ function delay(ms) {
177
+ return new Promise((resolve) => setTimeout(resolve, ms));
178
+ }
179
+ function deviceToString(device) {
180
+ return `${device.model} (${device.version}/${device.orientation}/${device.locale})`;
181
+ }
@@ -1,58 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.command = void 0;
4
+ const Table = require("cli-table");
4
5
  const command_1 = require("../command");
5
- const projectUtils_1 = require("../projectUtils");
6
+ const utils_1 = require("../utils");
6
7
  const error_1 = require("../error");
7
8
  const logger_1 = require("../logger");
9
+ const projectUtils_1 = require("../projectUtils");
8
10
  const apphosting = require("../gcp/apphosting");
9
- const Table = require("cli-table");
10
- const COLUMN_LENGTH = 20;
11
- const TABLE_HEAD = ["Backend Id", "Repository", "Location", "URL", "Created Date", "Updated Date"];
11
+ const TABLE_HEAD = ["Backend ID", "Repository", "Location", "URL", "Created Date", "Updated Date"];
12
12
  exports.command = new command_1.Command("apphosting:backends:list")
13
13
  .description("List backends of a Firebase project.")
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
18
  const projectId = (0, projectUtils_1.needProjectId)(options);
18
19
  const location = options.location;
19
- const table = new Table({
20
- head: TABLE_HEAD,
21
- style: { head: ["green"] },
22
- });
23
- table.colWidths = COLUMN_LENGTH;
24
- const backendsList = [];
20
+ const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
21
+ let backendRes;
25
22
  try {
26
- const backendsPerRegion = await apphosting.listBackends(projectId, location);
27
- backendsList.push(...(backendsPerRegion.backends || []));
28
- populateTable(backendsList, table);
29
- logger_1.logger.info(table.toString());
23
+ backendRes = await apphosting.listBackends(projectId, location);
30
24
  }
31
25
  catch (err) {
32
26
  throw new error_1.FirebaseError(`Unable to list backends present for project: ${projectId}. Please check the parameters you have provided.`, { original: err });
33
27
  }
34
- return backendsList;
35
- });
36
- function populateTable(backends, table) {
37
- var _a, _b;
28
+ const backends = backendRes === null || backendRes === void 0 ? void 0 : backendRes.backends;
38
29
  for (const backend of backends) {
39
- const [location, , backendId] = backend.name.split("/").slice(3, 6);
40
- const entry = [
30
+ const [backendLocation, , backendId] = backend.name.split("/").slice(3, 6);
31
+ table.push([
41
32
  backendId,
42
- (_b = (_a = backend.codebase) === null || _a === void 0 ? void 0 : _a.repository) === null || _b === void 0 ? void 0 : _b.split("/").pop(),
43
- location,
44
- backend.uri,
45
- backend.createTime,
46
- backend.updateTime,
47
- ];
48
- const newRow = entry.map((name) => {
49
- const maxCellWidth = COLUMN_LENGTH - 2;
50
- const chunks = [];
51
- for (let i = 0; name && i < name.length; i += maxCellWidth) {
52
- chunks.push(name.substring(i, i + maxCellWidth));
53
- }
54
- return chunks.join("\n");
55
- });
56
- table.push(newRow);
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 : "",
34
+ backendLocation,
35
+ backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
36
+ (0, utils_1.datetimeString)(new Date(backend.createTime)),
37
+ (0, utils_1.datetimeString)(new Date(backend.updateTime)),
38
+ ]);
57
39
  }
58
- }
40
+ logger_1.logger.info(table.toString());
41
+ return backends;
42
+ });
@@ -4,7 +4,6 @@ exports.command = void 0;
4
4
  const apphosting = require("../gcp/apphosting");
5
5
  const logger_1 = require("../logger");
6
6
  const command_1 = require("../command");
7
- const utils_1 = require("../utils");
8
7
  const projectUtils_1 = require("../projectUtils");
9
8
  exports.command = new command_1.Command("apphosting:builds:create <backendId>")
10
9
  .description("Create a build for an App Hosting backend")
@@ -15,7 +14,8 @@ exports.command = new command_1.Command("apphosting:builds:create <backendId>")
15
14
  .action(async (backendId, options) => {
16
15
  const projectId = (0, projectUtils_1.needProjectId)(options);
17
16
  const location = options.location;
18
- const buildId = options.buildId || (0, utils_1.generateId)();
17
+ const buildId = options.buildId ||
18
+ (await apphosting.getNextRolloutId(projectId, location, backendId));
19
19
  const branch = options.branch;
20
20
  const op = await apphosting.createBuild(projectId, location, backendId, buildId, {
21
21
  source: {
@@ -5,7 +5,6 @@ const apphosting = require("../gcp/apphosting");
5
5
  const logger_1 = require("../logger");
6
6
  const command_1 = require("../command");
7
7
  const projectUtils_1 = require("../projectUtils");
8
- const utils_1 = require("../utils");
9
8
  exports.command = new command_1.Command("apphosting:rollouts:create <backendId> <buildId>")
10
9
  .description("Create a build for an App Hosting backend")
11
10
  .option("-l, --location <location>", "Specify the region of the backend", "us-central1")
@@ -14,7 +13,8 @@ exports.command = new command_1.Command("apphosting:rollouts:create <backendId>
14
13
  .action(async (backendId, buildId, options) => {
15
14
  const projectId = (0, projectUtils_1.needProjectId)(options);
16
15
  const location = options.location;
17
- const rolloutId = options.buildId || (0, utils_1.generateId)();
16
+ const rolloutId = options.buildId ||
17
+ (await apphosting.getNextRolloutId(projectId, location, backendId));
18
18
  const build = `projects/${projectId}/backends/${backendId}/builds/${buildId}`;
19
19
  const op = await apphosting.createRollout(projectId, location, backendId, rolloutId, {
20
20
  build,
@@ -13,6 +13,9 @@ exports.command = new command_1.Command("apphosting:rollouts:list <backendId>")
13
13
  const projectId = (0, projectUtils_1.needProjectId)(options);
14
14
  const location = options.location;
15
15
  const rollouts = await apphosting.listRollouts(projectId, location, backendId);
16
- logger_1.logger.info(JSON.stringify(rollouts, null, 2));
16
+ if (rollouts.unreachable) {
17
+ logger_1.logger.error(`WARNING: the following locations were unreachable: ${rollouts.unreachable.join(", ")}`);
18
+ }
19
+ logger_1.logger.info(JSON.stringify(rollouts.rollouts, null, 2));
17
20
  return rollouts;
18
21
  });
@@ -110,8 +110,10 @@ exports.command = new command_1.Command("functions:config:export")
110
110
  const dotEnvs = pInfos.map((pInfo) => configExport.toDotenvFormat(pInfo.envs, header));
111
111
  const filenames = pInfos.map(configExport.generateDotenvFilename);
112
112
  const filesToWrite = fromEntries((0, functional_1.zip)(filenames, dotEnvs));
113
- filesToWrite[".env.local"] = `${header}\n# .env.local file contains environment variables for the Functions Emulator.\n`;
114
- filesToWrite[".env"] = `${header}# .env file contains environment variables that applies to all projects.\n`;
113
+ filesToWrite[".env.local"] =
114
+ `${header}\n# .env.local file contains environment variables for the Functions Emulator.\n`;
115
+ filesToWrite[".env"] =
116
+ `${header}# .env file contains environment variables that applies to all projects.\n`;
115
117
  for (const [filename, content] of Object.entries(filesToWrite)) {
116
118
  await options.config.askWriteProjectFile(path.join(functionsDir, filename), content);
117
119
  }
@@ -70,7 +70,10 @@ exports.command = new command_1.Command("use [alias_or_project_id]")
70
70
  }
71
71
  if (hasAlias) {
72
72
  if (!project) {
73
- return utils.reject("Unable to use alias " + clc.bold(newActive) + ", " + verifyMessage(resolvedProject));
73
+ return utils.reject("Unable to use alias " +
74
+ clc.bold(newActive) +
75
+ ", " +
76
+ verifyMessage(resolvedProject));
74
77
  }
75
78
  utils.makeActiveProject(options.projectRoot, newActive);
76
79
  logger_1.logger.info("Now using alias", clc.bold(newActive), "(" + resolvedProject + ")");
@@ -125,6 +125,11 @@ async function convertConfig(context, functionsPayload, deploy) {
125
125
  region: endpoint.region,
126
126
  } });
127
127
  if (rewrite.function.pinTag) {
128
+ if (endpoint.minInstances) {
129
+ throw new error_1.FirebaseError(`Function ${endpoint.id} has minInstances set and is in a rewrite ` +
130
+ "pinTags=true. These features are not currently compatible with each " +
131
+ "other.");
132
+ }
128
133
  experiments.assertEnabled("pintags", "pin a function version");
129
134
  apiRewrite.run.tag = runTags.TODO_TAG_NAME;
130
135
  }
@@ -154,7 +154,8 @@ async function unsafePins(context, config) {
154
154
  const existingUntaggedRewrites = {};
155
155
  for (const rewrite of ((_d = (_c = (_b = channelConfig === null || channelConfig === void 0 ? void 0 : channelConfig.release) === null || _b === void 0 ? void 0 : _b.version) === null || _c === void 0 ? void 0 : _c.config) === null || _d === void 0 ? void 0 : _d.rewrites) || []) {
156
156
  if ("run" in rewrite && !rewrite.run.tag) {
157
- existingUntaggedRewrites[rewriteTarget(rewrite)] = `${rewrite.run.region}/${rewrite.run.serviceId}`;
157
+ existingUntaggedRewrites[rewriteTarget(rewrite)] =
158
+ `${rewrite.run.region}/${rewrite.run.serviceId}`;
158
159
  }
159
160
  }
160
161
  return Object.keys(targetTaggedRewrites).filter((target) => targetTaggedRewrites[target] === existingUntaggedRewrites[target]);
@@ -159,7 +159,7 @@ function registerHandlers(app, getProjectStateByApiKey) {
159
159
  res.set("Content-Type", "text/html; charset=utf-8");
160
160
  const apiKey = req.query.apiKey;
161
161
  const providerId = req.query.providerId;
162
- const tenantId = req.query.tenantId;
162
+ const tenantId = (req.query.tenantId || req.query.tid);
163
163
  if (!apiKey || !providerId) {
164
164
  return res.status(400).json({
165
165
  authEmulator: {