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.
- package/lib/appdistribution/client.js +36 -28
- package/lib/appdistribution/options-parser-util.js +80 -1
- package/lib/appdistribution/types.js +31 -0
- package/lib/commands/appdistribution-distribute.js +70 -10
- package/lib/commands/apphosting-builds-create.js +2 -2
- package/lib/commands/apphosting-rollouts-create.js +2 -2
- package/lib/commands/apphosting-rollouts-list.js +4 -1
- package/lib/commands/functions-config-export.js +4 -2
- package/lib/commands/use.js +4 -1
- package/lib/deploy/hosting/prepare.js +2 -1
- package/lib/emulator/auth/operations.js +42 -13
- package/lib/emulator/auth/state.js +15 -1
- package/lib/gcp/apphosting.js +85 -16
- package/lib/init/features/apphosting/index.js +28 -11
- package/lib/utils.js +1 -12
- package/package.json +1 -1
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AppDistributionClient =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 !==
|
|
54
|
-
aabInfo.integrationState !==
|
|
72
|
+
if (aabInfo.integrationState !== types_1.IntegrationState.INTEGRATED &&
|
|
73
|
+
aabInfo.integrationState !== types_1.IntegrationState.AAB_STATE_UNAVAILABLE) {
|
|
55
74
|
switch (aabInfo.integrationState) {
|
|
56
|
-
case
|
|
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
|
|
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
|
|
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
|
|
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
|
|
100
|
+
case types_1.UploadReleaseResult.RELEASE_CREATED:
|
|
82
101
|
utils.logSuccess(`uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!`);
|
|
83
102
|
break;
|
|
84
|
-
case
|
|
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
|
|
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(`
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
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"] =
|
|
114
|
-
|
|
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
|
}
|
package/lib/commands/use.js
CHANGED
|
@@ -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 " +
|
|
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)] =
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
}
|
package/lib/gcp/apphosting.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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
|
|
57
|
-
|
|
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
|
|
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
|
|
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 = (
|
|
141
|
+
const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
|
|
124
142
|
const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
|
|
125
|
-
const
|
|
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-${
|
|
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.
|
|
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;
|