firebase-tools 13.26.0 → 13.28.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 +2 -1
- package/lib/appdistribution/options-parser-util.js +5 -5
- package/lib/apphosting/config.js +86 -1
- package/lib/apphosting/secrets/index.js +1 -39
- package/lib/apphosting/yaml.js +3 -0
- package/lib/auth.js +13 -3
- package/lib/bin/firebase.js +4 -3
- package/lib/commands/appdistribution-distribute.js +55 -31
- package/lib/commands/apphosting-config-export.js +4 -26
- package/lib/commands/firestore-indexes-list.js +4 -2
- package/lib/commands/index.js +4 -6
- package/lib/dataconnect/schemaMigration.js +8 -15
- package/lib/dataconnect/types.js +1 -1
- package/lib/deploy/extensions/tasks.js +2 -2
- package/lib/deploy/functions/release/fabricator.js +3 -3
- package/lib/emulator/apphosting/developmentServer.js +32 -0
- package/lib/emulator/apphosting/index.js +5 -4
- package/lib/emulator/apphosting/serve.js +8 -7
- package/lib/emulator/auth/operations.js +8 -5
- package/lib/emulator/commandUtils.js +2 -1
- package/lib/emulator/controller.js +31 -18
- package/lib/emulator/dataconnect/pgliteServer.js +51 -18
- package/lib/emulator/dataconnectEmulator.js +36 -2
- package/lib/emulator/downloadableEmulators.js +23 -10
- package/lib/emulator/eventarcEmulator.js +1 -0
- package/lib/emulator/hub.js +16 -0
- package/lib/emulator/hubExport.js +23 -0
- package/lib/emulator/initEmulators.js +64 -0
- package/lib/emulator/types.js +2 -2
- package/lib/error.js +9 -1
- package/lib/experiments.js +0 -6
- package/lib/extensions/localHelper.js +2 -2
- package/lib/extensions/runtimes/node.js +16 -15
- package/lib/extensions/types.js +6 -9
- package/lib/gcp/cloudfunctionsv2.js +1 -0
- package/lib/init/features/dataconnect/index.js +4 -5
- package/lib/init/features/emulators.js +8 -0
- package/lib/init/features/genkit/index.js +13 -5
- package/lib/init/spawn.js +38 -7
- package/lib/requireAuth.js +2 -1
- package/lib/utils.js +16 -1
- package/package.json +3 -2
- package/schema/firebase-config.json +6 -0
- package/templates/init/dataconnect/dataconnect.yaml +1 -0
- package/lib/emulator/apphosting/utils.js +0 -18
- package/templates/init/dataconnect/dataconnect-fdccompatiblemode.yaml +0 -12
|
@@ -231,7 +231,7 @@ class AppDistributionClient {
|
|
|
231
231
|
}
|
|
232
232
|
utils.logSuccess(`Testers removed from group successfully`);
|
|
233
233
|
}
|
|
234
|
-
async createReleaseTest(releaseName, devices, loginCredential) {
|
|
234
|
+
async createReleaseTest(releaseName, devices, loginCredential, testCaseName) {
|
|
235
235
|
try {
|
|
236
236
|
const response = await this.appDistroV1AlphaClient.request({
|
|
237
237
|
method: "POST",
|
|
@@ -239,6 +239,7 @@ class AppDistributionClient {
|
|
|
239
239
|
body: {
|
|
240
240
|
deviceExecutions: devices.map(types_1.mapDeviceToExecution),
|
|
241
241
|
loginCredential,
|
|
242
|
+
testCase: testCaseName,
|
|
242
243
|
},
|
|
243
244
|
});
|
|
244
245
|
return response.body;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getLoginCredential = exports.
|
|
3
|
+
exports.getLoginCredential = exports.parseTestDevices = exports.getAppName = exports.getProjectName = exports.ensureFileExists = exports.getEmails = exports.parseIntoStringArray = void 0;
|
|
4
4
|
const fs = require("fs-extra");
|
|
5
5
|
const error_1 = require("../error");
|
|
6
6
|
const projectUtils_1 = require("../projectUtils");
|
|
7
|
-
function
|
|
7
|
+
function parseIntoStringArray(value, file) {
|
|
8
8
|
if (!value && file) {
|
|
9
9
|
ensureFileExists(file);
|
|
10
10
|
value = fs.readFileSync(file, "utf8");
|
|
@@ -14,7 +14,7 @@ function getTestersOrGroups(value, file) {
|
|
|
14
14
|
}
|
|
15
15
|
return [];
|
|
16
16
|
}
|
|
17
|
-
exports.
|
|
17
|
+
exports.parseIntoStringArray = parseIntoStringArray;
|
|
18
18
|
function getEmails(emails, file) {
|
|
19
19
|
if (emails.length === 0) {
|
|
20
20
|
ensureFileExists(file);
|
|
@@ -49,7 +49,7 @@ function getAppName(options) {
|
|
|
49
49
|
return `projects/${appId.split(":")[1]}/apps/${appId}`;
|
|
50
50
|
}
|
|
51
51
|
exports.getAppName = getAppName;
|
|
52
|
-
function
|
|
52
|
+
function parseTestDevices(value, file) {
|
|
53
53
|
if (!value && file) {
|
|
54
54
|
ensureFileExists(file);
|
|
55
55
|
value = fs.readFileSync(file, "utf8");
|
|
@@ -63,7 +63,7 @@ function getTestDevices(value, file) {
|
|
|
63
63
|
.filter((entry) => !!entry)
|
|
64
64
|
.map((str) => parseTestDevice(str));
|
|
65
65
|
}
|
|
66
|
-
exports.
|
|
66
|
+
exports.parseTestDevices = parseTestDevices;
|
|
67
67
|
function parseTestDevice(testDeviceString) {
|
|
68
68
|
const entries = testDeviceString.split(",");
|
|
69
69
|
const allowedKeys = new Set(["model", "version", "orientation", "locale"]);
|
package/lib/apphosting/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadConfigForEnvironment = exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.listAppHostingFilesInPath = exports.discoverBackendRoot = exports.APPHOSTING_YAML_FILE_REGEX = exports.APPHOSTING_LOCAL_YAML_FILE = exports.APPHOSTING_BASE_YAML_FILE = void 0;
|
|
3
|
+
exports.loadConfigToExportSecrets = exports.loadConfigForEnvironment = exports.exportConfig = exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.listAppHostingFilesInPath = exports.discoverBackendRoot = exports.APPHOSTING_YAML_FILE_REGEX = exports.APPHOSTING_LOCAL_YAML_FILE = exports.APPHOSTING_BASE_YAML_FILE = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const yaml = require("yaml");
|
|
@@ -8,9 +8,17 @@ const fs = require("../fsutils");
|
|
|
8
8
|
const prompt = require("../prompt");
|
|
9
9
|
const dialogs = require("./secrets/dialogs");
|
|
10
10
|
const yaml_1 = require("./yaml");
|
|
11
|
+
const error_1 = require("../error");
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
const secrets_1 = require("./secrets");
|
|
14
|
+
const logger_1 = require("../logger");
|
|
15
|
+
const utils_2 = require("../utils");
|
|
16
|
+
const projects_1 = require("../management/projects");
|
|
11
17
|
exports.APPHOSTING_BASE_YAML_FILE = "apphosting.yaml";
|
|
12
18
|
exports.APPHOSTING_LOCAL_YAML_FILE = "apphosting.local.yaml";
|
|
13
19
|
exports.APPHOSTING_YAML_FILE_REGEX = /^apphosting(\.[a-z0-9_]+)?\.yaml$/;
|
|
20
|
+
const SECRET_CONFIG = "Secret";
|
|
21
|
+
const EXPORTABLE_CONFIG = [SECRET_CONFIG];
|
|
14
22
|
function discoverBackendRoot(cwd) {
|
|
15
23
|
let dir = cwd;
|
|
16
24
|
while (!fs.fileExistsSync((0, path_1.resolve)(dir, exports.APPHOSTING_BASE_YAML_FILE))) {
|
|
@@ -108,6 +116,49 @@ async function maybeAddSecretToYaml(secretName) {
|
|
|
108
116
|
dynamicDispatch.store(path, projectYaml);
|
|
109
117
|
}
|
|
110
118
|
exports.maybeAddSecretToYaml = maybeAddSecretToYaml;
|
|
119
|
+
async function exportConfig(cwd, projectRoot, backendRoot, projectId, userGivenConfigFile) {
|
|
120
|
+
const choices = await prompt.prompt({}, [
|
|
121
|
+
{
|
|
122
|
+
type: "checkbox",
|
|
123
|
+
name: "configurations",
|
|
124
|
+
message: "What configs would you like to export?",
|
|
125
|
+
choices: EXPORTABLE_CONFIG,
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
if (!choices.configurations.includes(SECRET_CONFIG)) {
|
|
129
|
+
logger_1.logger.info("No configs selected to export");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!projectId) {
|
|
133
|
+
const project = await (0, projects_1.getOrPromptProject)({});
|
|
134
|
+
projectId = project.projectId;
|
|
135
|
+
}
|
|
136
|
+
let localAppHostingConfig = yaml_1.AppHostingYamlConfig.empty();
|
|
137
|
+
const localAppHostingConfigPath = (0, path_1.resolve)(backendRoot, exports.APPHOSTING_LOCAL_YAML_FILE);
|
|
138
|
+
if (fs.fileExistsSync(localAppHostingConfigPath)) {
|
|
139
|
+
localAppHostingConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(localAppHostingConfigPath);
|
|
140
|
+
}
|
|
141
|
+
const configToExport = await loadConfigToExportSecrets(cwd, userGivenConfigFile);
|
|
142
|
+
const secretsToExport = configToExport.secrets;
|
|
143
|
+
if (!secretsToExport) {
|
|
144
|
+
logger_1.logger.info("No secrets found to export in the chosen App Hosting config files");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const secretMaterial = await (0, secrets_1.fetchSecrets)(projectId, secretsToExport);
|
|
148
|
+
for (const [key, value] of secretMaterial) {
|
|
149
|
+
localAppHostingConfig.addEnvironmentVariable({
|
|
150
|
+
variable: key,
|
|
151
|
+
value: value,
|
|
152
|
+
availability: ["RUNTIME"],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
localAppHostingConfig.clearSecrets();
|
|
156
|
+
localAppHostingConfig.upsertFile(localAppHostingConfigPath);
|
|
157
|
+
logger_1.logger.info(`Wrote secrets as environment variables to ${exports.APPHOSTING_LOCAL_YAML_FILE}.`);
|
|
158
|
+
(0, utils_2.updateOrCreateGitignore)(projectRoot, [exports.APPHOSTING_LOCAL_YAML_FILE]);
|
|
159
|
+
logger_1.logger.info(`${exports.APPHOSTING_LOCAL_YAML_FILE} has been automatically added to your .gitignore.`);
|
|
160
|
+
}
|
|
161
|
+
exports.exportConfig = exportConfig;
|
|
111
162
|
async function loadConfigForEnvironment(envYamlPath, baseYamlPath) {
|
|
112
163
|
const envYamlConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(envYamlPath);
|
|
113
164
|
if (baseYamlPath) {
|
|
@@ -118,3 +169,37 @@ async function loadConfigForEnvironment(envYamlPath, baseYamlPath) {
|
|
|
118
169
|
return envYamlConfig;
|
|
119
170
|
}
|
|
120
171
|
exports.loadConfigForEnvironment = loadConfigForEnvironment;
|
|
172
|
+
async function loadConfigToExportSecrets(cwd, userGivenConfigFile) {
|
|
173
|
+
if (userGivenConfigFile && !exports.APPHOSTING_YAML_FILE_REGEX.test(userGivenConfigFile)) {
|
|
174
|
+
throw new error_1.FirebaseError("Invalid apphosting yaml config file provided. File must be in format: 'apphosting.yaml' or 'apphosting.<environment>.yaml'");
|
|
175
|
+
}
|
|
176
|
+
const allConfigs = getValidConfigs(cwd);
|
|
177
|
+
let userGivenConfigFilePath;
|
|
178
|
+
if (userGivenConfigFile) {
|
|
179
|
+
if (!allConfigs.has(userGivenConfigFile)) {
|
|
180
|
+
throw new error_1.FirebaseError(`The provided app hosting config file "${userGivenConfigFile}" does not exist`);
|
|
181
|
+
}
|
|
182
|
+
userGivenConfigFilePath = allConfigs.get(userGivenConfigFile);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
userGivenConfigFilePath = await (0, utils_1.promptForAppHostingYaml)(allConfigs, "Which environment would you like to export secrets from Secret Manager for?");
|
|
186
|
+
}
|
|
187
|
+
if (userGivenConfigFile === exports.APPHOSTING_BASE_YAML_FILE) {
|
|
188
|
+
return yaml_1.AppHostingYamlConfig.loadFromFile(allConfigs.get(exports.APPHOSTING_BASE_YAML_FILE));
|
|
189
|
+
}
|
|
190
|
+
const baseFilePath = allConfigs.get(exports.APPHOSTING_BASE_YAML_FILE);
|
|
191
|
+
return await loadConfigForEnvironment(userGivenConfigFilePath, baseFilePath);
|
|
192
|
+
}
|
|
193
|
+
exports.loadConfigToExportSecrets = loadConfigToExportSecrets;
|
|
194
|
+
function getValidConfigs(cwd) {
|
|
195
|
+
const appHostingConfigPaths = listAppHostingFilesInPath(cwd).filter((path) => !path.endsWith(exports.APPHOSTING_LOCAL_YAML_FILE));
|
|
196
|
+
if (appHostingConfigPaths.length === 0) {
|
|
197
|
+
throw new error_1.FirebaseError("No apphosting.*.yaml configs found");
|
|
198
|
+
}
|
|
199
|
+
const fileNameToPathMap = new Map();
|
|
200
|
+
for (const path of appHostingConfigPaths) {
|
|
201
|
+
const fileName = (0, path_1.basename)(path);
|
|
202
|
+
fileNameToPathMap.set(fileName, path);
|
|
203
|
+
}
|
|
204
|
+
return fileNameToPathMap;
|
|
205
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getSecretNameParts = exports.
|
|
3
|
+
exports.getSecretNameParts = exports.fetchSecrets = exports.upsertSecret = exports.grantSecretAccess = exports.serviceAccountsForBackend = exports.toMulti = void 0;
|
|
4
4
|
const error_1 = require("../../error");
|
|
5
5
|
const gcsm = require("../../gcp/secretManager");
|
|
6
6
|
const gcb = require("../../gcp/cloudbuild");
|
|
@@ -10,10 +10,6 @@ const secretManager_1 = require("../../gcp/secretManager");
|
|
|
10
10
|
const secretManager_2 = require("../../gcp/secretManager");
|
|
11
11
|
const utils = require("../../utils");
|
|
12
12
|
const prompt = require("../../prompt");
|
|
13
|
-
const path_1 = require("path");
|
|
14
|
-
const config_1 = require("../config");
|
|
15
|
-
const utils_1 = require("../utils");
|
|
16
|
-
const yaml_1 = require("../yaml");
|
|
17
13
|
function toMulti(accounts) {
|
|
18
14
|
const m = {
|
|
19
15
|
buildServiceAccounts: [accounts.buildServiceAccount],
|
|
@@ -125,40 +121,6 @@ async function fetchSecrets(projectId, secrets) {
|
|
|
125
121
|
return secretsKeyValuePairs;
|
|
126
122
|
}
|
|
127
123
|
exports.fetchSecrets = fetchSecrets;
|
|
128
|
-
async function loadConfigToExport(cwd, userGivenConfigFile) {
|
|
129
|
-
if (userGivenConfigFile && !config_1.APPHOSTING_YAML_FILE_REGEX.test(userGivenConfigFile)) {
|
|
130
|
-
throw new error_1.FirebaseError("Invalid apphosting yaml config file provided. File must be in format: 'apphosting.yaml' or 'apphosting.<environment>.yaml'");
|
|
131
|
-
}
|
|
132
|
-
const allConfigs = getValidConfigs(cwd);
|
|
133
|
-
let userGivenConfigFilePath;
|
|
134
|
-
if (userGivenConfigFile) {
|
|
135
|
-
if (!allConfigs.has(userGivenConfigFile)) {
|
|
136
|
-
throw new error_1.FirebaseError(`The provided app hosting config file "${userGivenConfigFile}" does not exist`);
|
|
137
|
-
}
|
|
138
|
-
userGivenConfigFilePath = allConfigs.get(userGivenConfigFile);
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
userGivenConfigFilePath = await (0, utils_1.promptForAppHostingYaml)(allConfigs, "Which environment would you like to export secrets from Secret Manager for?");
|
|
142
|
-
}
|
|
143
|
-
if (userGivenConfigFile === config_1.APPHOSTING_BASE_YAML_FILE) {
|
|
144
|
-
return yaml_1.AppHostingYamlConfig.loadFromFile(allConfigs.get(config_1.APPHOSTING_BASE_YAML_FILE));
|
|
145
|
-
}
|
|
146
|
-
const baseFilePath = allConfigs.get(config_1.APPHOSTING_BASE_YAML_FILE);
|
|
147
|
-
return await (0, config_1.loadConfigForEnvironment)(userGivenConfigFilePath, baseFilePath);
|
|
148
|
-
}
|
|
149
|
-
exports.loadConfigToExport = loadConfigToExport;
|
|
150
|
-
function getValidConfigs(cwd) {
|
|
151
|
-
const appHostingConfigPaths = (0, config_1.listAppHostingFilesInPath)(cwd).filter((path) => !path.endsWith(config_1.APPHOSTING_LOCAL_YAML_FILE));
|
|
152
|
-
if (appHostingConfigPaths.length === 0) {
|
|
153
|
-
throw new error_1.FirebaseError("No apphosting.*.yaml configs found");
|
|
154
|
-
}
|
|
155
|
-
const fileNameToPathMap = new Map();
|
|
156
|
-
for (const path of appHostingConfigPaths) {
|
|
157
|
-
const fileName = (0, path_1.basename)(path);
|
|
158
|
-
fileNameToPathMap.set(fileName, path);
|
|
159
|
-
}
|
|
160
|
-
return fileNameToPathMap;
|
|
161
|
-
}
|
|
162
124
|
function getSecretNameParts(secret) {
|
|
163
125
|
let [name, version] = secret.split("@");
|
|
164
126
|
if (!version) {
|
package/lib/apphosting/yaml.js
CHANGED
|
@@ -43,6 +43,9 @@ class AppHostingYamlConfig {
|
|
|
43
43
|
addSecret(secret) {
|
|
44
44
|
this._secrets.set(secret.variable, secret);
|
|
45
45
|
}
|
|
46
|
+
clearSecrets() {
|
|
47
|
+
this._secrets.clear();
|
|
48
|
+
}
|
|
46
49
|
merge(other) {
|
|
47
50
|
for (const [key, value] of other._environmentVariables) {
|
|
48
51
|
this._environmentVariables.set(key, value);
|
package/lib/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addAdditionalAccount = exports.logout = exports.getAccessToken = exports.haveValidTokens = exports.loggedIn = exports.findAccountByEmail = exports.loginGithub = exports.loginGoogle = exports.setGlobalDefaultAccount = exports.setProjectAccount = exports.loginAdditionalAccount = exports.selectAccount = exports.setRefreshToken = exports.setActiveAccount = exports.getAllAccounts = exports.getAdditionalAccounts = exports.getProjectDefaultAccount = exports.getGlobalDefaultAccount = void 0;
|
|
3
|
+
exports.addAdditionalAccount = exports.logout = exports.getAccessToken = exports.haveValidTokens = exports.isExpired = exports.loggedIn = exports.findAccountByEmail = exports.loginGithub = exports.loginGoogle = exports.setGlobalDefaultAccount = exports.setProjectAccount = exports.loginAdditionalAccount = exports.selectAccount = exports.setRefreshToken = exports.setActiveAccount = exports.getAllAccounts = exports.getAdditionalAccounts = exports.getProjectDefaultAccount = exports.getGlobalDefaultAccount = void 0;
|
|
4
4
|
const clc = require("colorette");
|
|
5
5
|
const FormData = require("form-data");
|
|
6
6
|
const http = require("http");
|
|
@@ -401,6 +401,16 @@ function loggedIn() {
|
|
|
401
401
|
return !!lastAccessToken;
|
|
402
402
|
}
|
|
403
403
|
exports.loggedIn = loggedIn;
|
|
404
|
+
function isExpired(tokens) {
|
|
405
|
+
const hasExpiration = (p) => !!p.expires_at;
|
|
406
|
+
if (hasExpiration(tokens)) {
|
|
407
|
+
return !(tokens && tokens.expires_at && tokens.expires_at > Date.now());
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
return !tokens;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
exports.isExpired = isExpired;
|
|
404
414
|
function haveValidTokens(refreshToken, authScopes) {
|
|
405
415
|
var _a;
|
|
406
416
|
if (!(lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.access_token)) {
|
|
@@ -413,8 +423,8 @@ function haveValidTokens(refreshToken, authScopes) {
|
|
|
413
423
|
const oldScopesJSON = JSON.stringify(((_a = lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.scopes) === null || _a === void 0 ? void 0 : _a.sort()) || []);
|
|
414
424
|
const newScopesJSON = JSON.stringify(authScopes.sort());
|
|
415
425
|
const hasSameScopes = oldScopesJSON === newScopesJSON;
|
|
416
|
-
const
|
|
417
|
-
const valid = hasTokens && hasSameScopes && !
|
|
426
|
+
const expired = ((lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.expires_at) || 0) < Date.now() + FIFTEEN_MINUTES_IN_MS;
|
|
427
|
+
const valid = hasTokens && hasSameScopes && !expired;
|
|
418
428
|
if (hasTokens) {
|
|
419
429
|
logger_1.logger.debug(`Checked if tokens are valid: ${valid}, expires at: ${lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.expires_at}`);
|
|
420
430
|
}
|
package/lib/bin/firebase.js
CHANGED
|
@@ -26,7 +26,7 @@ const client = require("..");
|
|
|
26
26
|
const fsutils = require("../fsutils");
|
|
27
27
|
const utils = require("../utils");
|
|
28
28
|
const winston = require("winston");
|
|
29
|
-
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
30
|
let cmd;
|
|
31
31
|
function findAvailableLogFile() {
|
|
32
32
|
const candidates = ["firebase-debug.log"];
|
|
@@ -126,9 +126,10 @@ process.on("uncaughtException", (err) => {
|
|
|
126
126
|
(0, errorOut_1.errorOut)(err);
|
|
127
127
|
});
|
|
128
128
|
if (!(0, handlePreviewToggles_1.handlePreviewToggles)(args)) {
|
|
129
|
-
cmd = client.cli.parse(process.argv);
|
|
130
|
-
args = args.filter((arg) => !arg.includes("-"));
|
|
131
129
|
if (!args.length) {
|
|
132
130
|
client.cli.help();
|
|
133
131
|
}
|
|
132
|
+
else {
|
|
133
|
+
cmd = client.cli.parse(process.argv);
|
|
134
|
+
}
|
|
134
135
|
}
|
|
@@ -23,14 +23,14 @@ function getReleaseNotes(releaseNotes, releaseNotesFile) {
|
|
|
23
23
|
return "";
|
|
24
24
|
}
|
|
25
25
|
exports.command = new command_1.Command("appdistribution:distribute <release-binary-file>")
|
|
26
|
-
.description("upload a release binary")
|
|
26
|
+
.description("upload a release binary and optionally distribute it to testers and run automated tests")
|
|
27
27
|
.option("--app <app_id>", "the app id of your Firebase app")
|
|
28
28
|
.option("--release-notes <string>", "release notes to include")
|
|
29
29
|
.option("--release-notes-file <file>", "path to file with release notes")
|
|
30
|
-
.option("--testers <string>", "a comma
|
|
31
|
-
.option("--testers-file <file>", "path to file with a comma separated list of tester emails to distribute to")
|
|
32
|
-
.option("--groups <string>", "a comma
|
|
33
|
-
.option("--groups-file <file>", "path to file with a comma separated list of group aliases to distribute to")
|
|
30
|
+
.option("--testers <string>", "a comma-separated list of tester emails to distribute to")
|
|
31
|
+
.option("--testers-file <file>", "path to file with a comma- or newline-separated list of tester emails to distribute to")
|
|
32
|
+
.option("--groups <string>", "a comma-separated list of group aliases to distribute to")
|
|
33
|
+
.option("--groups-file <file>", "path to file with a comma- or newline-separated list of group aliases to distribute to")
|
|
34
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
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
36
|
.option("--test-username <string>", "username for automatic login")
|
|
@@ -39,14 +39,20 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
|
|
|
39
39
|
.option("--test-username-resource <string>", "resource name for the username field for automatic login")
|
|
40
40
|
.option("--test-password-resource <string>", "resource name for the password field for automatic login")
|
|
41
41
|
.option("--test-non-blocking", "run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
|
|
42
|
+
.option("--test-case-ids <string>", "a comma-separated list of test case IDs.")
|
|
43
|
+
.option("--test-case-ids-file <file>", "path to file with a comma- or newline-separated list of test case IDs.")
|
|
42
44
|
.before(requireAuth_1.requireAuth)
|
|
43
45
|
.action(async (file, options) => {
|
|
44
46
|
const appName = (0, options_parser_util_1.getAppName)(options);
|
|
45
47
|
const distribution = new distribution_1.Distribution(file);
|
|
46
48
|
const releaseNotes = getReleaseNotes(options.releaseNotes, options.releaseNotesFile);
|
|
47
|
-
const testers = (0, options_parser_util_1.
|
|
48
|
-
const groups = (0, options_parser_util_1.
|
|
49
|
-
const
|
|
49
|
+
const testers = (0, options_parser_util_1.parseIntoStringArray)(options.testers, options.testersFile);
|
|
50
|
+
const groups = (0, options_parser_util_1.parseIntoStringArray)(options.groups, options.groupsFile);
|
|
51
|
+
const testCases = (0, options_parser_util_1.parseIntoStringArray)(options.testCaseIds, options.testCaseIdsFile);
|
|
52
|
+
const testDevices = (0, options_parser_util_1.parseTestDevices)(options.testDevices, options.testDevicesFile);
|
|
53
|
+
if (testCases.length && (options.testUsernameResource || options.testPasswordResource)) {
|
|
54
|
+
throw new error_1.FirebaseError("Password and username resource names are not supported for the AI testing agent.");
|
|
55
|
+
}
|
|
50
56
|
const loginCredential = (0, options_parser_util_1.getLoginCredential)({
|
|
51
57
|
username: options.testUsername,
|
|
52
58
|
password: options.testPassword,
|
|
@@ -137,39 +143,57 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
|
|
|
137
143
|
}
|
|
138
144
|
await requests.updateReleaseNotes(releaseName, releaseNotes);
|
|
139
145
|
await requests.distribute(releaseName, testers, groups);
|
|
140
|
-
if (testDevices
|
|
141
|
-
utils.logBullet("starting automated
|
|
142
|
-
const
|
|
143
|
-
|
|
146
|
+
if (testDevices.length) {
|
|
147
|
+
utils.logBullet("starting automated test (note: this feature is in beta)");
|
|
148
|
+
const releaseTestPromises = [];
|
|
149
|
+
if (!testCases.length) {
|
|
150
|
+
releaseTestPromises.push(requests.createReleaseTest(releaseName, testDevices, loginCredential));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
for (const testCaseId of testCases) {
|
|
154
|
+
releaseTestPromises.push(requests.createReleaseTest(releaseName, testDevices, loginCredential, `${appName}/testCases/${testCaseId}`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const releaseTests = await Promise.all(releaseTestPromises);
|
|
158
|
+
utils.logSuccess(`${releaseTests.length} release test(s) started successfully`);
|
|
144
159
|
if (!options.testNonBlocking) {
|
|
145
|
-
await awaitTestResults(
|
|
160
|
+
await awaitTestResults(releaseTests, requests);
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
163
|
});
|
|
149
|
-
async function awaitTestResults(
|
|
164
|
+
async function awaitTestResults(releaseTests, requests) {
|
|
165
|
+
const releaseTestNames = new Set(releaseTests.map((rt) => rt.name));
|
|
150
166
|
for (let i = 0; i < TEST_MAX_POLLING_RETRIES; i++) {
|
|
151
|
-
utils.logBullet(
|
|
167
|
+
utils.logBullet(`${releaseTestNames.size} automated test results are pending...`);
|
|
152
168
|
await delay(TEST_POLLING_INTERVAL_MILLIS);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
for (const releaseTestName of releaseTestNames) {
|
|
170
|
+
const releaseTest = await requests.getReleaseTest(releaseTestName);
|
|
171
|
+
if (releaseTest.deviceExecutions.every((e) => e.state === "PASSED")) {
|
|
172
|
+
releaseTestNames.delete(releaseTestName);
|
|
173
|
+
if (releaseTestNames.size === 0) {
|
|
174
|
+
utils.logSuccess("Automated test(s) passed!");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
162
178
|
continue;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const execution of releaseTest.deviceExecutions) {
|
|
182
|
+
switch (execution.state) {
|
|
183
|
+
case "PASSED":
|
|
184
|
+
case "IN_PROGRESS":
|
|
185
|
+
continue;
|
|
186
|
+
case "FAILED":
|
|
187
|
+
throw new error_1.FirebaseError(`Automated test failed for ${deviceToString(execution.device)}: ${execution.failedReason}`, { exit: 1 });
|
|
188
|
+
case "INCONCLUSIVE":
|
|
189
|
+
throw new error_1.FirebaseError(`Automated test inconclusive for ${deviceToString(execution.device)}: ${execution.inconclusiveReason}`, { exit: 1 });
|
|
190
|
+
default:
|
|
191
|
+
throw new error_1.FirebaseError(`Unsupported automated test state for ${deviceToString(execution.device)}: ${execution.state}`, { exit: 1 });
|
|
192
|
+
}
|
|
169
193
|
}
|
|
170
194
|
}
|
|
171
195
|
}
|
|
172
|
-
throw new error_1.FirebaseError("It took longer than expected to
|
|
196
|
+
throw new error_1.FirebaseError("It took longer than expected to run your test(s), please try again.", {
|
|
173
197
|
exit: 1,
|
|
174
198
|
});
|
|
175
199
|
}
|
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.command = void 0;
|
|
4
4
|
const command_1 = require("../command");
|
|
5
|
-
const logger_1 = require("../logger");
|
|
6
5
|
const projectUtils_1 = require("../projectUtils");
|
|
7
6
|
const requireAuth_1 = require("../requireAuth");
|
|
8
7
|
const secretManager = require("../gcp/secretManager");
|
|
9
8
|
const requirePermissions_1 = require("../requirePermissions");
|
|
10
9
|
const config_1 = require("../apphosting/config");
|
|
11
|
-
const secrets_1 = require("../apphosting/secrets");
|
|
12
|
-
const path_1 = require("path");
|
|
13
|
-
const fs = require("../fsutils");
|
|
14
|
-
const yaml_1 = require("../apphosting/yaml");
|
|
15
10
|
const error_1 = require("../error");
|
|
11
|
+
const detectProjectRoot_1 = require("../detectProjectRoot");
|
|
16
12
|
exports.command = new command_1.Command("apphosting:config:export")
|
|
17
13
|
.description("Export App Hosting configurations such as secrets into an apphosting.local.yaml file")
|
|
18
14
|
.option("-s, --secrets <apphosting.yaml or apphosting.<environment>.yaml file to export secrets from>", "This command combines the base apphosting.yaml with the specified environment-specific file (e.g., apphosting.staging.yaml). If keys conflict, the environment-specific file takes precedence.")
|
|
@@ -20,32 +16,14 @@ exports.command = new command_1.Command("apphosting:config:export")
|
|
|
20
16
|
.before(secretManager.ensureApi)
|
|
21
17
|
.before(requirePermissions_1.requirePermissions, ["secretmanager.versions.access"])
|
|
22
18
|
.action(async (options) => {
|
|
19
|
+
var _a;
|
|
23
20
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
24
21
|
const environmentConfigFile = options.secrets;
|
|
25
22
|
const cwd = process.cwd();
|
|
26
|
-
let localAppHostingConfig = yaml_1.AppHostingYamlConfig.empty();
|
|
27
23
|
const backendRoot = (0, config_1.discoverBackendRoot)(cwd);
|
|
28
24
|
if (!backendRoot) {
|
|
29
25
|
throw new error_1.FirebaseError("Missing apphosting.yaml: This command requires an apphosting.yaml configuration file. Please run 'firebase init apphosting' and try again.");
|
|
30
26
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
localAppHostingConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(localAppHostingConfigPath);
|
|
34
|
-
}
|
|
35
|
-
const configToExport = await (0, secrets_1.loadConfigToExport)(cwd, environmentConfigFile);
|
|
36
|
-
const secretsToExport = configToExport.secrets;
|
|
37
|
-
if (!secretsToExport) {
|
|
38
|
-
logger_1.logger.warn("No secrets found to export in the chosen App Hosting config files");
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const secretMaterial = await (0, secrets_1.fetchSecrets)(projectId, secretsToExport);
|
|
42
|
-
for (const [key, value] of secretMaterial) {
|
|
43
|
-
localAppHostingConfig.addEnvironmentVariable({
|
|
44
|
-
variable: key,
|
|
45
|
-
value: value,
|
|
46
|
-
availability: ["RUNTIME"],
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
localAppHostingConfig.upsertFile(localAppHostingConfigPath);
|
|
50
|
-
logger_1.logger.info(`Wrote secrets as environment variables to ${config_1.APPHOSTING_LOCAL_YAML_FILE}.`);
|
|
27
|
+
const projectRoot = (_a = (0, detectProjectRoot_1.detectProjectRoot)({})) !== null && _a !== void 0 ? _a : backendRoot;
|
|
28
|
+
await (0, config_1.exportConfig)(cwd, projectRoot, backendRoot, projectId, environmentConfigFile);
|
|
51
29
|
});
|
|
@@ -9,6 +9,7 @@ const requirePermissions_1 = require("../requirePermissions");
|
|
|
9
9
|
const types_1 = require("../emulator/types");
|
|
10
10
|
const commandUtils_1 = require("../emulator/commandUtils");
|
|
11
11
|
const pretty_print_1 = require("../firestore/pretty-print");
|
|
12
|
+
const projectUtils_1 = require("../projectUtils");
|
|
12
13
|
exports.command = new command_1.Command("firestore:indexes")
|
|
13
14
|
.description("List indexes in your project's Cloud Firestore database.")
|
|
14
15
|
.option("--pretty", "Pretty print. When not specified the indexes are printed in the " +
|
|
@@ -21,8 +22,9 @@ exports.command = new command_1.Command("firestore:indexes")
|
|
|
21
22
|
const indexApi = new fsi.FirestoreApi();
|
|
22
23
|
const printer = new pretty_print_1.PrettyPrint();
|
|
23
24
|
const databaseId = (_a = options.database) !== null && _a !== void 0 ? _a : "(default)";
|
|
24
|
-
const
|
|
25
|
-
const
|
|
25
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
26
|
+
const indexes = await indexApi.listIndexes(projectId, databaseId);
|
|
27
|
+
const fieldOverrides = await indexApi.listFieldOverrides(projectId, databaseId);
|
|
26
28
|
const indexSpec = indexApi.makeIndexSpec(indexes, fieldOverrides);
|
|
27
29
|
if (options.pretty) {
|
|
28
30
|
logger_1.logger.info(clc.bold(clc.white("Compound Indexes")));
|
package/lib/commands/index.js
CHANGED
|
@@ -172,20 +172,18 @@ function load(client) {
|
|
|
172
172
|
client.apphosting.secrets.grantaccess = loadCommand("apphosting-secrets-grantaccess");
|
|
173
173
|
client.apphosting.secrets.describe = loadCommand("apphosting-secrets-describe");
|
|
174
174
|
client.apphosting.secrets.access = loadCommand("apphosting-secrets-access");
|
|
175
|
+
client.apphosting.rollouts = {};
|
|
176
|
+
client.apphosting.rollouts.create = loadCommand("apphosting-rollouts-create");
|
|
177
|
+
client.apphosting.config = {};
|
|
178
|
+
client.apphosting.config.export = loadCommand("apphosting-config-export");
|
|
175
179
|
if (experiments.isEnabled("internaltesting")) {
|
|
176
180
|
client.apphosting.builds = {};
|
|
177
181
|
client.apphosting.builds.get = loadCommand("apphosting-builds-get");
|
|
178
182
|
client.apphosting.builds.create = loadCommand("apphosting-builds-create");
|
|
179
183
|
client.apphosting.repos = {};
|
|
180
184
|
client.apphosting.repos.create = loadCommand("apphosting-repos-create");
|
|
181
|
-
client.apphosting.rollouts = {};
|
|
182
|
-
client.apphosting.rollouts.create = loadCommand("apphosting-rollouts-create");
|
|
183
185
|
client.apphosting.rollouts.list = loadCommand("apphosting-rollouts-list");
|
|
184
186
|
}
|
|
185
|
-
if (experiments.isEnabled("emulatorapphosting")) {
|
|
186
|
-
client.apphosting.config = {};
|
|
187
|
-
client.apphosting.config.export = loadCommand("apphosting-config-export");
|
|
188
|
-
}
|
|
189
187
|
}
|
|
190
188
|
client.login = loadCommand("login");
|
|
191
189
|
client.login.add = loadCommand("login-add");
|
|
@@ -13,18 +13,15 @@ const prompt_1 = require("../prompt");
|
|
|
13
13
|
const logger_1 = require("../logger");
|
|
14
14
|
const error_1 = require("../error");
|
|
15
15
|
const utils_1 = require("../utils");
|
|
16
|
-
const experiments = require("../experiments");
|
|
17
16
|
const errors = require("./errors");
|
|
18
17
|
async function diffSchema(schema, schemaValidation) {
|
|
19
18
|
const { serviceName, instanceName, databaseId } = getIdentifiers(schema);
|
|
20
19
|
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
|
|
21
20
|
let diffs = [];
|
|
22
|
-
let validationMode =
|
|
23
|
-
? schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE"
|
|
24
|
-
: "STRICT";
|
|
21
|
+
let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
|
|
25
22
|
setSchemaValidationMode(schema, validationMode);
|
|
26
23
|
try {
|
|
27
|
-
if (!schemaValidation
|
|
24
|
+
if (!schemaValidation) {
|
|
28
25
|
(0, utils_1.logLabeledBullet)("dataconnect", `generating required schema changes...`);
|
|
29
26
|
}
|
|
30
27
|
await (0, client_1.upsertSchema)(schema, true);
|
|
@@ -52,7 +49,7 @@ async function diffSchema(schema, schemaValidation) {
|
|
|
52
49
|
diffs = incompatible.diffs;
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
|
-
if (
|
|
52
|
+
if (!schemaValidation) {
|
|
56
53
|
validationMode = "STRICT";
|
|
57
54
|
setSchemaValidationMode(schema, validationMode);
|
|
58
55
|
try {
|
|
@@ -89,9 +86,7 @@ async function migrateSchema(args) {
|
|
|
89
86
|
const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
|
|
90
87
|
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, true);
|
|
91
88
|
let diffs = [];
|
|
92
|
-
let validationMode =
|
|
93
|
-
? schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE"
|
|
94
|
-
: "STRICT";
|
|
89
|
+
let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
|
|
95
90
|
setSchemaValidationMode(schema, validationMode);
|
|
96
91
|
try {
|
|
97
92
|
await (0, client_1.upsertSchema)(schema, validateOnly);
|
|
@@ -124,7 +119,7 @@ async function migrateSchema(args) {
|
|
|
124
119
|
await (0, client_1.upsertSchema)(schema, validateOnly);
|
|
125
120
|
}
|
|
126
121
|
}
|
|
127
|
-
if (
|
|
122
|
+
if (!schemaValidation) {
|
|
128
123
|
validationMode = "STRICT";
|
|
129
124
|
setSchemaValidationMode(schema, validationMode);
|
|
130
125
|
try {
|
|
@@ -185,11 +180,9 @@ function diffsEqual(x, y) {
|
|
|
185
180
|
return true;
|
|
186
181
|
}
|
|
187
182
|
function setSchemaValidationMode(schema, schemaValidation) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
postgresDatasource.postgresql.schemaValidation = schemaValidation;
|
|
192
|
-
}
|
|
183
|
+
const postgresDatasource = schema.datasources.find((d) => d.postgresql);
|
|
184
|
+
if (postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) {
|
|
185
|
+
postgresDatasource.postgresql.schemaValidation = schemaValidation;
|
|
193
186
|
}
|
|
194
187
|
}
|
|
195
188
|
function getIdentifiers(schema) {
|
package/lib/dataconnect/types.js
CHANGED
|
@@ -17,7 +17,7 @@ var Platform;
|
|
|
17
17
|
Platform["MULTIPLE"] = "MULTIPLE";
|
|
18
18
|
})(Platform = exports.Platform || (exports.Platform = {}));
|
|
19
19
|
function toDatasource(projectId, locationId, ds) {
|
|
20
|
-
if (ds.postgresql) {
|
|
20
|
+
if (ds === null || ds === void 0 ? void 0 : ds.postgresql) {
|
|
21
21
|
return {
|
|
22
22
|
postgresql: {
|
|
23
23
|
database: ds.postgresql.database,
|
|
@@ -7,7 +7,7 @@ const extensionsApi = require("../../extensions/extensionsApi");
|
|
|
7
7
|
const extensionsHelper_1 = require("../../extensions/extensionsHelper");
|
|
8
8
|
const refs = require("../../extensions/refs");
|
|
9
9
|
const utils = require("../../utils");
|
|
10
|
-
const
|
|
10
|
+
const error_2 = require("../../error");
|
|
11
11
|
const isRetryable = (err) => err.status === 429 || err.status === 409;
|
|
12
12
|
function extensionsDeploymentHandler(errorHandler) {
|
|
13
13
|
return async (task) => {
|
|
@@ -54,7 +54,7 @@ function createExtensionInstanceTask(projectId, instanceSpec, validateOnly = fal
|
|
|
54
54
|
await extensionsApi.createInstance(createArgs);
|
|
55
55
|
}
|
|
56
56
|
catch (err) {
|
|
57
|
-
if ((0,
|
|
57
|
+
if ((0, error_2.isObject)(err) && err.status === 409) {
|
|
58
58
|
throw new error_1.FirebaseError(`Failed to create extension instance. Extension instance ${clc.bold(instanceSpec.instanceId)} already exists.`);
|
|
59
59
|
}
|
|
60
60
|
throw err;
|