firebase-tools 15.19.0 → 15.20.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/agentSkills.js +2 -1
- package/lib/api.js +1 -3
- package/lib/appdistribution/client.js +6 -5
- package/lib/appdistribution/options-parser-util.js +21 -0
- package/lib/archiveFile.js +30 -0
- package/lib/command.js +7 -0
- package/lib/commands/appdistribution-distribute.js +13 -4
- package/lib/commands/apptesting.js +9 -3
- package/lib/commands/crashlytics-sourcemap-upload.js +61 -0
- package/lib/commands/index.js +4 -0
- package/lib/crashlytics/sourcemap.js +270 -0
- package/lib/dataconnect/ensureApis.js +0 -13
- package/lib/deploy/apphosting/prepare.js +6 -6
- package/lib/deploy/dataconnect/context.js +0 -2
- package/lib/deploy/dataconnect/deploy.js +0 -19
- package/lib/deploy/functions/prepare.js +8 -104
- package/lib/deploy/functions/services/ailogic.js +17 -10
- package/lib/deploy/functions/services/auth.js +3 -0
- package/lib/deploy/functions/services/database.js +18 -0
- package/lib/deploy/functions/services/dataconnect.js +20 -0
- package/lib/deploy/functions/services/firestore.js +12 -0
- package/lib/deploy/functions/services/index.js +18 -7
- package/lib/deploy/functions/services/storage.js +14 -0
- package/lib/deploy/functions/triggerRegionHelper.js +2 -4
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/experiments.js +8 -3
- package/lib/firebase_studio/migrate.js +8 -0
- package/lib/gcp/location.js +16 -1
- package/lib/gemini/fdcExperience.js +171 -26
- package/lib/init/features/dataconnect/create_app.js +3 -4
- package/lib/init/features/dataconnect/index.js +49 -15
- package/lib/init/features/dataconnect/sdk.js +2 -2
- package/lib/mcp/tools/apptesting/tests.js +3 -1
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/lib/utils.js +48 -0
- package/package.json +1 -1
- package/lib/dataconnect/cloudAICompanionTypes.js +0 -2
package/lib/agentSkills.js
CHANGED
|
@@ -17,11 +17,12 @@ async function installAgentSkills(options) {
|
|
|
17
17
|
if (!utils.commandExistsSync("npx")) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
const skillPackage = options.skillPackage || "firebase/agent-skills";
|
|
20
21
|
const args = [
|
|
21
22
|
"-y",
|
|
22
23
|
"skills",
|
|
23
24
|
"add",
|
|
24
|
-
|
|
25
|
+
skillPackage,
|
|
25
26
|
"--skill",
|
|
26
27
|
"*",
|
|
27
28
|
"-y",
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.hostingApiOrigin = exports.firebaseStorageOrigin = exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.crashlyticsApiOrigin = exports.messagingApiOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.consoleOrigin = exports.authManagementOrigin = exports.authOrigin = exports.apphostingGitHubAppInstallationURL = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
|
|
4
|
-
exports.developerKnowledgeOrigin = exports.cloudTestingOrigin = exports.appTestingOrigin = exports.
|
|
4
|
+
exports.developerKnowledgeOrigin = exports.cloudTestingOrigin = exports.appTestingOrigin = exports.aiLogicProxyOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.studioApiOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = void 0;
|
|
5
5
|
exports.getScopes = getScopes;
|
|
6
6
|
exports.setScopes = setScopes;
|
|
7
7
|
const constants_1 = require("./emulator/constants");
|
|
@@ -144,8 +144,6 @@ const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatf
|
|
|
144
144
|
exports.vertexAIOrigin = vertexAIOrigin;
|
|
145
145
|
const aiLogicProxyOrigin = () => utils.envOverride("AI_LOGIC_PROXY_URL", "https://firebasevertexai.googleapis.com");
|
|
146
146
|
exports.aiLogicProxyOrigin = aiLogicProxyOrigin;
|
|
147
|
-
const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
|
|
148
|
-
exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
|
|
149
147
|
const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
|
|
150
148
|
exports.appTestingOrigin = appTestingOrigin;
|
|
151
149
|
const cloudTestingOrigin = () => utils.envOverride("CLOUD_TESTING_URL", "https://testing.googleapis.com");
|
|
@@ -225,17 +225,18 @@ 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, options = {}) {
|
|
229
229
|
try {
|
|
230
230
|
const response = await this.appDistroV1AlphaClient.request({
|
|
231
231
|
method: "POST",
|
|
232
232
|
path: `${releaseName}/tests`,
|
|
233
233
|
body: {
|
|
234
234
|
deviceExecutions: devices.map((device) => ({ device })),
|
|
235
|
-
loginCredential,
|
|
236
|
-
testCase: testCaseName,
|
|
237
|
-
aiInstructions: aiInstructions,
|
|
238
|
-
displayName: displayName,
|
|
235
|
+
loginCredential: options.loginCredential,
|
|
236
|
+
testCase: options.testCaseName,
|
|
237
|
+
aiInstructions: options.aiInstructions,
|
|
238
|
+
displayName: options.displayName,
|
|
239
|
+
resultsBucket: options.resultsBucket,
|
|
239
240
|
},
|
|
240
241
|
});
|
|
241
242
|
return response.body;
|
|
@@ -8,6 +8,7 @@ exports.getAppName = getAppName;
|
|
|
8
8
|
exports.toAppName = toAppName;
|
|
9
9
|
exports.parseTestDevices = parseTestDevices;
|
|
10
10
|
exports.getLoginCredential = getLoginCredential;
|
|
11
|
+
exports.getResultsBucket = getResultsBucket;
|
|
11
12
|
const fs = require("fs-extra");
|
|
12
13
|
const error_1 = require("../error");
|
|
13
14
|
const projectUtils_1 = require("../projectUtils");
|
|
@@ -137,3 +138,23 @@ function getLoginCredential(args) {
|
|
|
137
138
|
function isPresenceMismatched(value1, value2) {
|
|
138
139
|
return (value1 && !value2) || (!value1 && value2);
|
|
139
140
|
}
|
|
141
|
+
const APP_NAME_REGEX = /^projects\/(?<projectNumber>[^\/]+)\/apps\/(?<appId>[^\/]+)$/;
|
|
142
|
+
const BUCKET_NAME_FORMAT_REGEX = /^[a-z0-9_.-]+$/;
|
|
143
|
+
function getResultsBucket(bucket, appName) {
|
|
144
|
+
if (!bucket) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
let bucketName = bucket;
|
|
148
|
+
if (bucketName.startsWith("gs://")) {
|
|
149
|
+
bucketName = bucketName.substring(5);
|
|
150
|
+
}
|
|
151
|
+
if (!BUCKET_NAME_FORMAT_REGEX.test(bucketName)) {
|
|
152
|
+
throw new error_1.FirebaseError(`Invalid results bucket format: ${bucket}`);
|
|
153
|
+
}
|
|
154
|
+
const match = APP_NAME_REGEX.exec(appName);
|
|
155
|
+
if (!match || typeof match.groups === "undefined") {
|
|
156
|
+
throw new error_1.FirebaseError(`Invalid app name: ${appName}`);
|
|
157
|
+
}
|
|
158
|
+
const { projectNumber } = match.groups;
|
|
159
|
+
return `projects/${projectNumber}/buckets/${bucketName}`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.archiveFile = archiveFile;
|
|
4
|
+
const archiver = require("archiver");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const tmp = require("tmp");
|
|
8
|
+
async function archiveFile(filePath, options) {
|
|
9
|
+
const tmpFileObj = tmp.fileSync({ postfix: ".zip" });
|
|
10
|
+
const tmpFile = tmpFileObj.name;
|
|
11
|
+
fs.closeSync(tmpFileObj.fd);
|
|
12
|
+
const fileStream = fs.createWriteStream(tmpFile, {
|
|
13
|
+
flags: "w",
|
|
14
|
+
encoding: "binary",
|
|
15
|
+
});
|
|
16
|
+
const archive = archiver("zip");
|
|
17
|
+
const name = options?.archivedFileName ?? path.basename(filePath);
|
|
18
|
+
archive.file(filePath, { name });
|
|
19
|
+
await pipeAsync(archive, fileStream);
|
|
20
|
+
return tmpFile;
|
|
21
|
+
}
|
|
22
|
+
async function pipeAsync(from, to) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
to.on("finish", resolve);
|
|
25
|
+
to.on("error", reject);
|
|
26
|
+
from.on("error", reject);
|
|
27
|
+
from.pipe(to);
|
|
28
|
+
from.finalize().catch(reject);
|
|
29
|
+
});
|
|
30
|
+
}
|
package/lib/command.js
CHANGED
|
@@ -185,6 +185,13 @@ class Command {
|
|
|
185
185
|
.filter(Boolean)
|
|
186
186
|
.join(",");
|
|
187
187
|
}
|
|
188
|
+
const exceptOption = (0, utils_1.getInheritedOption)(options, "except");
|
|
189
|
+
if (exceptOption) {
|
|
190
|
+
options.except = exceptOption
|
|
191
|
+
.split(/[\s,]+/)
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join(",");
|
|
194
|
+
}
|
|
188
195
|
try {
|
|
189
196
|
options.config = config_1.Config.load(options);
|
|
190
197
|
}
|
|
@@ -39,9 +39,11 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
|
|
|
39
39
|
.option("--test-non-blocking", "run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
|
|
40
40
|
.option("--test-case-ids <string>", "a comma-separated list of test case IDs.")
|
|
41
41
|
.option("--test-case-ids-file <file>", "path to file with a comma- or newline-separated list of test case IDs.")
|
|
42
|
+
.option("--results-bucket <bucket>", "The name of a Google Cloud Storage bucket where raw results of any automated tests will be stored. If this flag is not set, Firebase creates a bucket for you. Note that the bucket must be owned by a billing-enabled project, and that using a non-default bucket will result in billing charges for the storage used.")
|
|
42
43
|
.before(requireAuth_1.requireAuth)
|
|
43
44
|
.action(async (file, options) => {
|
|
44
45
|
const appName = (0, options_parser_util_1.getAppName)(options);
|
|
46
|
+
const resultsBucket = (0, options_parser_util_1.getResultsBucket)(options.resultsBucket, appName);
|
|
45
47
|
const distribution = new distribution_1.Distribution(file);
|
|
46
48
|
const releaseNotes = getReleaseNotes(options.releaseNotes, options.releaseNotesFile);
|
|
47
49
|
const testers = (0, options_parser_util_1.parseIntoStringArray)(options.testers, options.testersFile);
|
|
@@ -58,9 +60,9 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
|
|
|
58
60
|
usernameResourceName: options.testUsernameResource,
|
|
59
61
|
passwordResourceName: options.testPasswordResource,
|
|
60
62
|
});
|
|
61
|
-
await distribute(appName, distribution, testCases, testDevices, releaseNotes, testers, groups, options.testNonBlocking, loginCredential);
|
|
63
|
+
await distribute(appName, distribution, testCases, testDevices, releaseNotes, testers, groups, options.testNonBlocking, loginCredential, resultsBucket);
|
|
62
64
|
});
|
|
63
|
-
async function distribute(appName, distribution, testCases, testDevices, releaseNotes, testers, groups, testNonBlocking, loginCredential) {
|
|
65
|
+
async function distribute(appName, distribution, testCases, testDevices, releaseNotes, testers, groups, testNonBlocking, loginCredential, resultsBucket) {
|
|
64
66
|
const requests = new client_1.AppDistributionClient();
|
|
65
67
|
let aabInfo;
|
|
66
68
|
if (distribution.distributionFileType() === distribution_1.DistributionFileType.AAB) {
|
|
@@ -116,11 +118,18 @@ async function distribute(appName, distribution, testCases, testDevices, release
|
|
|
116
118
|
utils.logBullet("starting automated test (note: this feature is in beta)");
|
|
117
119
|
const releaseTestPromises = [];
|
|
118
120
|
if (!testCases.length) {
|
|
119
|
-
releaseTestPromises.push(requests.createReleaseTest(release.name, testDevices,
|
|
121
|
+
releaseTestPromises.push(requests.createReleaseTest(release.name, testDevices, {
|
|
122
|
+
loginCredential,
|
|
123
|
+
resultsBucket,
|
|
124
|
+
}));
|
|
120
125
|
}
|
|
121
126
|
else {
|
|
122
127
|
for (const testCaseId of testCases) {
|
|
123
|
-
releaseTestPromises.push(requests.createReleaseTest(release.name, testDevices,
|
|
128
|
+
releaseTestPromises.push(requests.createReleaseTest(release.name, testDevices, {
|
|
129
|
+
loginCredential,
|
|
130
|
+
testCaseName: `${appName}/testCases/${testCaseId}`,
|
|
131
|
+
resultsBucket,
|
|
132
|
+
}));
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
135
|
const releaseTests = await Promise.all(releaseTestPromises);
|
|
@@ -29,9 +29,11 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
|
|
|
29
29
|
.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.")
|
|
30
30
|
.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.")
|
|
31
31
|
.option("--test-non-blocking", "Run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
|
|
32
|
+
.option("--results-bucket <bucket>", "The name of a Google Cloud Storage bucket where raw test results will be stored. If this flag is not set, Firebase creates a default bucket for you. Note that the bucket must be owned by a billing-enabled project, and that using a non-default bucket will result in billing charges for the storage used.")
|
|
32
33
|
.before(requireAuth_1.requireAuth)
|
|
33
34
|
.action(async (target, options) => {
|
|
34
35
|
const appName = (0, options_parser_util_1.getAppName)(options);
|
|
36
|
+
const resultsBucket = (0, options_parser_util_1.getResultsBucket)(options.resultsBucket, appName);
|
|
35
37
|
const testDir = path.resolve(options.testDir || "tests");
|
|
36
38
|
if (!(0, fsutils_1.dirExistsSync)(testDir)) {
|
|
37
39
|
throw new error_1.FirebaseError(`Tests directory not found: ${testDir}. Use the --test-dir flag to choose a different directory.`);
|
|
@@ -60,7 +62,7 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
|
|
|
60
62
|
utils.logBullet(`Using release ${release.displayVersion} created at ${release.createTime}`);
|
|
61
63
|
}
|
|
62
64
|
invokeSpinner.start();
|
|
63
|
-
releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices);
|
|
65
|
+
releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices, resultsBucket);
|
|
64
66
|
invokeSpinner.text = `${(0, parseTestFiles_1.pluralizeTests)(releaseTests.length)} started successfully!`;
|
|
65
67
|
invokeSpinner.succeed();
|
|
66
68
|
}
|
|
@@ -76,14 +78,18 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
|
|
|
76
78
|
utils.logBullet(`View detailed results in the Firebase Console:\n${release.firebaseConsoleUri}`);
|
|
77
79
|
}
|
|
78
80
|
});
|
|
79
|
-
async function invokeTests(client, releaseName, testDefs, devices) {
|
|
81
|
+
async function invokeTests(client, releaseName, testDefs, devices, resultsBucket) {
|
|
80
82
|
try {
|
|
81
83
|
const releaseTests = [];
|
|
82
84
|
for (const testDef of testDefs) {
|
|
83
85
|
const aiInstructions = {
|
|
84
86
|
steps: testDef.testCase.steps,
|
|
85
87
|
};
|
|
86
|
-
releaseTests.push(await client.createReleaseTest(releaseName, devices,
|
|
88
|
+
releaseTests.push(await client.createReleaseTest(releaseName, devices, {
|
|
89
|
+
aiInstructions,
|
|
90
|
+
displayName: testDef.testCase.displayName,
|
|
91
|
+
resultsBucket,
|
|
92
|
+
}));
|
|
87
93
|
}
|
|
88
94
|
return releaseTests;
|
|
89
95
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs_extra_1 = require("fs-extra");
|
|
6
|
+
const fsAsync_1 = require("../fsAsync");
|
|
7
|
+
const command_1 = require("../command");
|
|
8
|
+
const error_1 = require("../error");
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const projectUtils_1 = require("../projectUtils");
|
|
11
|
+
const getProjectNumber_1 = require("../getProjectNumber");
|
|
12
|
+
const requireAuth_1 = require("../requireAuth");
|
|
13
|
+
const sourcemap_1 = require("../crashlytics/sourcemap");
|
|
14
|
+
exports.command = new command_1.Command("crashlytics:sourcemap:upload [mappingFiles]")
|
|
15
|
+
.description("upload javascript source maps to de-minify stack traces")
|
|
16
|
+
.option("--app <appID>", "the app id of your Firebase app")
|
|
17
|
+
.option("--bucket-location <bucketLocation>", 'the location of the Google Cloud Storage bucket (default: "US-CENTRAL1"')
|
|
18
|
+
.option("--app-version <appVersion>", "the version of your Firebase app (defaults to Git commit hash, if available)")
|
|
19
|
+
.before(requireAuth_1.requireAuth)
|
|
20
|
+
.action(async (mappingFiles, options) => {
|
|
21
|
+
(0, sourcemap_1.checkGoogleAppID)(options);
|
|
22
|
+
const appVersion = (0, sourcemap_1.getAppVersion)(options);
|
|
23
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
24
|
+
const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(options);
|
|
25
|
+
const bucketName = await (0, sourcemap_1.upsertBucket)(projectId, projectNumber, options);
|
|
26
|
+
const rootDir = path.resolve(options.projectRoot ?? process.cwd());
|
|
27
|
+
const filePath = mappingFiles ? path.resolve(mappingFiles) : rootDir;
|
|
28
|
+
let fstat;
|
|
29
|
+
try {
|
|
30
|
+
fstat = (0, fs_extra_1.statSync)(filePath);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new error_1.FirebaseError("provide a valid directory to mapping file(s), e.g. app/build/outputs");
|
|
34
|
+
}
|
|
35
|
+
let successCount = 0;
|
|
36
|
+
const failedFiles = [];
|
|
37
|
+
if (fstat.isDirectory()) {
|
|
38
|
+
(0, utils_1.logLabeledBullet)("crashlytics", "Looking for mapping files in your directory...");
|
|
39
|
+
const files = await (0, fsAsync_1.readdirRecursive)({
|
|
40
|
+
path: filePath,
|
|
41
|
+
ignoreStrings: ["node_modules", ".git"],
|
|
42
|
+
maxDepth: 20,
|
|
43
|
+
});
|
|
44
|
+
const mappings = await (0, sourcemap_1.findSourceMapMappings)(files, rootDir);
|
|
45
|
+
const result = await (0, sourcemap_1.uploadSourceMaps)(mappings, {
|
|
46
|
+
projectId,
|
|
47
|
+
bucketName,
|
|
48
|
+
appVersion,
|
|
49
|
+
options,
|
|
50
|
+
});
|
|
51
|
+
successCount = result.successCount;
|
|
52
|
+
failedFiles.push(...result.failedFiles);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
throw new error_1.FirebaseError("provide a valid directory to mapping file(s), e.g. app/build/outputs");
|
|
56
|
+
}
|
|
57
|
+
(0, utils_1.logLabeledBullet)("crashlytics", `Uploaded ${successCount} (${failedFiles.length} failed) mapping files to ${bucketName}`);
|
|
58
|
+
if (failedFiles.length > 0) {
|
|
59
|
+
(0, utils_1.logLabeledBullet)("crashlytics", `Could not upload the following files:\n${failedFiles.join("\n")}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
package/lib/commands/index.js
CHANGED
|
@@ -57,6 +57,10 @@ function load(client) {
|
|
|
57
57
|
client.crashlytics.mappingfile = {};
|
|
58
58
|
client.crashlytics.mappingfile.generateid = loadCommand("crashlytics-mappingfile-generateid");
|
|
59
59
|
client.crashlytics.mappingfile.upload = loadCommand("crashlytics-mappingfile-upload");
|
|
60
|
+
if (experiments.isEnabled("crashlyticsWeb")) {
|
|
61
|
+
client.crashlytics.sourcemap = {};
|
|
62
|
+
client.crashlytics.sourcemap.upload = loadCommand("crashlytics-sourcemap-upload");
|
|
63
|
+
}
|
|
60
64
|
client.database = {};
|
|
61
65
|
client.database.get = loadCommand("database-get");
|
|
62
66
|
client.database.import = loadCommand("database-import");
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONCURRENCY = void 0;
|
|
4
|
+
exports.checkGoogleAppID = checkGoogleAppID;
|
|
5
|
+
exports.getAppVersion = getAppVersion;
|
|
6
|
+
exports.getGitCommit = getGitCommit;
|
|
7
|
+
exports.getPackageVersion = getPackageVersion;
|
|
8
|
+
exports.upsertBucket = upsertBucket;
|
|
9
|
+
exports.findSourceMapMappings = findSourceMapMappings;
|
|
10
|
+
exports.getLinkedSourceMapPath = getLinkedSourceMapPath;
|
|
11
|
+
exports.uploadSourceMaps = uploadSourceMaps;
|
|
12
|
+
exports.uploadMap = uploadMap;
|
|
13
|
+
exports.normalizeFileName = normalizeFileName;
|
|
14
|
+
exports.registerSourceMap = registerSourceMap;
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
18
|
+
const pLimit = require("p-limit");
|
|
19
|
+
const apiv2_1 = require("../apiv2");
|
|
20
|
+
const error_1 = require("../error");
|
|
21
|
+
const logger_1 = require("../logger");
|
|
22
|
+
const utils_1 = require("../utils");
|
|
23
|
+
const gcs = require("../gcp/storage");
|
|
24
|
+
const archiveFile_1 = require("../archiveFile");
|
|
25
|
+
exports.CONCURRENCY = 25;
|
|
26
|
+
function checkGoogleAppID(options) {
|
|
27
|
+
if (!options.app) {
|
|
28
|
+
throw new error_1.FirebaseError("set --app <appId> to a valid Firebase application id, e.g. 1:00000000:android:0000000");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getAppVersion(options) {
|
|
32
|
+
if (options.appVersion) {
|
|
33
|
+
return options.appVersion;
|
|
34
|
+
}
|
|
35
|
+
const gitCommit = getGitCommit();
|
|
36
|
+
if (gitCommit) {
|
|
37
|
+
(0, utils_1.logLabeledBullet)("crashlytics", `Using git commit as app version: ${gitCommit}`);
|
|
38
|
+
return gitCommit;
|
|
39
|
+
}
|
|
40
|
+
const packageVersion = getPackageVersion();
|
|
41
|
+
if (packageVersion) {
|
|
42
|
+
(0, utils_1.logLabeledBullet)("crashlytics", `Using package version as app version: ${packageVersion}`);
|
|
43
|
+
return packageVersion;
|
|
44
|
+
}
|
|
45
|
+
return "unset";
|
|
46
|
+
}
|
|
47
|
+
function getGitCommit() {
|
|
48
|
+
if (!(0, utils_1.commandExistsSync)("git")) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return (0, node_child_process_1.execSync)("git rev-parse HEAD").toString().trim();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getPackageVersion() {
|
|
59
|
+
if (!(0, utils_1.commandExistsSync)("npm")) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return (0, node_child_process_1.execSync)("npm pkg get version").toString().trim().replaceAll('"', "");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function upsertBucket(projectId, projectNumber, options) {
|
|
70
|
+
let loc = "US-CENTRAL1";
|
|
71
|
+
if (options.bucketLocation) {
|
|
72
|
+
loc = options.bucketLocation.toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
(0, utils_1.logLabeledBullet)("crashlytics", "No Google Cloud Storage bucket location specified. Defaulting to US-CENTRAL1.");
|
|
76
|
+
}
|
|
77
|
+
const baseName = `firebasecrashlytics-sourcemaps-${projectNumber}-${loc.toLowerCase()}`;
|
|
78
|
+
return await gcs.upsertBucket({
|
|
79
|
+
product: "crashlytics",
|
|
80
|
+
createMessage: `Creating Cloud Storage bucket in ${loc} to store Crashlytics source maps at ${baseName}...`,
|
|
81
|
+
projectId,
|
|
82
|
+
req: {
|
|
83
|
+
baseName,
|
|
84
|
+
purposeLabel: `crashlytics-sourcemaps-${loc.toLowerCase()}`,
|
|
85
|
+
location: loc,
|
|
86
|
+
lifecycle: {
|
|
87
|
+
rule: [
|
|
88
|
+
{
|
|
89
|
+
action: {
|
|
90
|
+
type: "Delete",
|
|
91
|
+
},
|
|
92
|
+
condition: {
|
|
93
|
+
age: 30,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function findSourceMapMappings(files, rootDir) {
|
|
102
|
+
const jsFiles = files.filter((f) => f.name.endsWith(".js"));
|
|
103
|
+
const mapFiles = files.filter((f) => f.name.endsWith(".js.map"));
|
|
104
|
+
const mappings = [];
|
|
105
|
+
const mapFilePathsSet = new Set(mapFiles.map((f) => f.name));
|
|
106
|
+
const mapFilesLinkedInJsComment = new Set();
|
|
107
|
+
const limit = pLimit(exports.CONCURRENCY);
|
|
108
|
+
const results = await Promise.all(jsFiles.map((jsFile) => limit(async () => {
|
|
109
|
+
const mapFilePath = await getLinkedSourceMapPath(jsFile.name);
|
|
110
|
+
return { jsFile, mapFilePath };
|
|
111
|
+
})));
|
|
112
|
+
for (const { jsFile, mapFilePath } of results) {
|
|
113
|
+
if (mapFilePath && mapFilePathsSet.has(mapFilePath)) {
|
|
114
|
+
mappings.push({
|
|
115
|
+
mapFilePath,
|
|
116
|
+
obfuscatedFilePath: path.relative(rootDir, path.resolve(jsFile.name)),
|
|
117
|
+
});
|
|
118
|
+
mapFilesLinkedInJsComment.add(mapFilePath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const mapFile of mapFiles) {
|
|
122
|
+
if (!mapFilesLinkedInJsComment.has(mapFile.name)) {
|
|
123
|
+
mappings.push({
|
|
124
|
+
mapFilePath: mapFile.name,
|
|
125
|
+
obfuscatedFilePath: path.relative(rootDir, path.resolve(mapFile.name)),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return mappings;
|
|
130
|
+
}
|
|
131
|
+
async function getLinkedSourceMapPath(jsFilePath) {
|
|
132
|
+
let fileHandle;
|
|
133
|
+
try {
|
|
134
|
+
const stat = await fs.promises.stat(jsFilePath);
|
|
135
|
+
const size = stat.size;
|
|
136
|
+
const bufferSize = Math.min(size, 4096);
|
|
137
|
+
if (bufferSize === 0) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
fileHandle = await fs.promises.open(jsFilePath, "r");
|
|
141
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
142
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, bufferSize, size - bufferSize);
|
|
143
|
+
const tail = buffer.toString("utf-8", 0, bytesRead);
|
|
144
|
+
const regex = /^\/\/\s*[#@]\s*sourceMappingURL=(?<sourceMappingURL>.+)\s*$/m;
|
|
145
|
+
const match = regex.exec(tail);
|
|
146
|
+
const sourceMappingURL = match?.groups?.sourceMappingURL?.trim();
|
|
147
|
+
if (sourceMappingURL) {
|
|
148
|
+
return path.join(path.dirname(jsFilePath), sourceMappingURL);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
logger_1.logger.debug(`Error reading sourceMappingURL from ${jsFilePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (fileHandle) {
|
|
156
|
+
try {
|
|
157
|
+
await fileHandle.close();
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
async function uploadSourceMaps(mappings, request) {
|
|
166
|
+
const { projectId, bucketName, appVersion, options } = request;
|
|
167
|
+
const limit = pLimit(exports.CONCURRENCY);
|
|
168
|
+
const results = await Promise.all(mappings.map((mapping) => limit(async () => {
|
|
169
|
+
const uploadRequest = {
|
|
170
|
+
projectId,
|
|
171
|
+
mappingFile: mapping.mapFilePath,
|
|
172
|
+
obfuscatedFilePath: mapping.obfuscatedFilePath,
|
|
173
|
+
bucketName,
|
|
174
|
+
appVersion,
|
|
175
|
+
options,
|
|
176
|
+
};
|
|
177
|
+
let success = await uploadMap(uploadRequest, 1);
|
|
178
|
+
if (!success) {
|
|
179
|
+
await new Promise((res) => setTimeout(res, options.retryDelay || 5000));
|
|
180
|
+
success = await uploadMap(uploadRequest);
|
|
181
|
+
}
|
|
182
|
+
return success;
|
|
183
|
+
})));
|
|
184
|
+
let successCount = 0;
|
|
185
|
+
const failedFiles = [];
|
|
186
|
+
for (const [i, success] of results.entries()) {
|
|
187
|
+
if (success) {
|
|
188
|
+
successCount++;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
failedFiles.push(mappings[i].mapFilePath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
successCount,
|
|
196
|
+
failedFiles,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function uploadMap(request, attemptsRemaining = 0) {
|
|
200
|
+
const { projectId, mappingFile, obfuscatedFilePath, bucketName, appVersion, options } = request;
|
|
201
|
+
const filePath = path.relative(options.projectRoot ?? process.cwd(), mappingFile);
|
|
202
|
+
const obfuscatedPath = obfuscatedFilePath
|
|
203
|
+
.split(path.sep)
|
|
204
|
+
.map((p) => (p === ".next" ? "_next" : p))
|
|
205
|
+
.filter((p) => p !== "dev")
|
|
206
|
+
.join("/");
|
|
207
|
+
const tmpArchive = await (0, archiveFile_1.archiveFile)(filePath, { archivedFileName: "mapping.js.map" });
|
|
208
|
+
const appId = options.app || "";
|
|
209
|
+
const gcsFile = `${appId}-${appVersion}-${normalizeFileName(obfuscatedPath)}.zip`;
|
|
210
|
+
const uid = (0, utils_1.murmurHashV3)(`${appId}-${appVersion}-${obfuscatedPath}`);
|
|
211
|
+
const name = `projects/${projectId}/locations/global/mappingFiles/${uid}`;
|
|
212
|
+
const stream = fs.createReadStream(tmpArchive);
|
|
213
|
+
stream.on("error", (err) => {
|
|
214
|
+
logger_1.logger.debug(`Stream error on tmpArchive: ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
const { bucket, object } = await gcs.uploadObject({
|
|
218
|
+
file: gcsFile,
|
|
219
|
+
stream,
|
|
220
|
+
}, bucketName);
|
|
221
|
+
const fileUri = `gs://${bucket}/${object}`;
|
|
222
|
+
logger_1.logger.debug(`Uploaded mapping file ${filePath} to ${fileUri}`);
|
|
223
|
+
await registerSourceMap({
|
|
224
|
+
name,
|
|
225
|
+
appId,
|
|
226
|
+
version: appVersion,
|
|
227
|
+
obfuscatedFilePath: `/${obfuscatedPath}`,
|
|
228
|
+
fileUri,
|
|
229
|
+
});
|
|
230
|
+
logger_1.logger.debug(`Registered mapping file ${filePath}`);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
if (attemptsRemaining === 0) {
|
|
235
|
+
(0, utils_1.logLabeledWarning)("crashlytics", `Failed to upload mapping file ${filePath}:\n${e instanceof Error ? e.message : String(e)}`);
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
stream.destroy();
|
|
241
|
+
try {
|
|
242
|
+
fs.rmSync(tmpArchive, { force: true });
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
logger_1.logger.debug(`Failed to delete temporary archive ${tmpArchive}: ${err instanceof Error ? err.message : String(err)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function normalizeFileName(fileName) {
|
|
250
|
+
return fileName.replaceAll(/\//g, "-");
|
|
251
|
+
}
|
|
252
|
+
async function registerSourceMap(sourceMap) {
|
|
253
|
+
const client = new apiv2_1.Client({
|
|
254
|
+
urlPrefix: "https://firebasetelemetryadmin.googleapis.com",
|
|
255
|
+
auth: true,
|
|
256
|
+
apiVersion: "v1alpha",
|
|
257
|
+
});
|
|
258
|
+
try {
|
|
259
|
+
await client.patch(sourceMap.name, sourceMap, { queryParams: { allowMissing: "true" } });
|
|
260
|
+
logger_1.logger.debug(`Registered source map ${sourceMap.obfuscatedFilePath} with Firebase Telemetry service`);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
if (e instanceof error_1.FirebaseError) {
|
|
264
|
+
if (e.status === 409) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
throw new error_1.FirebaseError(`Failed to register source map ${sourceMap.obfuscatedFilePath} with Firebase Telemetry service:\n${e instanceof Error ? e.message : String(e)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensureApis = ensureApis;
|
|
4
|
-
exports.ensureGIFApiTos = ensureGIFApiTos;
|
|
5
4
|
const api = require("../api");
|
|
6
|
-
const configstore_1 = require("../configstore");
|
|
7
5
|
const ensureApiEnabled_1 = require("../ensureApiEnabled");
|
|
8
6
|
const prefix = "dataconnect";
|
|
9
7
|
async function ensureApis(projectId, silent = false) {
|
|
@@ -12,14 +10,3 @@ async function ensureApis(projectId, silent = false) {
|
|
|
12
10
|
(0, ensureApiEnabled_1.ensure)(projectId, api.cloudSQLAdminOrigin(), prefix, silent),
|
|
13
11
|
]);
|
|
14
12
|
}
|
|
15
|
-
async function ensureGIFApiTos(projectId) {
|
|
16
|
-
if (configstore_1.configstore.get("gemini")) {
|
|
17
|
-
await (0, ensureApiEnabled_1.ensure)(projectId, api.cloudAiCompanionOrigin(), "");
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
if (!(await (0, ensureApiEnabled_1.check)(projectId, api.cloudAiCompanionOrigin(), ""))) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
@@ -4,7 +4,9 @@ exports.default = default_1;
|
|
|
4
4
|
exports.injectEnvVarsFromApphostingConfig = injectEnvVarsFromApphostingConfig;
|
|
5
5
|
exports.injectAutoInitEnvVars = injectAutoInitEnvVars;
|
|
6
6
|
exports.getBackendConfigs = getBackendConfigs;
|
|
7
|
+
const crypto = require("crypto");
|
|
7
8
|
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
8
10
|
const path = require("path");
|
|
9
11
|
const fsAsync = require("../../fsAsync");
|
|
10
12
|
const util_1 = require("./util");
|
|
@@ -19,7 +21,6 @@ const getProjectNumber_1 = require("../../getProjectNumber");
|
|
|
19
21
|
const prompt_1 = require("../../prompt");
|
|
20
22
|
const utils_1 = require("../../utils");
|
|
21
23
|
const localbuilds_1 = require("../../apphosting/localbuilds");
|
|
22
|
-
const constants_1 = require("../../apphosting/constants");
|
|
23
24
|
const error_1 = require("../../error");
|
|
24
25
|
const managementApps = require("../../management/apps");
|
|
25
26
|
const utils_2 = require("../../apphosting/utils");
|
|
@@ -136,8 +137,9 @@ async function default_1(context, options) {
|
|
|
136
137
|
(0, utils_1.logLabeledBullet)("apphosting", `Starting local build for backend ${cfg.backendId}`);
|
|
137
138
|
await injectEnvVarsFromApphostingConfig(configs.filter((c) => c.backendId === cfg.backendId), options, buildEnv, runtimeEnv);
|
|
138
139
|
await injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv);
|
|
139
|
-
const rootDir = options.projectRoot || process.cwd();
|
|
140
|
-
const
|
|
140
|
+
const rootDir = path.resolve(options.projectRoot || process.cwd());
|
|
141
|
+
const pathHash = crypto.createHash("md5").update(rootDir).digest("hex").substring(0, 8);
|
|
142
|
+
const localBuildScratchDir = path.join(os.tmpdir(), `apphosting-local-build-${cfg.backendId}-${pathHash}`);
|
|
141
143
|
try {
|
|
142
144
|
await prepareLocalBuildScratchDirectory(rootDir, localBuildScratchDir, cfg);
|
|
143
145
|
const { outputFiles, buildConfig } = await (0, localbuilds_1.localBuild)(projectId, localBuildScratchDir, buildEnv[cfg.backendId] || {}, {
|
|
@@ -250,9 +252,7 @@ async function ensureAppHostingServiceAgentRoles(projectId, projectNumber) {
|
|
|
250
252
|
}
|
|
251
253
|
async function prepareLocalBuildScratchDirectory(rootDir, localBuildScratchDir, cfg) {
|
|
252
254
|
const ignore = (0, util_1.resolveIgnorePatterns)(cfg);
|
|
253
|
-
|
|
254
|
-
throw new error_1.FirebaseError(`The local build scratch directory '${localBuildScratchDir}' already exists. Please delete it and try again.`);
|
|
255
|
-
}
|
|
255
|
+
fs.rmSync(localBuildScratchDir, { recursive: true, force: true });
|
|
256
256
|
fs.mkdirSync(localBuildScratchDir, { recursive: true });
|
|
257
257
|
const filesToCopy = await fsAsync.readdirRecursive({
|
|
258
258
|
path: rootDir,
|
|
@@ -7,7 +7,6 @@ function initDeployStats() {
|
|
|
7
7
|
numBuildErrors: 0,
|
|
8
8
|
numBuildWarnings: new Map(),
|
|
9
9
|
numServiceCreated: 0,
|
|
10
|
-
numServiceDeleted: 0,
|
|
11
10
|
numSchemaMigrated: 0,
|
|
12
11
|
numConnectorUpdatedBeforeSchema: 0,
|
|
13
12
|
numConnectorUpdatedAfterSchema: 0,
|
|
@@ -24,7 +23,6 @@ function deployStatsParams(stats) {
|
|
|
24
23
|
return {
|
|
25
24
|
missing_billing: (!!stats.missingBilling).toString(),
|
|
26
25
|
num_service_created: stats.numServiceCreated,
|
|
27
|
-
num_service_deleted: stats.numServiceDeleted,
|
|
28
26
|
num_schema_migrated: stats.numSchemaMigrated,
|
|
29
27
|
num_connector_updated_before_schema: stats.numConnectorUpdatedBeforeSchema,
|
|
30
28
|
num_connector_updated_after_schema: stats.numConnectorUpdatedAfterSchema,
|