firebase-tools 15.8.0 → 15.9.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/apiv2.js +13 -10
- package/lib/appdistribution/client.js +2 -2
- package/lib/appdistribution/yaml_helper.js +24 -16
- package/lib/apphosting/secrets/dialogs.js +3 -3
- package/lib/apphosting/secrets/index.js +59 -0
- package/lib/apptesting/parseTestFiles.js +2 -1
- package/lib/bin/mcp.js +2 -1
- package/lib/commands/apphosting-secrets-set.js +2 -53
- package/lib/commands/apptesting.js +2 -2
- package/lib/commands/studio-export.js +13 -4
- package/lib/dataconnect/schemaMigration.js +12 -1
- package/lib/deploy/apphosting/release.js +7 -1
- package/lib/deploy/extensions/prepare.js +13 -4
- package/lib/downloadUtils.js +3 -1
- package/lib/emulator/download.js +15 -1
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/emulator/downloadableEmulators.js +33 -6
- package/lib/env.js +6 -1
- package/lib/experiments.js +1 -1
- package/lib/firebase_studio/migrate.js +378 -0
- package/lib/functionsConfig.js +3 -3
- package/lib/gcp/cloudsql/cloudsqladmin.js +8 -3
- package/lib/gcp/cloudsql/fbToolsAuthClient.js +1 -1
- package/lib/mcp/prompts/apptesting/run_test.js +6 -5
- package/lib/mcp/tools/apptesting/tests.js +4 -4
- package/lib/mcp/tools/firestore/converter.js +101 -0
- package/lib/mcp/tools/firestore/index.js +5 -0
- package/lib/mcp/tools/firestore/query_collection.js +132 -0
- package/lib/mcp/tools/index.js +2 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/templates/firebase-studio-export/readme_template.md +11 -0
- package/templates/firebase-studio-export/system_instructions_template.md +14 -0
- package/templates/firebase-studio-export/workflows/startup_workflow.md +16 -0
- package/templates/init/functions/javascript/package-ongraphrequest.lint.json +1 -1
- package/templates/init/functions/javascript/package-ongraphrequest.nolint.json +1 -1
- package/templates/init/functions/typescript/package-ongraphrequest.lint.json +1 -1
- package/templates/init/functions/typescript/package-ongraphrequest.nolint.json +1 -1
package/lib/apiv2.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.GOOG_USER_PROJECT_HEADER = exports.
|
|
3
|
+
exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.GOOG_USER_PROJECT_HEADER = exports.standardHeaders = void 0;
|
|
4
4
|
exports.setRefreshToken = setRefreshToken;
|
|
5
5
|
exports.setAccessToken = setAccessToken;
|
|
6
6
|
exports.getAccessToken = getAccessToken;
|
|
@@ -19,15 +19,18 @@ const responseToError_1 = require("./responseToError");
|
|
|
19
19
|
const FormData = require("form-data");
|
|
20
20
|
const pkg = require("../package.json");
|
|
21
21
|
const CLI_VERSION = pkg.version;
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
const standardHeaders = () => {
|
|
23
|
+
const agent = (0, env_1.detectAIAgent)();
|
|
24
|
+
const agentStr = agent === "unknown" ? "" : ` agent-name/${agent}`;
|
|
25
|
+
const platform = (0, env_1.isFirebaseMcp)() ? "FirebaseMCP" : "FirebaseCLI";
|
|
26
|
+
const clientVersion = `${platform}/${CLI_VERSION}${agentStr}`;
|
|
27
|
+
return {
|
|
28
|
+
Connection: "keep-alive",
|
|
29
|
+
"User-Agent": clientVersion,
|
|
30
|
+
"X-Client-Version": clientVersion,
|
|
31
|
+
};
|
|
30
32
|
};
|
|
33
|
+
exports.standardHeaders = standardHeaders;
|
|
31
34
|
const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
|
|
32
35
|
exports.GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
|
|
33
36
|
const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
|
|
@@ -150,7 +153,7 @@ class Client {
|
|
|
150
153
|
if (!reqOptions.headers) {
|
|
151
154
|
reqOptions.headers = new node_fetch_1.Headers();
|
|
152
155
|
}
|
|
153
|
-
for (const [h, v] of Object.entries(exports.
|
|
156
|
+
for (const [h, v] of Object.entries((0, exports.standardHeaders)())) {
|
|
154
157
|
if (!reqOptions.headers.has(h)) {
|
|
155
158
|
reqOptions.headers.set(h, v);
|
|
156
159
|
}
|
|
@@ -225,7 +225,7 @@ class AppDistributionClient {
|
|
|
225
225
|
}
|
|
226
226
|
utils.logSuccess(`Testers removed from group successfully`);
|
|
227
227
|
}
|
|
228
|
-
async createReleaseTest(releaseName, devices,
|
|
228
|
+
async createReleaseTest(releaseName, devices, aiInstructions, loginCredential, testCaseName, displayName) {
|
|
229
229
|
try {
|
|
230
230
|
const response = await this.appDistroV1AlphaClient.request({
|
|
231
231
|
method: "POST",
|
|
@@ -234,7 +234,7 @@ class AppDistributionClient {
|
|
|
234
234
|
deviceExecutions: devices.map((device) => ({ device })),
|
|
235
235
|
loginCredential,
|
|
236
236
|
testCase: testCaseName,
|
|
237
|
-
aiInstructions:
|
|
237
|
+
aiInstructions: aiInstructions,
|
|
238
238
|
displayName: displayName,
|
|
239
239
|
},
|
|
240
240
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.toYaml = toYaml;
|
|
4
|
+
exports.fromYamlStep = fromYamlStep;
|
|
4
5
|
exports.fromYaml = fromYaml;
|
|
5
6
|
const jsYaml = require("js-yaml");
|
|
6
7
|
const error_1 = require("../error");
|
|
7
|
-
const ALLOWED_YAML_STEP_KEYS = new Set(["goal", "hint", "
|
|
8
|
+
const ALLOWED_YAML_STEP_KEYS = new Set(["goal", "hint", "finalScreenAssertion"]);
|
|
8
9
|
const ALLOWED_YAML_TEST_CASE_KEYS = new Set([
|
|
9
10
|
"displayName",
|
|
10
11
|
"id",
|
|
@@ -24,12 +25,14 @@ function toYamlTestCases(testCases) {
|
|
|
24
25
|
steps: testCase.aiInstructions.steps.map((step) => ({
|
|
25
26
|
goal: step.goal,
|
|
26
27
|
...(step.hint && { hint: step.hint }),
|
|
27
|
-
...(step.successCriteria && {
|
|
28
|
+
...(step.successCriteria && {
|
|
29
|
+
finalScreenAssertion: step.successCriteria,
|
|
30
|
+
}),
|
|
28
31
|
})),
|
|
29
32
|
}));
|
|
30
33
|
}
|
|
31
34
|
function toYaml(testCases) {
|
|
32
|
-
return jsYaml.safeDump(toYamlTestCases(testCases));
|
|
35
|
+
return jsYaml.safeDump({ tests: toYamlTestCases(testCases) });
|
|
33
36
|
}
|
|
34
37
|
function castExists(it, thing) {
|
|
35
38
|
if (it == null) {
|
|
@@ -44,22 +47,23 @@ function checkAllowedKeys(allowedKeys, o) {
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
}
|
|
50
|
+
function fromYamlStep(yamlStep) {
|
|
51
|
+
checkAllowedKeys(ALLOWED_YAML_STEP_KEYS, yamlStep);
|
|
52
|
+
return {
|
|
53
|
+
goal: castExists(yamlStep.goal, "goal"),
|
|
54
|
+
...(yamlStep.hint && { hint: yamlStep.hint }),
|
|
55
|
+
...(yamlStep.finalScreenAssertion && {
|
|
56
|
+
successCriteria: yamlStep.finalScreenAssertion,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
47
60
|
function fromYamlTestCases(appName, yamlTestCases) {
|
|
48
61
|
return yamlTestCases.map((yamlTestCase) => {
|
|
49
62
|
checkAllowedKeys(ALLOWED_YAML_TEST_CASE_KEYS, yamlTestCase);
|
|
50
63
|
return {
|
|
51
64
|
displayName: castExists(yamlTestCase.displayName, "displayName"),
|
|
52
65
|
aiInstructions: {
|
|
53
|
-
steps: castExists(yamlTestCase.steps, "steps").map((yamlStep) =>
|
|
54
|
-
checkAllowedKeys(ALLOWED_YAML_STEP_KEYS, yamlStep);
|
|
55
|
-
return {
|
|
56
|
-
goal: castExists(yamlStep.goal, "goal"),
|
|
57
|
-
...(yamlStep.hint && { hint: yamlStep.hint }),
|
|
58
|
-
...(yamlStep.successCriteria && {
|
|
59
|
-
successCriteria: yamlStep.successCriteria,
|
|
60
|
-
}),
|
|
61
|
-
};
|
|
62
|
-
}),
|
|
66
|
+
steps: castExists(yamlTestCase.steps, "steps").map((yamlStep) => fromYamlStep(yamlStep)),
|
|
63
67
|
},
|
|
64
68
|
...(yamlTestCase.id && {
|
|
65
69
|
name: `${appName}/testCases/${yamlTestCase.id}`,
|
|
@@ -78,8 +82,12 @@ function fromYaml(appName, yaml) {
|
|
|
78
82
|
catch (err) {
|
|
79
83
|
throw new error_1.FirebaseError(`Failed to parse YAML: ${(0, error_1.getErrMsg)(err)}`);
|
|
80
84
|
}
|
|
81
|
-
if (!
|
|
82
|
-
throw new error_1.FirebaseError("YAML file must contain a list of test cases.");
|
|
85
|
+
if (!parsedYaml || typeof parsedYaml !== "object" || !("tests" in parsedYaml)) {
|
|
86
|
+
throw new error_1.FirebaseError("YAML file must contain a top-level 'tests' field with a list of test cases.");
|
|
87
|
+
}
|
|
88
|
+
const yamlTestCases = parsedYaml.tests;
|
|
89
|
+
if (!Array.isArray(yamlTestCases)) {
|
|
90
|
+
throw new error_1.FirebaseError("The 'tests' field in the YAML file must contain a list of test cases.");
|
|
83
91
|
}
|
|
84
|
-
return fromYamlTestCases(appName,
|
|
92
|
+
return fromYamlTestCases(appName, yamlTestCases);
|
|
85
93
|
}
|
|
@@ -71,7 +71,7 @@ exports.WARN_NO_BACKENDS = "To use this secret, your backend's service account m
|
|
|
71
71
|
"It does not look like you have a backend yet. After creating a backend, grant access with " +
|
|
72
72
|
clc.bold("firebase apphosting:secrets:grantaccess");
|
|
73
73
|
exports.GRANT_ACCESS_IN_FUTURE = `To grant access in the future, run ${clc.bold("firebase apphosting:secrets:grantaccess")}`;
|
|
74
|
-
async function selectBackendServiceAccounts(projectNumber, projectId,
|
|
74
|
+
async function selectBackendServiceAccounts(projectNumber, projectId, nonInteractive) {
|
|
75
75
|
const listBackends = await apphosting.listBackends(projectId, "-");
|
|
76
76
|
if (listBackends.unreachable.length) {
|
|
77
77
|
utils.logWarning(`Could not reach location(s) ${listBackends.unreachable.join(", ")}. You may need to run ` +
|
|
@@ -83,7 +83,7 @@ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
|
|
|
83
83
|
}
|
|
84
84
|
if (listBackends.backends.length === 1) {
|
|
85
85
|
const grant = await prompt.confirm({
|
|
86
|
-
nonInteractive
|
|
86
|
+
nonInteractive,
|
|
87
87
|
default: true,
|
|
88
88
|
message: "To use this secret, your backend's service account must be granted access. Would you like to grant access now?",
|
|
89
89
|
});
|
|
@@ -101,7 +101,7 @@ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
|
|
|
101
101
|
serviceAccountDisplay(metadata[0]) +
|
|
102
102
|
".\nGranting access to one backend will grant access to all backends.");
|
|
103
103
|
const grant = await prompt.confirm({
|
|
104
|
-
nonInteractive
|
|
104
|
+
nonInteractive,
|
|
105
105
|
default: true,
|
|
106
106
|
message: "Would you like to grant access to all backends now?",
|
|
107
107
|
});
|
|
@@ -7,6 +7,8 @@ exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
|
|
|
7
7
|
exports.upsertSecret = upsertSecret;
|
|
8
8
|
exports.fetchSecrets = fetchSecrets;
|
|
9
9
|
exports.getSecretNameParts = getSecretNameParts;
|
|
10
|
+
exports.apphostingSecretsSetAction = apphostingSecretsSetAction;
|
|
11
|
+
const clc = require("colorette");
|
|
10
12
|
const error_1 = require("../../error");
|
|
11
13
|
const gcsm = require("../../gcp/secretManager");
|
|
12
14
|
const gcb = require("../../gcp/cloudbuild");
|
|
@@ -16,6 +18,9 @@ const secretManager_1 = require("../../gcp/secretManager");
|
|
|
16
18
|
const secretManager_2 = require("../../gcp/secretManager");
|
|
17
19
|
const utils = require("../../utils");
|
|
18
20
|
const prompt = require("../../prompt");
|
|
21
|
+
const dialogs = require("../../apphosting/secrets/dialogs");
|
|
22
|
+
const config = require("../../apphosting/config");
|
|
23
|
+
const projects_1 = require("../../management/projects");
|
|
19
24
|
function toMulti(accounts) {
|
|
20
25
|
const m = {
|
|
21
26
|
buildServiceAccounts: [accounts.buildServiceAccount],
|
|
@@ -169,3 +174,57 @@ function getSecretNameParts(secret) {
|
|
|
169
174
|
}
|
|
170
175
|
return [name, version];
|
|
171
176
|
}
|
|
177
|
+
async function apphostingSecretsSetAction(secretName, projectId, projectNumber, location, dataFile, nonInteractive) {
|
|
178
|
+
if (!projectNumber) {
|
|
179
|
+
projectNumber = (await (0, projects_1.getProject)(projectId)).projectNumber;
|
|
180
|
+
}
|
|
181
|
+
const created = await upsertSecret(projectId, secretName, location);
|
|
182
|
+
if (created === null) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
else if (created) {
|
|
186
|
+
utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`);
|
|
187
|
+
}
|
|
188
|
+
const secretValue = await utils.readSecretValue(`Enter a value for ${secretName}`, dataFile);
|
|
189
|
+
const version = await gcsm.addVersion(projectId, secretName, secretValue);
|
|
190
|
+
utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
|
|
191
|
+
utils.logBullet(`You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`);
|
|
192
|
+
if (!created) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const type = await prompt.select({
|
|
196
|
+
message: "Is this secret for production or only local testing?",
|
|
197
|
+
choices: [
|
|
198
|
+
{ name: "Production", value: "production" },
|
|
199
|
+
{ name: "Local testing only", value: "local" },
|
|
200
|
+
],
|
|
201
|
+
nonInteractive: !!nonInteractive,
|
|
202
|
+
default: "production",
|
|
203
|
+
});
|
|
204
|
+
if (type === "local") {
|
|
205
|
+
const emailList = await prompt.input({
|
|
206
|
+
message: "Please enter a comma separated list of user or groups who should have access to this secret:",
|
|
207
|
+
});
|
|
208
|
+
if (emailList.length) {
|
|
209
|
+
await grantEmailsSecretAccess(projectId, [secretName], emailList.split(","));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
utils.logBullet("To grant access in the future run " +
|
|
213
|
+
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --emails [email list]`));
|
|
214
|
+
}
|
|
215
|
+
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_EMULATORS_YAML_FILE);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, !!nonInteractive);
|
|
219
|
+
if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) {
|
|
220
|
+
utils.logWarning(`To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await grantSecretAccess(projectId, projectNumber, secretName, accounts);
|
|
224
|
+
}
|
|
225
|
+
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_BASE_YAML_FILE);
|
|
226
|
+
utils.logBullet("To grant additional users access to this secret run " +
|
|
227
|
+
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --email [email list]`) +
|
|
228
|
+
".\nTo grant additional backends access to this secret run " +
|
|
229
|
+
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --backend [backend ID]`));
|
|
230
|
+
}
|
|
@@ -7,6 +7,7 @@ const logger_1 = require("../logger");
|
|
|
7
7
|
const types_1 = require("./types");
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
9
|
const error_1 = require("../error");
|
|
10
|
+
const yaml_helper_1 = require("../appdistribution/yaml_helper");
|
|
10
11
|
async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
|
|
11
12
|
if (targetUri) {
|
|
12
13
|
try {
|
|
@@ -115,7 +116,7 @@ function toTestCaseInvocation(testDef, targetUri, defaultConfig) {
|
|
|
115
116
|
prerequisiteTestCaseId: testDef.prerequisiteTestCaseId,
|
|
116
117
|
startUri: targetUri + route,
|
|
117
118
|
displayName: testDef.displayName,
|
|
118
|
-
steps: steps,
|
|
119
|
+
steps: steps.map((step) => (0, yaml_helper_1.fromYamlStep)(step)),
|
|
119
120
|
},
|
|
120
121
|
testExecution: browsers.map((browser) => ({ config: { browser } })),
|
|
121
122
|
};
|
package/lib/bin/mcp.js
CHANGED
|
@@ -6,6 +6,7 @@ const path_1 = require("path");
|
|
|
6
6
|
const util_1 = require("util");
|
|
7
7
|
const logger_1 = require("../logger");
|
|
8
8
|
const index_1 = require("../mcp/index");
|
|
9
|
+
const env_1 = require("../env");
|
|
9
10
|
const index_js_1 = require("../mcp/prompts/index.js");
|
|
10
11
|
const index_js_2 = require("../mcp/resources/index.js");
|
|
11
12
|
const index_js_3 = require("../mcp/tools/index.js");
|
|
@@ -80,7 +81,7 @@ async function mcp() {
|
|
|
80
81
|
}
|
|
81
82
|
if (earlyExit)
|
|
82
83
|
return;
|
|
83
|
-
|
|
84
|
+
(0, env_1.setFirebaseMcp)(true);
|
|
84
85
|
(0, logger_1.useFileLogger)();
|
|
85
86
|
const activeFeatures = (values.only || "")
|
|
86
87
|
.split(",")
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.command = void 0;
|
|
4
|
-
const clc = require("colorette");
|
|
5
4
|
const command_1 = require("../command");
|
|
6
5
|
const projectUtils_1 = require("../projectUtils");
|
|
7
6
|
const requireAuth_1 = require("../requireAuth");
|
|
8
7
|
const gcsm = require("../gcp/secretManager");
|
|
9
8
|
const apphosting = require("../gcp/apphosting");
|
|
10
9
|
const requirePermissions_1 = require("../requirePermissions");
|
|
11
|
-
const
|
|
12
|
-
const dialogs = require("../apphosting/secrets/dialogs");
|
|
13
|
-
const config = require("../apphosting/config");
|
|
14
|
-
const utils = require("../utils");
|
|
15
|
-
const prompt = require("../prompt");
|
|
10
|
+
const secrets_1 = require("../apphosting/secrets");
|
|
16
11
|
exports.command = new command_1.Command("apphosting:secrets:set <secretName>")
|
|
17
12
|
.description("create or update a secret for use in Firebase App Hosting")
|
|
18
13
|
.option("-l, --location <location>", "optional location to retrict secret replication")
|
|
@@ -32,51 +27,5 @@ exports.command = new command_1.Command("apphosting:secrets:set <secretName>")
|
|
|
32
27
|
.action(async (secretName, options) => {
|
|
33
28
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
34
29
|
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
35
|
-
|
|
36
|
-
if (created === null) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
else if (created) {
|
|
40
|
-
utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`);
|
|
41
|
-
}
|
|
42
|
-
const secretValue = await utils.readSecretValue(`Enter a value for ${secretName}`, options.dataFile);
|
|
43
|
-
const version = await gcsm.addVersion(projectId, secretName, secretValue);
|
|
44
|
-
utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
|
|
45
|
-
utils.logBullet(`You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`);
|
|
46
|
-
if (!created) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const type = await prompt.select({
|
|
50
|
-
message: "Is this secret for production or only local testing?",
|
|
51
|
-
choices: [
|
|
52
|
-
{ name: "Production", value: "production" },
|
|
53
|
-
{ name: "Local testing only", value: "local" },
|
|
54
|
-
],
|
|
55
|
-
});
|
|
56
|
-
if (type === "local") {
|
|
57
|
-
const emailList = await prompt.input({
|
|
58
|
-
message: "Please enter a comma separated list of user or groups who should have access to this secret:",
|
|
59
|
-
});
|
|
60
|
-
if (emailList.length) {
|
|
61
|
-
await secrets.grantEmailsSecretAccess(projectId, [secretName], emailList.split(","));
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
utils.logBullet("To grant access in the future run " +
|
|
65
|
-
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --emails [email list]`));
|
|
66
|
-
}
|
|
67
|
-
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_EMULATORS_YAML_FILE);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, options);
|
|
71
|
-
if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) {
|
|
72
|
-
utils.logWarning(`To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
await secrets.grantSecretAccess(projectId, projectNumber, secretName, accounts);
|
|
76
|
-
}
|
|
77
|
-
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_BASE_YAML_FILE);
|
|
78
|
-
utils.logBullet("To grant additional users access to this secret run " +
|
|
79
|
-
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --email [email list]`) +
|
|
80
|
-
".\nTo grant additional backends access to this secret run " +
|
|
81
|
-
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --backend [backend ID]`));
|
|
30
|
+
return (0, secrets_1.apphostingSecretsSetAction)(secretName, projectId, projectNumber, options.location, options.dataFile, options.nonInteractive);
|
|
82
31
|
});
|
|
@@ -62,10 +62,10 @@ async function invokeTests(client, releaseName, testDefs, devices) {
|
|
|
62
62
|
try {
|
|
63
63
|
const testInvocations = [];
|
|
64
64
|
for (const testDef of testDefs) {
|
|
65
|
-
const
|
|
65
|
+
const aiInstructions = {
|
|
66
66
|
steps: testDef.testCase.steps,
|
|
67
67
|
};
|
|
68
|
-
testInvocations.push(await client.createReleaseTest(releaseName, devices,
|
|
68
|
+
testInvocations.push(await client.createReleaseTest(releaseName, devices, aiInstructions, undefined, undefined, testDef.testCase.displayName));
|
|
69
69
|
}
|
|
70
70
|
return testInvocations;
|
|
71
71
|
}
|
|
@@ -3,10 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.command = void 0;
|
|
4
4
|
const command_1 = require("../command");
|
|
5
5
|
const logger_1 = require("../logger");
|
|
6
|
+
const migrate_1 = require("../firebase_studio/migrate");
|
|
7
|
+
const path = require("path");
|
|
6
8
|
const experiments = require("../experiments");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
.
|
|
9
|
+
const error_1 = require("../error");
|
|
10
|
+
exports.command = new command_1.Command("studio:export <path>")
|
|
11
|
+
.description("Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download.")
|
|
12
|
+
.option("--no-start-agy", "skip starting the Antigravity IDE after migration")
|
|
13
|
+
.action(async (exportPath, options) => {
|
|
10
14
|
experiments.assertEnabled("studioexport", "export Studio apps");
|
|
11
|
-
|
|
15
|
+
if (!exportPath) {
|
|
16
|
+
throw new error_1.FirebaseError("Must specify a path for migration.", { exit: 1 });
|
|
17
|
+
}
|
|
18
|
+
const rootPath = path.resolve(exportPath);
|
|
19
|
+
logger_1.logger.info(`Exporting Studio apps from ${rootPath} to Antigravity...`);
|
|
20
|
+
await (0, migrate_1.migrate)(rootPath, options);
|
|
12
21
|
});
|
|
@@ -24,6 +24,7 @@ const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
|
|
|
24
24
|
const errors = require("./errors");
|
|
25
25
|
const provisionCloudSql_1 = require("./provisionCloudSql");
|
|
26
26
|
const requireAuth_1 = require("../requireAuth");
|
|
27
|
+
const cloudbilling_1 = require("../gcp/cloudbilling");
|
|
27
28
|
async function setupSchemaIfNecessary(instanceId, databaseId, options) {
|
|
28
29
|
try {
|
|
29
30
|
await (0, connect_1.setupIAMUsers)(instanceId, options);
|
|
@@ -429,7 +430,17 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, dat
|
|
|
429
430
|
postgresql?.schemaValidation === "NONE") {
|
|
430
431
|
const [, , , , , serviceId] = serviceName.split("/");
|
|
431
432
|
const [, projectId, , , , instanceId] = postgresql.cloudSql.instance.split("/");
|
|
432
|
-
|
|
433
|
+
let isFreeTrial = false;
|
|
434
|
+
let billingEnabled = false;
|
|
435
|
+
try {
|
|
436
|
+
const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
|
|
437
|
+
isFreeTrial = instance.settings?.userLabels?.["firebase-data-connect"] === "ft";
|
|
438
|
+
billingEnabled = await (0, cloudbilling_1.checkBillingEnabled)(projectId);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
}
|
|
442
|
+
throw new error_1.FirebaseError(`While checking the service ${serviceId}, ` +
|
|
443
|
+
(0, provisionCloudSql_1.cloudSQLBeingCreated)(projectId, instanceId, isFreeTrial, billingEnabled));
|
|
433
444
|
}
|
|
434
445
|
if (!currentSchema || !postgresql) {
|
|
435
446
|
if (!linkIfNotConnected) {
|
|
@@ -7,6 +7,7 @@ const backend_1 = require("../../apphosting/backend");
|
|
|
7
7
|
const rollout_1 = require("../../apphosting/rollout");
|
|
8
8
|
const projectUtils_1 = require("../../projectUtils");
|
|
9
9
|
const utils_1 = require("../../utils");
|
|
10
|
+
const error_1 = require("../../error");
|
|
10
11
|
async function default_1(context, options) {
|
|
11
12
|
let backendIds = Object.keys(context.backendConfigs);
|
|
12
13
|
const missingBackends = backendIds.filter((id) => !context.backendLocations[id] || !context.backendStorageUris[id]);
|
|
@@ -39,6 +40,7 @@ async function default_1(context, options) {
|
|
|
39
40
|
(0, utils_1.logLabeledBullet)("apphosting", `You may also track the rollout(s) at:\n\t${(0, api_1.consoleOrigin)()}/project/${projectId}/apphosting`);
|
|
40
41
|
const rolloutsSpinner = ora(`Starting rollout(s) for backend(s) ${backendIds.join(", ")}; this may take a few minutes. It's safe to exit now.\n`).start();
|
|
41
42
|
const results = await Promise.allSettled(rollouts);
|
|
43
|
+
let failed = false;
|
|
42
44
|
for (let i = 0; i < results.length; i++) {
|
|
43
45
|
const res = results[i];
|
|
44
46
|
if (res.status === "fulfilled") {
|
|
@@ -47,9 +49,13 @@ async function default_1(context, options) {
|
|
|
47
49
|
(0, utils_1.logLabeledSuccess)("apphosting", `Your backend is now deployed at:\n\thttps://${backend.uri}`);
|
|
48
50
|
}
|
|
49
51
|
else {
|
|
52
|
+
failed = true;
|
|
50
53
|
(0, utils_1.logLabeledWarning)("apphosting", `Rollout for backend ${backendIds[i]} failed.`);
|
|
51
|
-
(0, utils_1.logLabeledError)("apphosting", res.reason);
|
|
54
|
+
(0, utils_1.logLabeledError)("apphosting", `${res.reason}`);
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
rolloutsSpinner.stop();
|
|
58
|
+
if (failed) {
|
|
59
|
+
throw new error_1.FirebaseError("One or more rollouts failed. Please review the errors above and try again.");
|
|
60
|
+
}
|
|
55
61
|
}
|
|
@@ -20,6 +20,7 @@ const tos_1 = require("../../extensions/tos");
|
|
|
20
20
|
const common_1 = require("../../extensions/runtimes/common");
|
|
21
21
|
const functionsDeployHelper_1 = require("../functions/functionsDeployHelper");
|
|
22
22
|
const projectConfig_1 = require("../../functions/projectConfig");
|
|
23
|
+
const utils_1 = require("../../utils");
|
|
23
24
|
const matchesInstanceId = (dep) => (test) => {
|
|
24
25
|
return dep.instanceId === test.instanceId;
|
|
25
26
|
};
|
|
@@ -123,10 +124,18 @@ async function prepareDynamicExtensions(context, options, payload, builds) {
|
|
|
123
124
|
const extensions = (0, common_1.extractExtensionsFromBuilds)(builds, filters);
|
|
124
125
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
125
126
|
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
let haveExtensions = [];
|
|
128
|
+
try {
|
|
129
|
+
await (0, extensionsHelper_1.ensureExtensionsApiEnabled)(options);
|
|
130
|
+
await (0, requirePermissions_1.requirePermissions)(options, ["firebaseextensions.instances.list"]);
|
|
131
|
+
haveExtensions = await planner.haveDynamic(projectId);
|
|
132
|
+
haveExtensions = haveExtensions.filter((e) => (0, common_1.extensionMatchesAnyFilter)(e.labels?.codebase, e.instanceId, filters));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
(0, utils_1.logLabeledError)("extensions", "Failed to fetch the list of extensions. Assuming for now that there are no existing extensions. " +
|
|
136
|
+
"If you are trying to install an extension through Firebase Functions this may fail later.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
130
139
|
if (Object.keys(extensions).length === 0 && haveExtensions.length === 0) {
|
|
131
140
|
return;
|
|
132
141
|
}
|
package/lib/downloadUtils.js
CHANGED
|
@@ -21,7 +21,9 @@ async function downloadToTmp(remoteUrl, auth = false) {
|
|
|
21
21
|
resolveOnHTTPError: true,
|
|
22
22
|
});
|
|
23
23
|
if (res.status !== 200) {
|
|
24
|
-
throw new error_1.FirebaseError(`download failed, status ${res.status}: ${await res.response.text()}
|
|
24
|
+
throw new error_1.FirebaseError(`download failed, status ${res.status}: ${await res.response.text()}`, {
|
|
25
|
+
status: res.status,
|
|
26
|
+
});
|
|
25
27
|
}
|
|
26
28
|
const total = parseInt(res.response.headers.get("content-length") || "0", 10);
|
|
27
29
|
const totalMb = Math.ceil(total / 1000000);
|
package/lib/emulator/download.js
CHANGED
|
@@ -18,9 +18,23 @@ async function downloadEmulator(name) {
|
|
|
18
18
|
emulatorLogger_1.EmulatorLogger.forEmulator(name).logLabeled("WARN", name, `Env variable override detected, skipping download. Using ${emulator} emulator at ${emulator.binaryPath}`);
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
+
const overrideVersion = downloadableEmulators.emulatorVersionOverride(name);
|
|
22
|
+
if (overrideVersion) {
|
|
23
|
+
emulatorLogger_1.EmulatorLogger.forEmulator(name).logLabeled("WARN", name, `Env variable override detected. Using custom ${name} emulator version ${overrideVersion}.`);
|
|
24
|
+
}
|
|
21
25
|
emulatorLogger_1.EmulatorLogger.forEmulator(name).logLabeled("BULLET", name, `downloading ${path.basename(emulator.downloadPath)}...`);
|
|
22
26
|
fs.ensureDirSync(emulator.opts.cacheDir);
|
|
23
|
-
|
|
27
|
+
let tmpfile;
|
|
28
|
+
try {
|
|
29
|
+
tmpfile = await downloadUtils.downloadToTmp(emulator.opts.remoteUrl, !!emulator.opts.auth);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (overrideVersion && err instanceof error_1.FirebaseError && err.status === 404) {
|
|
33
|
+
throw new error_1.FirebaseError(`env variable ${name.toUpperCase()}_EMULATOR_VERSION set to ${overrideVersion},
|
|
34
|
+
but no such version of ${name} was found. Please double check the version number, or unset this environment variable to use the latest default.`);
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
24
38
|
if (!emulator.opts.skipChecksumAndSize) {
|
|
25
39
|
await validateSize(tmpfile, emulator.opts.expectedSize);
|
|
26
40
|
await validateChecksum(tmpfile, emulator.opts.expectedChecksum);
|
|
@@ -54,36 +54,36 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dataconnect": {
|
|
56
56
|
"darwin": {
|
|
57
|
-
"version": "3.2.
|
|
58
|
-
"expectedSize":
|
|
59
|
-
"expectedChecksum": "
|
|
60
|
-
"expectedChecksumSHA256": "
|
|
61
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.2.
|
|
62
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
57
|
+
"version": "3.2.1",
|
|
58
|
+
"expectedSize": 30880608,
|
|
59
|
+
"expectedChecksum": "ef63443e5132e4ade7b89506fea60e29",
|
|
60
|
+
"expectedChecksumSHA256": "0a9c05413cb3048a4bef1968e55109ca67eb607e6d77355741cb9a7dcae51607",
|
|
61
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.2.1",
|
|
62
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1"
|
|
63
63
|
},
|
|
64
64
|
"darwin_arm64": {
|
|
65
|
-
"version": "3.2.
|
|
66
|
-
"expectedSize":
|
|
67
|
-
"expectedChecksum": "
|
|
68
|
-
"expectedChecksumSHA256": "
|
|
69
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.2.
|
|
70
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
65
|
+
"version": "3.2.1",
|
|
66
|
+
"expectedSize": 30339010,
|
|
67
|
+
"expectedChecksum": "866989b28ae5cad1084403bd4404dcee",
|
|
68
|
+
"expectedChecksumSHA256": "558e334c456c646b1ddf4b32ab43d976c5b03c191a4c92855e8c93cd32e1215a",
|
|
69
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.2.1",
|
|
70
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1"
|
|
71
71
|
},
|
|
72
72
|
"win32": {
|
|
73
|
-
"version": "3.2.
|
|
74
|
-
"expectedSize":
|
|
75
|
-
"expectedChecksum": "
|
|
76
|
-
"expectedChecksumSHA256": "
|
|
77
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.2.
|
|
78
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
73
|
+
"version": "3.2.1",
|
|
74
|
+
"expectedSize": 31385088,
|
|
75
|
+
"expectedChecksum": "cb46949156c889d5724f7fa80e8a613b",
|
|
76
|
+
"expectedChecksumSHA256": "1dd1f9ef7910d8295baac61741fde29fac536d82ff841a2d5cefe25e98f7cd98",
|
|
77
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.2.1",
|
|
78
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1.exe"
|
|
79
79
|
},
|
|
80
80
|
"linux": {
|
|
81
|
-
"version": "3.2.
|
|
82
|
-
"expectedSize":
|
|
83
|
-
"expectedChecksum": "
|
|
84
|
-
"expectedChecksumSHA256": "
|
|
85
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.2.
|
|
86
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
81
|
+
"version": "3.2.1",
|
|
82
|
+
"expectedSize": 30798008,
|
|
83
|
+
"expectedChecksum": "8c29b6b526efcbf13de278bd3b5b598a",
|
|
84
|
+
"expectedChecksumSHA256": "d7eb7f7afd98dd9018039c83782312f975b160c3708eb49f8fecdf7d27508d82",
|
|
85
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.2.1",
|
|
86
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1"
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}
|