firebase-tools 13.0.3 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-backends-list.js +20 -36
- 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/convertConfig.js +5 -0
- package/lib/deploy/hosting/prepare.js +2 -1
- package/lib/emulator/auth/handlers.js +1 -1
- package/lib/emulator/auth/operations.js +43 -14
- package/lib/emulator/auth/state.js +15 -1
- package/lib/frameworks/angular/index.js +2 -2
- package/lib/frameworks/angular/utils.js +14 -10
- package/lib/frameworks/astro/index.js +1 -2
- package/lib/frameworks/flutter/index.js +1 -1
- package/lib/frameworks/index.js +2 -3
- package/lib/frameworks/next/index.js +5 -3
- package/lib/frameworks/nuxt/index.js +2 -2
- package/lib/frameworks/nuxt2/index.js +1 -2
- package/lib/frameworks/sveltekit/index.js +2 -2
- package/lib/frameworks/utils.js +11 -2
- package/lib/frameworks/vite/index.js +8 -4
- package/lib/gcp/apphosting.js +84 -15
- package/lib/gcp/cloudfunctionsv2.js +2 -4
- package/lib/hosting/api.js +6 -9
- 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
|
+
}
|
|
@@ -1,58 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.command = void 0;
|
|
4
|
+
const Table = require("cli-table");
|
|
4
5
|
const command_1 = require("../command");
|
|
5
|
-
const
|
|
6
|
+
const utils_1 = require("../utils");
|
|
6
7
|
const error_1 = require("../error");
|
|
7
8
|
const logger_1 = require("../logger");
|
|
9
|
+
const projectUtils_1 = require("../projectUtils");
|
|
8
10
|
const apphosting = require("../gcp/apphosting");
|
|
9
|
-
const
|
|
10
|
-
const COLUMN_LENGTH = 20;
|
|
11
|
-
const TABLE_HEAD = ["Backend Id", "Repository", "Location", "URL", "Created Date", "Updated Date"];
|
|
11
|
+
const TABLE_HEAD = ["Backend ID", "Repository", "Location", "URL", "Created Date", "Updated Date"];
|
|
12
12
|
exports.command = new command_1.Command("apphosting:backends:list")
|
|
13
13
|
.description("List backends of a Firebase project.")
|
|
14
14
|
.option("-l, --location <location>", "App Backend location", "-")
|
|
15
15
|
.before(apphosting.ensureApiEnabled)
|
|
16
16
|
.action(async (options) => {
|
|
17
|
+
var _a, _b, _c;
|
|
17
18
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
18
19
|
const location = options.location;
|
|
19
|
-
const table = new Table({
|
|
20
|
-
|
|
21
|
-
style: { head: ["green"] },
|
|
22
|
-
});
|
|
23
|
-
table.colWidths = COLUMN_LENGTH;
|
|
24
|
-
const backendsList = [];
|
|
20
|
+
const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
|
|
21
|
+
let backendRes;
|
|
25
22
|
try {
|
|
26
|
-
|
|
27
|
-
backendsList.push(...(backendsPerRegion.backends || []));
|
|
28
|
-
populateTable(backendsList, table);
|
|
29
|
-
logger_1.logger.info(table.toString());
|
|
23
|
+
backendRes = await apphosting.listBackends(projectId, location);
|
|
30
24
|
}
|
|
31
25
|
catch (err) {
|
|
32
26
|
throw new error_1.FirebaseError(`Unable to list backends present for project: ${projectId}. Please check the parameters you have provided.`, { original: err });
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
function populateTable(backends, table) {
|
|
37
|
-
var _a, _b;
|
|
28
|
+
const backends = backendRes === null || backendRes === void 0 ? void 0 : backendRes.backends;
|
|
38
29
|
for (const backend of backends) {
|
|
39
|
-
const [
|
|
40
|
-
|
|
30
|
+
const [backendLocation, , backendId] = backend.name.split("/").slice(3, 6);
|
|
31
|
+
table.push([
|
|
41
32
|
backendId,
|
|
42
|
-
(_b = (_a = backend.codebase) === null || _a === void 0 ? void 0 : _a.repository) === null || _b === void 0 ? void 0 : _b.split("/").pop(),
|
|
43
|
-
|
|
44
|
-
backend.uri,
|
|
45
|
-
backend.createTime,
|
|
46
|
-
backend.updateTime,
|
|
47
|
-
];
|
|
48
|
-
const newRow = entry.map((name) => {
|
|
49
|
-
const maxCellWidth = COLUMN_LENGTH - 2;
|
|
50
|
-
const chunks = [];
|
|
51
|
-
for (let i = 0; name && i < name.length; i += maxCellWidth) {
|
|
52
|
-
chunks.push(name.substring(i, i + maxCellWidth));
|
|
53
|
-
}
|
|
54
|
-
return chunks.join("\n");
|
|
55
|
-
});
|
|
56
|
-
table.push(newRow);
|
|
33
|
+
(_c = (_b = (_a = backend.codebase) === null || _a === void 0 ? void 0 : _a.repository) === null || _b === void 0 ? void 0 : _b.split("/").pop()) !== null && _c !== void 0 ? _c : "",
|
|
34
|
+
backendLocation,
|
|
35
|
+
backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
|
|
36
|
+
(0, utils_1.datetimeString)(new Date(backend.createTime)),
|
|
37
|
+
(0, utils_1.datetimeString)(new Date(backend.updateTime)),
|
|
38
|
+
]);
|
|
57
39
|
}
|
|
58
|
-
|
|
40
|
+
logger_1.logger.info(table.toString());
|
|
41
|
+
return backends;
|
|
42
|
+
});
|
|
@@ -4,7 +4,6 @@ exports.command = void 0;
|
|
|
4
4
|
const apphosting = require("../gcp/apphosting");
|
|
5
5
|
const logger_1 = require("../logger");
|
|
6
6
|
const command_1 = require("../command");
|
|
7
|
-
const utils_1 = require("../utils");
|
|
8
7
|
const projectUtils_1 = require("../projectUtils");
|
|
9
8
|
exports.command = new command_1.Command("apphosting:builds:create <backendId>")
|
|
10
9
|
.description("Create a build for an App Hosting backend")
|
|
@@ -15,7 +14,8 @@ exports.command = new command_1.Command("apphosting:builds:create <backendId>")
|
|
|
15
14
|
.action(async (backendId, options) => {
|
|
16
15
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
17
16
|
const location = options.location;
|
|
18
|
-
const buildId = options.buildId ||
|
|
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 + ")");
|
|
@@ -125,6 +125,11 @@ async function convertConfig(context, functionsPayload, deploy) {
|
|
|
125
125
|
region: endpoint.region,
|
|
126
126
|
} });
|
|
127
127
|
if (rewrite.function.pinTag) {
|
|
128
|
+
if (endpoint.minInstances) {
|
|
129
|
+
throw new error_1.FirebaseError(`Function ${endpoint.id} has minInstances set and is in a rewrite ` +
|
|
130
|
+
"pinTags=true. These features are not currently compatible with each " +
|
|
131
|
+
"other.");
|
|
132
|
+
}
|
|
128
133
|
experiments.assertEnabled("pintags", "pin a function version");
|
|
129
134
|
apiRewrite.run.tag = runTags.TODO_TAG_NAME;
|
|
130
135
|
}
|
|
@@ -154,7 +154,8 @@ async function unsafePins(context, config) {
|
|
|
154
154
|
const existingUntaggedRewrites = {};
|
|
155
155
|
for (const rewrite of ((_d = (_c = (_b = channelConfig === null || channelConfig === void 0 ? void 0 : channelConfig.release) === null || _b === void 0 ? void 0 : _b.version) === null || _c === void 0 ? void 0 : _c.config) === null || _d === void 0 ? void 0 : _d.rewrites) || []) {
|
|
156
156
|
if ("run" in rewrite && !rewrite.run.tag) {
|
|
157
|
-
existingUntaggedRewrites[rewriteTarget(rewrite)] =
|
|
157
|
+
existingUntaggedRewrites[rewriteTarget(rewrite)] =
|
|
158
|
+
`${rewrite.run.region}/${rewrite.run.serviceId}`;
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
return Object.keys(targetTaggedRewrites).filter((target) => targetTaggedRewrites[target] === existingUntaggedRewrites[target]);
|
|
@@ -159,7 +159,7 @@ function registerHandlers(app, getProjectStateByApiKey) {
|
|
|
159
159
|
res.set("Content-Type", "text/html; charset=utf-8");
|
|
160
160
|
const apiKey = req.query.apiKey;
|
|
161
161
|
const providerId = req.query.providerId;
|
|
162
|
-
const tenantId = req.query.tenantId;
|
|
162
|
+
const tenantId = (req.query.tenantId || req.query.tid);
|
|
163
163
|
if (!apiKey || !providerId) {
|
|
164
164
|
return res.status(400).json({
|
|
165
165
|
authEmulator: {
|