firebase-tools 13.1.0 → 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
+ }
@@ -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 + ")");
@@ -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]);
@@ -484,13 +484,21 @@ function createAuthUri(state, reqBody) {
484
484
  }
485
485
  }
486
486
  }
487
- return {
488
- kind: "identitytoolkit#CreateAuthUriResponse",
489
- registered,
490
- allProviders,
491
- sessionId,
492
- signinMethods,
493
- };
487
+ if (state.enableImprovedEmailPrivacy) {
488
+ return {
489
+ kind: "identitytoolkit#CreateAuthUriResponse",
490
+ sessionId,
491
+ };
492
+ }
493
+ else {
494
+ return {
495
+ kind: "identitytoolkit#CreateAuthUriResponse",
496
+ registered,
497
+ allProviders,
498
+ sessionId,
499
+ signinMethods,
500
+ };
501
+ }
494
502
  }
495
503
  const SESSION_COOKIE_MIN_VALID_DURATION = 5 * 60;
496
504
  exports.SESSION_COOKIE_MAX_VALID_DURATION = 14 * 24 * 60 * 60;
@@ -634,7 +642,14 @@ function sendOobCode(state, reqBody, ctx) {
634
642
  mode = "resetPassword";
635
643
  (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
636
644
  email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
637
- (0, errors_1.assert)(state.getUserByEmail(email), "EMAIL_NOT_FOUND");
645
+ const maybeUser = state.getUserByEmail(email);
646
+ if (state.enableImprovedEmailPrivacy && !maybeUser) {
647
+ return {
648
+ kind: "identitytoolkit#GetOobConfirmationCodeResponse",
649
+ email,
650
+ };
651
+ }
652
+ (0, errors_1.assert)(maybeUser, "EMAIL_NOT_FOUND");
638
653
  break;
639
654
  case "VERIFY_EMAIL":
640
655
  mode = "verifyEmail";
@@ -1199,10 +1214,18 @@ async function signInWithPassword(state, reqBody) {
1199
1214
  }
1200
1215
  const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
1201
1216
  let user = state.getUserByEmail(email);
1202
- (0, errors_1.assert)(user, "EMAIL_NOT_FOUND");
1203
- (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
1204
- (0, errors_1.assert)(user.passwordHash && user.salt, "INVALID_PASSWORD");
1205
- (0, errors_1.assert)(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD");
1217
+ if (state.enableImprovedEmailPrivacy) {
1218
+ (0, errors_1.assert)(user, "INVALID_LOGIN_CREDENTIALS");
1219
+ (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
1220
+ (0, errors_1.assert)(user.passwordHash && user.salt, "INVALID_LOGIN_CREDENTIALS");
1221
+ (0, errors_1.assert)(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_LOGIN_CREDENTIALS");
1222
+ }
1223
+ else {
1224
+ (0, errors_1.assert)(user, "EMAIL_NOT_FOUND");
1225
+ (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
1226
+ (0, errors_1.assert)(user.passwordHash && user.salt, "INVALID_PASSWORD");
1227
+ (0, errors_1.assert)(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD");
1228
+ }
1206
1229
  const response = {
1207
1230
  kind: "identitytoolkit#VerifyPasswordResponse",
1208
1231
  registered: true,
@@ -1311,14 +1334,20 @@ function getEmulatorProjectConfig(state) {
1311
1334
  signIn: {
1312
1335
  allowDuplicateEmails: !state.oneAccountPerEmail,
1313
1336
  },
1337
+ emailPrivacyConfig: {
1338
+ enableImprovedEmailPrivacy: state.enableImprovedEmailPrivacy,
1339
+ },
1314
1340
  };
1315
1341
  }
1316
1342
  function updateEmulatorProjectConfig(state, reqBody, ctx) {
1317
- var _a;
1343
+ var _a, _b;
1318
1344
  const updateMask = [];
1319
1345
  if (((_a = reqBody.signIn) === null || _a === void 0 ? void 0 : _a.allowDuplicateEmails) != null) {
1320
1346
  updateMask.push("signIn.allowDuplicateEmails");
1321
1347
  }
1348
+ if (((_b = reqBody.emailPrivacyConfig) === null || _b === void 0 ? void 0 : _b.enableImprovedEmailPrivacy) != null) {
1349
+ updateMask.push("emailPrivacyConfig.enableImprovedEmailPrivacy");
1350
+ }
1322
1351
  ctx.params.query.updateMask = updateMask.join();
1323
1352
  updateConfig(state, reqBody, ctx);
1324
1353
  return getEmulatorProjectConfig(state);
@@ -414,6 +414,9 @@ class AgentProjectState extends ProjectState {
414
414
  this._config = {
415
415
  signIn: { allowDuplicateEmails: false },
416
416
  blockingFunctions: {},
417
+ emailPrivacyConfig: {
418
+ enableImprovedEmailPrivacy: false,
419
+ },
417
420
  };
418
421
  }
419
422
  get authCloudFunction() {
@@ -425,6 +428,12 @@ class AgentProjectState extends ProjectState {
425
428
  set oneAccountPerEmail(oneAccountPerEmail) {
426
429
  this._config.signIn.allowDuplicateEmails = !oneAccountPerEmail;
427
430
  }
431
+ get enableImprovedEmailPrivacy() {
432
+ return !!this._config.emailPrivacyConfig.enableImprovedEmailPrivacy;
433
+ }
434
+ set enableImprovedEmailPrivacy(improveEmailPrivacy) {
435
+ this._config.emailPrivacyConfig.enableImprovedEmailPrivacy = improveEmailPrivacy;
436
+ }
428
437
  get allowPasswordSignup() {
429
438
  return true;
430
439
  }
@@ -470,10 +479,12 @@ class AgentProjectState extends ProjectState {
470
479
  return undefined;
471
480
  }
472
481
  updateConfig(update, updateMask) {
473
- var _a, _b, _c;
482
+ var _a, _b, _c, _d, _e;
474
483
  if (!updateMask) {
475
484
  this.oneAccountPerEmail = (_b = !((_a = update.signIn) === null || _a === void 0 ? void 0 : _a.allowDuplicateEmails)) !== null && _b !== void 0 ? _b : true;
476
485
  this.blockingFunctionsConfig = (_c = update.blockingFunctions) !== null && _c !== void 0 ? _c : {};
486
+ this.enableImprovedEmailPrivacy =
487
+ (_e = (_d = update.emailPrivacyConfig) === null || _d === void 0 ? void 0 : _d.enableImprovedEmailPrivacy) !== null && _e !== void 0 ? _e : false;
477
488
  return this.config;
478
489
  }
479
490
  return applyMask(updateMask, this.config, update);
@@ -546,6 +557,9 @@ class TenantProjectState extends ProjectState {
546
557
  get oneAccountPerEmail() {
547
558
  return this.parentProject.oneAccountPerEmail;
548
559
  }
560
+ get enableImprovedEmailPrivacy() {
561
+ return this.parentProject.enableImprovedEmailPrivacy;
562
+ }
549
563
  get authCloudFunction() {
550
564
  return this.parentProject.authCloudFunction;
551
565
  }
@@ -1,79 +1,120 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureApiEnabled = exports.listLocations = exports.updateTraffic = exports.listRollouts = exports.createRollout = exports.createBuild = exports.getBuild = exports.deleteBackend = exports.listBackends = exports.getBackend = exports.createBackend = exports.API_VERSION = exports.API_HOST = void 0;
3
+ exports.getNextRolloutId = exports.ensureApiEnabled = exports.listLocations = exports.updateTraffic = exports.listRollouts = exports.createRollout = exports.createBuild = exports.listBuilds = exports.getBuild = exports.deleteBackend = exports.listBackends = exports.getBackend = exports.createBackend = exports.client = exports.API_VERSION = exports.API_HOST = void 0;
4
4
  const proto = require("../gcp/proto");
5
5
  const apiv2_1 = require("../apiv2");
6
6
  const projectUtils_1 = require("../projectUtils");
7
7
  const api_1 = require("../api");
8
8
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
9
+ const deploymentTool = require("../deploymentTool");
10
+ const error_1 = require("../error");
9
11
  exports.API_HOST = new URL(api_1.apphostingOrigin).host;
10
12
  exports.API_VERSION = "v1alpha";
11
- const client = new apiv2_1.Client({
13
+ exports.client = new apiv2_1.Client({
12
14
  urlPrefix: api_1.apphostingOrigin,
13
15
  auth: true,
14
16
  apiVersion: exports.API_VERSION,
15
17
  });
16
18
  async function createBackend(projectId, location, backendReqBoby, backendId) {
17
- const res = await client.post(`projects/${projectId}/locations/${location}/backends`, backendReqBoby, { queryParams: { backendId } });
19
+ 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 } });
18
20
  return res.body;
19
21
  }
20
22
  exports.createBackend = createBackend;
21
23
  async function getBackend(projectId, location, backendId) {
22
24
  const name = `projects/${projectId}/locations/${location}/backends/${backendId}`;
23
- const res = await client.get(name);
25
+ const res = await exports.client.get(name);
24
26
  return res.body;
25
27
  }
26
28
  exports.getBackend = getBackend;
27
29
  async function listBackends(projectId, location) {
28
30
  const name = `projects/${projectId}/locations/${location}/backends`;
29
- const res = await client.get(name);
31
+ const res = await exports.client.get(name);
30
32
  return res.body;
31
33
  }
32
34
  exports.listBackends = listBackends;
33
35
  async function deleteBackend(projectId, location, backendId) {
34
- const name = `projects/${projectId}/locations/${location}/backends/${backendId}?force=true`;
35
- const res = await client.delete(name);
36
+ const name = `projects/${projectId}/locations/${location}/backends/${backendId}`;
37
+ const res = await exports.client.delete(name, { queryParams: { force: "true" } });
36
38
  return res.body;
37
39
  }
38
40
  exports.deleteBackend = deleteBackend;
39
41
  async function getBuild(projectId, location, backendId, buildId) {
40
42
  const name = `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`;
41
- const res = await client.get(name);
43
+ const res = await exports.client.get(name);
42
44
  return res.body;
43
45
  }
44
46
  exports.getBuild = getBuild;
47
+ async function listBuilds(projectId, location, backendId) {
48
+ var _a;
49
+ const name = `projects/${projectId}/locations/${location}/backends/${backendId}/builds`;
50
+ let pageToken;
51
+ const res = {
52
+ builds: [],
53
+ unreachable: [],
54
+ };
55
+ do {
56
+ const queryParams = pageToken ? { pageToken } : {};
57
+ const int = await exports.client.get(name, { queryParams });
58
+ res.builds.push(...(int.body.builds || []));
59
+ (_a = res.unreachable) === null || _a === void 0 ? void 0 : _a.push(...(int.body.unreachable || []));
60
+ pageToken = int.body.nextPageToken;
61
+ } while (pageToken);
62
+ res.unreachable = [...new Set(res.unreachable)];
63
+ return res;
64
+ }
65
+ exports.listBuilds = listBuilds;
45
66
  async function createBuild(projectId, location, backendId, buildId, buildInput) {
46
- const res = await client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/builds`, buildInput, { queryParams: { buildId } });
67
+ const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/builds`, Object.assign(Object.assign({}, buildInput), { labels: Object.assign(Object.assign({}, buildInput.labels), deploymentTool.labels()) }), { queryParams: { buildId } });
47
68
  return res.body;
48
69
  }
49
70
  exports.createBuild = createBuild;
50
71
  async function createRollout(projectId, location, backendId, rolloutId, rollout) {
51
- const res = await client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`, rollout, { queryParams: { rolloutId } });
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 } });
52
73
  return res.body;
53
74
  }
54
75
  exports.createRollout = createRollout;
55
76
  async function listRollouts(projectId, location, backendId) {
56
- const res = await client.get(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`);
57
- return res.body.rollouts;
77
+ const name = `projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`;
78
+ let pageToken = undefined;
79
+ const res = {
80
+ rollouts: [],
81
+ unreachable: [],
82
+ };
83
+ do {
84
+ const queryParams = pageToken ? { pageToken } : {};
85
+ const int = await exports.client.get(name, { queryParams });
86
+ res.rollouts.splice(res.rollouts.length, 0, ...(int.body.rollouts || []));
87
+ res.unreachable.splice(res.unreachable.length, 0, ...(int.body.unreachable || []));
88
+ pageToken = int.body.nextPageToken;
89
+ } while (pageToken);
90
+ res.unreachable = [...new Set(res.unreachable)];
91
+ return res;
58
92
  }
59
93
  exports.listRollouts = listRollouts;
60
94
  async function updateTraffic(projectId, location, backendId, traffic) {
61
- const fieldMasks = proto.fieldMasks(traffic);
95
+ const trafficCopy = Object.assign({}, traffic);
96
+ if ("rolloutPolicy" in traffic) {
97
+ trafficCopy.rolloutPolicy = {};
98
+ }
99
+ const fieldMasks = proto.fieldMasks(trafficCopy);
62
100
  const queryParams = {
63
101
  updateMask: fieldMasks.join(","),
64
102
  };
65
103
  const name = `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`;
66
- const res = await client.patch(name, Object.assign(Object.assign({}, traffic), { name }), {
104
+ const res = await exports.client.patch(name, Object.assign(Object.assign({}, traffic), { name }), {
67
105
  queryParams,
68
106
  });
69
107
  return res.body;
70
108
  }
71
109
  exports.updateTraffic = updateTraffic;
72
110
  async function listLocations(projectId) {
73
- let pageToken;
111
+ let pageToken = undefined;
74
112
  let locations = [];
75
113
  do {
76
- const response = await client.get(`projects/${projectId}/locations`);
114
+ const queryParams = pageToken ? { pageToken } : {};
115
+ const response = await exports.client.get(`projects/${projectId}/locations`, {
116
+ queryParams,
117
+ });
77
118
  if (response.body.locations && response.body.locations.length > 0) {
78
119
  locations = locations.concat(response.body.locations);
79
120
  }
@@ -87,3 +128,31 @@ async function ensureApiEnabled(options) {
87
128
  return await (0, ensureApiEnabled_1.ensure)(projectId, exports.API_HOST, "frameworks", true);
88
129
  }
89
130
  exports.ensureApiEnabled = ensureApiEnabled;
131
+ async function getNextRolloutId(projectId, location, backendId, counter) {
132
+ var _a;
133
+ const date = new Date();
134
+ const year = date.getUTCFullYear();
135
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
136
+ const day = String(date.getUTCDay()).padStart(2, "0");
137
+ if (counter) {
138
+ return `build-${year}-${month}-${day}-${String(counter).padStart(3, "0")}`;
139
+ }
140
+ const builds = await exports.listRollouts(projectId, location, backendId);
141
+ if ((_a = builds.unreachable) === null || _a === void 0 ? void 0 : _a.includes(location)) {
142
+ throw new error_1.FirebaseError(`Firebase App Hosting is currently unreachable in location ${location}`);
143
+ }
144
+ let highest = 0;
145
+ const test = new RegExp(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts/build-${year}-${month}-${day}-(\\d+)`);
146
+ for (const rollout of builds.rollouts) {
147
+ const match = rollout.name.match(test);
148
+ if (!match) {
149
+ continue;
150
+ }
151
+ const n = Number(match[1]);
152
+ if (n > highest) {
153
+ highest = n;
154
+ }
155
+ }
156
+ return `build-${year}-${month}-${day}-${String(highest + 1).padStart(3, "0")}`;
157
+ }
158
+ exports.getNextRolloutId = getNextRolloutId;
@@ -12,6 +12,7 @@ const error_1 = require("../../../error");
12
12
  const prompt_1 = require("../../../prompt");
13
13
  const constants_1 = require("./constants");
14
14
  const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
15
+ const deploymentTool = require("../../../deploymentTool");
15
16
  const apphostingPollerOptions = {
16
17
  apiOrigin: api_1.apphostingOrigin,
17
18
  apiVersion: apphosting_1.API_VERSION,
@@ -54,6 +55,25 @@ async function doSetup(setup, projectId) {
54
55
  (0, utils_1.logWarning)(`Backend with id ${backendId} already exists in ${location}`);
55
56
  }
56
57
  const backend = await onboardBackend(projectId, location, backendId);
58
+ const branch = await (0, prompt_1.promptOnce)({
59
+ name: "branch",
60
+ type: "input",
61
+ default: "main",
62
+ message: "Pick a branch for continuous deployment",
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 }));
57
77
  const confirmRollout = await (0, prompt_1.promptOnce)({
58
78
  type: "confirm",
59
79
  name: "rollout",
@@ -65,12 +85,6 @@ async function doSetup(setup, projectId) {
65
85
  (0, utils_1.logSuccess)(`Your site will be deployed at:\n\thttps://${backend.uri}`);
66
86
  return;
67
87
  }
68
- const branch = await (0, prompt_1.promptOnce)({
69
- name: "branch",
70
- type: "input",
71
- default: "main",
72
- message: "Which branch do you want to deploy?",
73
- });
74
88
  const { build } = await onboardRollout(projectId, location, backendId, {
75
89
  source: {
76
90
  codebase: {
@@ -79,6 +93,10 @@ async function doSetup(setup, projectId) {
79
93
  },
80
94
  });
81
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
+ }
82
100
  throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
83
101
  }
84
102
  (0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
@@ -103,7 +121,7 @@ function toBackend(cloudBuildConnRepo) {
103
121
  repository: `${cloudBuildConnRepo.name}`,
104
122
  rootDirectory: "/",
105
123
  },
106
- labels: {},
124
+ labels: deploymentTool.labels(),
107
125
  };
108
126
  }
109
127
  async function onboardBackend(projectId, location, backendId) {
@@ -120,13 +138,12 @@ async function createBackend(projectId, location, backendReqBoby, backendId) {
120
138
  exports.createBackend = createBackend;
121
139
  async function onboardRollout(projectId, location, backendId, buildInput) {
122
140
  (0, utils_1.logBullet)("Starting a new rollout... this may take a few minutes.");
123
- const buildId = (0, utils_1.generateId)();
141
+ const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
124
142
  const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
125
- const rolloutId = (0, utils_1.generateId)();
126
- const rolloutOp = await apphosting.createRollout(projectId, location, backendId, rolloutId, {
143
+ const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, {
127
144
  build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`,
128
145
  });
129
- const rolloutPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${rolloutId}`, operationResourceName: rolloutOp.name }));
146
+ const rolloutPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, operationResourceName: rolloutOp.name }));
130
147
  const buildPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`, operationResourceName: buildOp.name }));
131
148
  const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);
132
149
  (0, utils_1.logSuccess)("Rollout completed.");
package/lib/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateId = exports.getHostnameFromUrl = exports.openInBrowserPopup = exports.openInBrowser = exports.connectableHostname = exports.randomInt = exports.debounce = exports.last = exports.cloneDeep = exports.groupBy = exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isVSCodeExtension = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.promiseWithSpinner = exports.setupLoggers = exports.tryParse = exports.tryStringify = exports.promiseProps = exports.withTimeout = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledError = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.getInheritedOption = exports.consoleUrl = exports.envOverrides = void 0;
3
+ exports.getHostnameFromUrl = exports.openInBrowserPopup = exports.openInBrowser = exports.connectableHostname = exports.randomInt = exports.debounce = exports.last = exports.cloneDeep = exports.groupBy = exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isVSCodeExtension = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.promiseWithSpinner = exports.setupLoggers = exports.tryParse = exports.tryStringify = exports.promiseProps = exports.withTimeout = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledError = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.getInheritedOption = exports.consoleUrl = exports.envOverrides = void 0;
4
4
  const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const _ = require("lodash");
@@ -524,14 +524,3 @@ function getHostnameFromUrl(url) {
524
524
  }
525
525
  }
526
526
  exports.getHostnameFromUrl = getHostnameFromUrl;
527
- function generateId(n = 6) {
528
- const letters = "abcdefghijklmnopqrstuvwxyz";
529
- const allChars = "01234567890-abcdefghijklmnopqrstuvwxyz";
530
- let id = letters[Math.floor(Math.random() * letters.length)];
531
- for (let i = 1; i < n; i++) {
532
- const idx = Math.floor(Math.random() * allChars.length);
533
- id += allChars[idx];
534
- }
535
- return id;
536
- }
537
- exports.generateId = generateId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.1.0",
3
+ "version": "13.2.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {