firebase-tools 14.10.1 → 14.11.1
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/api.js +3 -1
- package/lib/apptesting/ensureProjectConfigured.js +55 -0
- package/lib/apptesting/invokeTests.js +40 -0
- package/lib/apptesting/parseTestFiles.js +75 -0
- package/lib/apptesting/types.js +18 -0
- package/lib/commands/apptesting-execute.js +102 -0
- package/lib/commands/functions-config-clone.js +2 -0
- package/lib/commands/functions-config-get.js +2 -0
- package/lib/commands/functions-config-set.js +2 -0
- package/lib/commands/functions-config-unset.js +2 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/init.js +7 -0
- package/lib/deploy/functions/prepare.js +3 -1
- package/lib/deploy/functions/prepareFunctionsUpload.js +2 -0
- package/lib/deploy/functions/release/reporter.js +1 -0
- package/lib/deploy/functions/runtimes/discovery/index.js +53 -4
- package/lib/deploy/functions/runtimes/node/index.js +74 -44
- package/lib/emulator/downloadableEmulatorInfo.json +18 -18
- package/lib/emulator/env.js +2 -1
- package/lib/emulator/hub.js +1 -2
- package/lib/emulator/tasksEmulator.js +1 -1
- package/lib/emulator/ui.js +3 -1
- package/lib/env.js +18 -0
- package/lib/experiments.js +11 -0
- package/lib/firestore/api.js +15 -11
- package/lib/functions/deprecationWarnings.js +21 -0
- package/lib/init/features/aitools/claude.js +44 -0
- package/lib/init/features/aitools/cursor.js +62 -0
- package/lib/init/features/aitools/gemini.js +58 -0
- package/lib/init/features/aitools/index.js +28 -0
- package/lib/init/features/aitools/promptUpdater.js +112 -0
- package/lib/init/features/aitools/studio.js +17 -0
- package/lib/init/features/aitools/types.js +2 -0
- package/lib/init/features/aitools.js +83 -0
- package/lib/init/features/apptesting/index.js +33 -0
- package/lib/init/features/index.js +4 -1
- package/lib/init/index.js +5 -0
- package/lib/mcp/index.js +75 -27
- package/lib/mcp/logging-transport.js +23 -0
- package/lib/mcp/util.js +2 -1
- package/lib/requireAuth.js +9 -3
- package/lib/timeout.js +21 -0
- package/lib/track.js +2 -2
- package/lib/utils.js +27 -2
- package/package.json +2 -1
- package/prompts/FIREBASE.md +122 -0
- package/prompts/FIREBASE_FUNCTIONS.md +221 -0
- package/schema/apptesting-yaml.json +64 -0
- package/templates/init/aitools/cursor-rules-header.txt +8 -0
- package/templates/init/aitools/gemini-extension.json +11 -0
- package/templates/init/apptesting/smoke_test.yaml +6 -0
- package/lib/emulator/adminSdkConfig.test.js +0 -16
- package/lib/firestore/api-sort.test.js +0 -74
- package/lib/firestore/backupUtils.test.js +0 -18
- package/lib/firestore/pretty-print.test.js +0 -61
- package/lib/firestore/util.test.js +0 -42
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
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.dynamicLinksKey = exports.dynamicLinksOrigin = 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.setScopes = exports.getScopes = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
4
|
+
exports.setScopes = exports.getScopes = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
5
5
|
const constants_1 = require("./emulator/constants");
|
|
6
6
|
const logger_1 = require("./logger");
|
|
7
7
|
const scopes = require("./scopes");
|
|
@@ -144,6 +144,8 @@ const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatf
|
|
|
144
144
|
exports.vertexAIOrigin = vertexAIOrigin;
|
|
145
145
|
const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
|
|
146
146
|
exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
|
|
147
|
+
const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
|
|
148
|
+
exports.appTestingOrigin = appTestingOrigin;
|
|
147
149
|
function getScopes() {
|
|
148
150
|
return Array.from(commandScopes);
|
|
149
151
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureProjectConfigured = void 0;
|
|
4
|
+
const resourceManager_1 = require("../gcp/resourceManager");
|
|
5
|
+
const ensureApiEnabled_1 = require("../ensureApiEnabled");
|
|
6
|
+
const api_1 = require("../api");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
const error_1 = require("../error");
|
|
9
|
+
const iam = require("../gcp/iam");
|
|
10
|
+
const prompt_1 = require("../prompt");
|
|
11
|
+
const TEST_RUNNER_ROLE = "roles/firebaseapptesting.testRunner";
|
|
12
|
+
const TEST_RUNNER_SERVICE_ACCOUNT_NAME = "firebaseapptesting-test-runner";
|
|
13
|
+
async function ensureProjectConfigured(projectId) {
|
|
14
|
+
await (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.appTestingOrigin)(), "storage", false);
|
|
15
|
+
await (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.appTestingOrigin)(), "run", false);
|
|
16
|
+
await (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.appTestingOrigin)(), "artifactregistry", false);
|
|
17
|
+
const serviceAccount = runnerServiceAccount(projectId);
|
|
18
|
+
const serviceAccountExistsAndIsRunner = await (0, resourceManager_1.serviceAccountHasRoles)(projectId, serviceAccount, [TEST_RUNNER_ROLE], true);
|
|
19
|
+
if (!serviceAccountExistsAndIsRunner) {
|
|
20
|
+
const grant = await (0, prompt_1.confirm)(`Firebase App Testing runs tests in Cloud Run using a service account, provision an account, "${serviceAccount}", with the role "${TEST_RUNNER_ROLE}"?`);
|
|
21
|
+
if (!grant) {
|
|
22
|
+
(0, utils_1.logBullet)("You, or your project administrator, should run the following command to grant the required role:\n\n" +
|
|
23
|
+
`\tgcloud projects add-iam-policy-binding ${projectId} \\\n` +
|
|
24
|
+
`\t --member="serviceAccount:${serviceAccount}" \\\n` +
|
|
25
|
+
`\t --role="${TEST_RUNNER_ROLE}"\n`);
|
|
26
|
+
throw new error_1.FirebaseError(`Firebase App Testing requires a service account named "${serviceAccount}" with the "${TEST_RUNNER_ROLE}" role to execute tests using Cloud Run`);
|
|
27
|
+
}
|
|
28
|
+
await provisionServiceAccount(projectId, serviceAccount);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.ensureProjectConfigured = ensureProjectConfigured;
|
|
32
|
+
async function provisionServiceAccount(projectId, serviceAccount) {
|
|
33
|
+
try {
|
|
34
|
+
await iam.createServiceAccount(projectId, TEST_RUNNER_SERVICE_ACCOUNT_NAME, "Service Account used in Cloud Run, responsible for running tests", "Firebase App Testing Test Runner");
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if ((0, error_1.getErrStatus)(err) !== 409) {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await (0, resourceManager_1.addServiceAccountToRoles)(projectId, serviceAccount, [TEST_RUNNER_ROLE], true);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if ((0, error_1.getErrStatus)(err) === 400) {
|
|
46
|
+
(0, utils_1.logWarning)(`Your App Testing runner service account, "${serviceAccount}", is still being provisioned in the background. If you encounter an error, please try again after a few moments.`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function runnerServiceAccount(projectId) {
|
|
54
|
+
return `${TEST_RUNNER_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pollInvocationStatus = exports.invokeTests = void 0;
|
|
4
|
+
const apiv2_1 = require("../apiv2");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const operationPoller = require("../operation-poller");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.appTestingOrigin)(), apiVersion: "v1alpha" });
|
|
9
|
+
async function invokeTests(appId, startUri, testDefs) {
|
|
10
|
+
const appResource = `projects/${appId.split(":")[1]}/apps/${appId}`;
|
|
11
|
+
try {
|
|
12
|
+
const invocationResponse = await apiClient.post(`${appResource}/testInvocations:invokeTestCases`, buildInvokeTestCasesRequest(testDefs));
|
|
13
|
+
return invocationResponse.body;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new error_1.FirebaseError("Test invocation failed", { original: (0, error_1.getError)(err) });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.invokeTests = invokeTests;
|
|
20
|
+
function buildInvokeTestCasesRequest(testCaseInvocations) {
|
|
21
|
+
return {
|
|
22
|
+
resource: {
|
|
23
|
+
testInvocation: {},
|
|
24
|
+
testCaseInvocations,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function pollInvocationStatus(operationName, onPoll, backoff = 30 * 1000) {
|
|
29
|
+
return operationPoller.pollOperation({
|
|
30
|
+
pollerName: "App Testing Invocation Poller",
|
|
31
|
+
apiOrigin: (0, api_1.appTestingOrigin)(),
|
|
32
|
+
apiVersion: "v1alpha",
|
|
33
|
+
operationResourceName: operationName,
|
|
34
|
+
masterTimeout: 30 * 60 * 1000,
|
|
35
|
+
backoff,
|
|
36
|
+
maxBackoff: 15 * 1000,
|
|
37
|
+
onPoll,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
exports.pollInvocationStatus = pollInvocationStatus;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTestFiles = void 0;
|
|
4
|
+
const fsutils_1 = require("../fsutils");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const error_1 = require("../error");
|
|
10
|
+
function createFilter(pattern) {
|
|
11
|
+
const regex = pattern ? new RegExp(pattern) : undefined;
|
|
12
|
+
return (s) => !regex || regex.test(s);
|
|
13
|
+
}
|
|
14
|
+
async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
|
|
15
|
+
try {
|
|
16
|
+
new URL(targetUri);
|
|
17
|
+
}
|
|
18
|
+
catch (ex) {
|
|
19
|
+
const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)");
|
|
20
|
+
throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
|
|
21
|
+
}
|
|
22
|
+
const fileFilterFn = createFilter(filePattern);
|
|
23
|
+
const nameFilterFn = createFilter(namePattern);
|
|
24
|
+
async function parseTestFilesRecursive(testDir) {
|
|
25
|
+
const items = (0, fsutils_1.listFiles)(testDir);
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
const path = (0, path_1.join)(testDir, item);
|
|
29
|
+
if ((0, fsutils_1.dirExistsSync)(path)) {
|
|
30
|
+
results.push(...(await parseTestFilesRecursive(path)));
|
|
31
|
+
}
|
|
32
|
+
else if (fileFilterFn(path) && (0, fsutils_1.fileExistsSync)(path)) {
|
|
33
|
+
try {
|
|
34
|
+
const file = await (0, utils_1.readFileFromDirectory)(testDir, item);
|
|
35
|
+
const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
|
|
36
|
+
const tests = parsedFile.tests;
|
|
37
|
+
const defaultConfig = parsedFile.defaultConfig;
|
|
38
|
+
if (!tests || !tests.length) {
|
|
39
|
+
logger_1.logger.info(`No tests found in ${path}. Ignoring.`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const rawTestDef of parsedFile.tests) {
|
|
43
|
+
if (!nameFilterFn(rawTestDef.testName))
|
|
44
|
+
continue;
|
|
45
|
+
const testDef = toTestDef(rawTestDef, targetUri, defaultConfig);
|
|
46
|
+
results.push(testDef);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (ex) {
|
|
50
|
+
const errMsg = (0, error_1.getErrMsg)(ex);
|
|
51
|
+
const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
|
|
52
|
+
logger_1.logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
return parseTestFilesRecursive(dir);
|
|
60
|
+
}
|
|
61
|
+
exports.parseTestFiles = parseTestFiles;
|
|
62
|
+
function toTestDef(testDef, targetUri, defaultConfig) {
|
|
63
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
64
|
+
const steps = (_a = testDef.steps) !== null && _a !== void 0 ? _a : [];
|
|
65
|
+
const route = (_d = (_c = (_b = testDef.testConfig) === null || _b === void 0 ? void 0 : _b.route) !== null && _c !== void 0 ? _c : defaultConfig === null || defaultConfig === void 0 ? void 0 : defaultConfig.route) !== null && _d !== void 0 ? _d : "";
|
|
66
|
+
const browsers = (_g = (_f = (_e = testDef.testConfig) === null || _e === void 0 ? void 0 : _e.browsers) !== null && _f !== void 0 ? _f : defaultConfig === null || defaultConfig === void 0 ? void 0 : defaultConfig.browsers) !== null && _g !== void 0 ? _g : [types_1.Browser.CHROME];
|
|
67
|
+
return {
|
|
68
|
+
testCase: {
|
|
69
|
+
startUri: targetUri + route,
|
|
70
|
+
displayName: testDef.testName,
|
|
71
|
+
instructions: { steps },
|
|
72
|
+
},
|
|
73
|
+
testExecution: browsers.map((browser) => ({ config: { browser } })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompletionReason = exports.Browser = void 0;
|
|
4
|
+
var Browser;
|
|
5
|
+
(function (Browser) {
|
|
6
|
+
Browser["BROWSER_UNSPECIFIED"] = "BROWSER_UNSPECIFIED";
|
|
7
|
+
Browser["CHROME"] = "CHROME";
|
|
8
|
+
})(Browser = exports.Browser || (exports.Browser = {}));
|
|
9
|
+
var CompletionReason;
|
|
10
|
+
(function (CompletionReason) {
|
|
11
|
+
CompletionReason[CompletionReason["COMPLETION_REASON_UNSPECIFIED"] = 0] = "COMPLETION_REASON_UNSPECIFIED";
|
|
12
|
+
CompletionReason[CompletionReason["MAX_STEPS_REACHED"] = 1] = "MAX_STEPS_REACHED";
|
|
13
|
+
CompletionReason[CompletionReason["GOAL_FAILED"] = 2] = "GOAL_FAILED";
|
|
14
|
+
CompletionReason[CompletionReason["NO_ACTIONS_REQUIRED"] = 3] = "NO_ACTIONS_REQUIRED";
|
|
15
|
+
CompletionReason[CompletionReason["GOAL_INCONCLUSIVE"] = 4] = "GOAL_INCONCLUSIVE";
|
|
16
|
+
CompletionReason[CompletionReason["TEST_CANCELLED"] = 5] = "TEST_CANCELLED";
|
|
17
|
+
CompletionReason[CompletionReason["GOAL_SUCCEEDED"] = 6] = "GOAL_SUCCEEDED";
|
|
18
|
+
})(CompletionReason = exports.CompletionReason || (exports.CompletionReason = {}));
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const requireAuth_1 = require("../requireAuth");
|
|
5
|
+
const command_1 = require("../command");
|
|
6
|
+
const requireConfig_1 = require("../requireConfig");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const clc = require("colorette");
|
|
9
|
+
const parseTestFiles_1 = require("../apptesting/parseTestFiles");
|
|
10
|
+
const ora = require("ora");
|
|
11
|
+
const invokeTests_1 = require("../apptesting/invokeTests");
|
|
12
|
+
const error_1 = require("../error");
|
|
13
|
+
const marked_1 = require("marked");
|
|
14
|
+
const projectUtils_1 = require("../projectUtils");
|
|
15
|
+
const utils_1 = require("../utils");
|
|
16
|
+
const apps_1 = require("../management/apps");
|
|
17
|
+
exports.command = new command_1.Command("apptesting:execute <target>")
|
|
18
|
+
.description("Run automated tests written in natural language driven by AI")
|
|
19
|
+
.option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
|
|
20
|
+
.option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
|
|
21
|
+
.option("--test-name-pattern <pattern>", "Test name pattern. Only tests with names that match this pattern will be executed.")
|
|
22
|
+
.option("--tests-non-blocking", "Request test execution without waiting for them to complete.")
|
|
23
|
+
.before(requireAuth_1.requireAuth)
|
|
24
|
+
.before(requireConfig_1.requireConfig)
|
|
25
|
+
.action(async (target, options) => {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
28
|
+
const appList = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
|
|
29
|
+
let app = appList.find((a) => a.appId === options.app);
|
|
30
|
+
if (!app && appList.length === 1) {
|
|
31
|
+
app = appList[0];
|
|
32
|
+
logger_1.logger.info(`No app specified, defaulting to ${app.appId}`);
|
|
33
|
+
}
|
|
34
|
+
else if (!app) {
|
|
35
|
+
throw new error_1.FirebaseError("Invalid app id");
|
|
36
|
+
}
|
|
37
|
+
const testDir = ((_a = options.config.src.apptesting) === null || _a === void 0 ? void 0 : _a.testDir) || "tests";
|
|
38
|
+
const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, target, options.testFilePattern, options.testNamePattern);
|
|
39
|
+
if (!tests.length) {
|
|
40
|
+
throw new error_1.FirebaseError("No tests found");
|
|
41
|
+
}
|
|
42
|
+
const invokeSpinner = ora("Requesting test execution");
|
|
43
|
+
invokeSpinner.start();
|
|
44
|
+
let invocationOperation;
|
|
45
|
+
try {
|
|
46
|
+
invocationOperation = await (0, invokeTests_1.invokeTests)(app.appId, target, tests);
|
|
47
|
+
invokeSpinner.text = "Test execution requested";
|
|
48
|
+
invokeSpinner.succeed();
|
|
49
|
+
}
|
|
50
|
+
catch (ex) {
|
|
51
|
+
invokeSpinner.fail("Failed to request test execution");
|
|
52
|
+
throw ex;
|
|
53
|
+
}
|
|
54
|
+
logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(tests.length)}`));
|
|
55
|
+
const invocationId = (_b = invocationOperation.name) === null || _b === void 0 ? void 0 : _b.split("/").pop();
|
|
56
|
+
const appWebId = app.webId;
|
|
57
|
+
const url = (0, utils_1.consoleUrl)(projectId, `/apptesting/app/web:${appWebId}/invocations/${invocationId}`);
|
|
58
|
+
logger_1.logger.info(await (0, marked_1.marked)(`**Invocation ID:** ${invocationId}`));
|
|
59
|
+
logger_1.logger.info(await (0, marked_1.marked)(`View progress and results in the [Firebase Console](${url})`));
|
|
60
|
+
if (options.testsNonBlocking) {
|
|
61
|
+
logger_1.logger.info("Not waiting for results");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!invocationOperation.metadata) {
|
|
65
|
+
throw new error_1.FirebaseError("Invocation details unavailable");
|
|
66
|
+
}
|
|
67
|
+
const executionSpinner = ora(getOutput(invocationOperation.metadata));
|
|
68
|
+
executionSpinner.start();
|
|
69
|
+
const invocationOp = await (0, invokeTests_1.pollInvocationStatus)(invocationOperation.name, (operation) => {
|
|
70
|
+
if (!operation.done) {
|
|
71
|
+
executionSpinner.text = getOutput(operation.metadata);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const response = invocationOp.resource.testInvocation;
|
|
75
|
+
executionSpinner.text = `Testing complete\n${getOutput(response)}`;
|
|
76
|
+
if (response.failedExecutions || response.cancelledExecutions) {
|
|
77
|
+
executionSpinner.fail();
|
|
78
|
+
throw new error_1.FirebaseError("Testing complete with errors");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
executionSpinner.succeed();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
function pluralizeTests(numTests) {
|
|
85
|
+
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
86
|
+
}
|
|
87
|
+
function getOutput(invocation) {
|
|
88
|
+
const output = [];
|
|
89
|
+
if (invocation.runningExecutions) {
|
|
90
|
+
output.push(`${pluralizeTests(invocation.runningExecutions)} running (this may take a while)...`);
|
|
91
|
+
}
|
|
92
|
+
if (invocation.succeededExecutions) {
|
|
93
|
+
output.push(`✔ ${pluralizeTests(invocation.succeededExecutions)} passed`);
|
|
94
|
+
}
|
|
95
|
+
if (invocation.failedExecutions) {
|
|
96
|
+
output.push(`✖ ${pluralizeTests(invocation.failedExecutions)} failed`);
|
|
97
|
+
}
|
|
98
|
+
if (invocation.cancelledExecutions) {
|
|
99
|
+
output.push(`⊝ ${pluralizeTests(invocation.cancelledExecutions)} cancelled`);
|
|
100
|
+
}
|
|
101
|
+
return output.length ? output.join("\n") : "Tests are starting";
|
|
102
|
+
}
|
|
@@ -10,6 +10,7 @@ const requirePermissions_1 = require("../requirePermissions");
|
|
|
10
10
|
const functionsConfig = require("../functionsConfig");
|
|
11
11
|
const functionsConfigClone_1 = require("../functionsConfigClone");
|
|
12
12
|
const utils = require("../utils");
|
|
13
|
+
const deprecationWarnings_1 = require("../functions/deprecationWarnings");
|
|
13
14
|
exports.command = new command_1.Command("functions:config:clone")
|
|
14
15
|
.description("clone environment config from another project")
|
|
15
16
|
.option("--from <projectId>", "the project from which to clone configuration")
|
|
@@ -50,4 +51,5 @@ exports.command = new command_1.Command("functions:config:clone")
|
|
|
50
51
|
await (0, functionsConfigClone_1.functionsConfigClone)(options.from, projectId, only, except);
|
|
51
52
|
utils.logSuccess(`Cloned functions config from ${clc.bold(options.from)} into ${clc.bold(projectId)}`);
|
|
52
53
|
logger_1.logger.info(`\nPlease deploy your functions for the change to take effect by running ${clc.bold("firebase deploy --only functions")}\n`);
|
|
54
|
+
(0, deprecationWarnings_1.logFunctionsConfigDeprecationWarning)();
|
|
53
55
|
});
|
|
@@ -8,6 +8,7 @@ const logger_1 = require("../logger");
|
|
|
8
8
|
const projectUtils_1 = require("../projectUtils");
|
|
9
9
|
const requirePermissions_1 = require("../requirePermissions");
|
|
10
10
|
const functionsConfig = require("../functionsConfig");
|
|
11
|
+
const deprecationWarnings_1 = require("../functions/deprecationWarnings");
|
|
11
12
|
async function materialize(projectId, path) {
|
|
12
13
|
if (path === undefined) {
|
|
13
14
|
return functionsConfig.materializeAll(projectId);
|
|
@@ -31,5 +32,6 @@ exports.command = new command_1.Command("functions:config:get [path]")
|
|
|
31
32
|
.action(async (path, options) => {
|
|
32
33
|
const result = await materialize((0, projectUtils_1.needProjectId)(options), path);
|
|
33
34
|
logger_1.logger.info(JSON.stringify(result, null, 2));
|
|
35
|
+
(0, deprecationWarnings_1.logFunctionsConfigDeprecationWarning)();
|
|
34
36
|
return result;
|
|
35
37
|
});
|
|
@@ -9,6 +9,7 @@ const projectUtils_1 = require("../projectUtils");
|
|
|
9
9
|
const requirePermissions_1 = require("../requirePermissions");
|
|
10
10
|
const functionsConfig = require("../functionsConfig");
|
|
11
11
|
const utils = require("../utils");
|
|
12
|
+
const deprecationWarnings_1 = require("../functions/deprecationWarnings");
|
|
12
13
|
exports.command = new command_1.Command("functions:config:set [values...]")
|
|
13
14
|
.description("set environment config with key=value syntax")
|
|
14
15
|
.before(requirePermissions_1.requirePermissions, [
|
|
@@ -40,4 +41,5 @@ exports.command = new command_1.Command("functions:config:set [values...]")
|
|
|
40
41
|
await Promise.all(promises);
|
|
41
42
|
utils.logSuccess("Functions config updated.");
|
|
42
43
|
logger_1.logger.info(`\nPlease deploy your functions for the change to take effect by running ${clc.bold("firebase deploy --only functions")}\n`);
|
|
44
|
+
(0, deprecationWarnings_1.logFunctionsConfigDeprecationWarning)();
|
|
43
45
|
});
|
|
@@ -10,6 +10,7 @@ const functionsConfig = require("../functionsConfig");
|
|
|
10
10
|
const runtimeconfig = require("../gcp/runtimeconfig");
|
|
11
11
|
const utils = require("../utils");
|
|
12
12
|
const error_1 = require("../error");
|
|
13
|
+
const deprecationWarnings_1 = require("../functions/deprecationWarnings");
|
|
13
14
|
exports.command = new command_1.Command("functions:config:unset [keys...]")
|
|
14
15
|
.description("unset environment config at the specified path(s)")
|
|
15
16
|
.before(requirePermissions_1.requirePermissions, [
|
|
@@ -39,4 +40,5 @@ exports.command = new command_1.Command("functions:config:unset [keys...]")
|
|
|
39
40
|
}));
|
|
40
41
|
utils.logSuccess("Environment updated.");
|
|
41
42
|
logger_1.logger.info(`\nPlease deploy your functions for the change to take effect by running ${clc.bold("firebase deploy --only functions")}\n`);
|
|
43
|
+
(0, deprecationWarnings_1.logFunctionsConfigDeprecationWarning)();
|
|
42
44
|
});
|
package/lib/commands/index.js
CHANGED
|
@@ -228,6 +228,10 @@ function load(client) {
|
|
|
228
228
|
client.target.clear = loadCommand("target-clear");
|
|
229
229
|
client.target.remove = loadCommand("target-remove");
|
|
230
230
|
client.use = loadCommand("use");
|
|
231
|
+
if (experiments.isEnabled("apptesting")) {
|
|
232
|
+
client.apptesting = {};
|
|
233
|
+
client.apptesting.execute = loadCommand("apptesting-execute");
|
|
234
|
+
}
|
|
231
235
|
const t1 = process.hrtime.bigint();
|
|
232
236
|
const diffMS = (t1 - t0) / BigInt(1e6);
|
|
233
237
|
if (diffMS > 100) {
|
package/lib/commands/init.js
CHANGED
|
@@ -99,6 +99,13 @@ if ((0, experiments_1.isEnabled)("genkit")) {
|
|
|
99
99
|
...choices.slice(2),
|
|
100
100
|
];
|
|
101
101
|
}
|
|
102
|
+
if ((0, experiments_1.isEnabled)("apptesting")) {
|
|
103
|
+
choices.push({
|
|
104
|
+
value: "apptesting",
|
|
105
|
+
name: "App Testing: create a smoke test, enable Cloud APIs (storage, run, & artifactregistry), and add a service account.",
|
|
106
|
+
checked: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
102
109
|
const featureNames = choices.map((choice) => choice.value);
|
|
103
110
|
const HELP = `Interactively configure the current directory as a Firebase project or initialize new features in an already configured Firebase project directory.
|
|
104
111
|
|
|
@@ -53,7 +53,9 @@ async function prepare(context, options, payload) {
|
|
|
53
53
|
context.firebaseConfig = firebaseConfig;
|
|
54
54
|
let runtimeConfig = { firebase: firebaseConfig };
|
|
55
55
|
if (checkAPIsEnabled[1]) {
|
|
56
|
-
|
|
56
|
+
const config = await (0, prepareFunctionsUpload_1.getFunctionsConfig)(projectId);
|
|
57
|
+
runtimeConfig = Object.assign(Object.assign({}, runtimeConfig), config);
|
|
58
|
+
context.hasRuntimeConfig = Object.keys(config).length > 0;
|
|
57
59
|
}
|
|
58
60
|
context.codebaseDeployEvents = {};
|
|
59
61
|
const wantBuilds = await loadCodebases(context.config, options, firebaseConfig, runtimeConfig, context.filters);
|
|
@@ -13,6 +13,7 @@ const hash_1 = require("./cache/hash");
|
|
|
13
13
|
const functionsConfig = require("../../functionsConfig");
|
|
14
14
|
const utils = require("../../utils");
|
|
15
15
|
const fsAsync = require("../../fsAsync");
|
|
16
|
+
const deprecationWarnings_1 = require("../../functions/deprecationWarnings");
|
|
16
17
|
const CONFIG_DEST_FILE = ".runtimeconfig.json";
|
|
17
18
|
async function getFunctionsConfig(projectId) {
|
|
18
19
|
var _a, _b;
|
|
@@ -73,6 +74,7 @@ async function packageSource(sourceDir, config, runtimeConfig) {
|
|
|
73
74
|
name: CONFIG_DEST_FILE,
|
|
74
75
|
mode: 420,
|
|
75
76
|
});
|
|
77
|
+
(0, deprecationWarnings_1.logFunctionsConfigDeprecationWarning)();
|
|
76
78
|
}
|
|
77
79
|
await pipeAsync(archive, fileStream);
|
|
78
80
|
}
|
|
@@ -81,6 +81,7 @@ async function logAndTrackDeployStats(summary, context) {
|
|
|
81
81
|
fn_deploy_num_successes: totalSuccesses,
|
|
82
82
|
fn_deploy_num_canceled: totalAborts,
|
|
83
83
|
fn_deploy_num_failures: totalErrors,
|
|
84
|
+
has_runtime_config: String(!!(context === null || context === void 0 ? void 0 : context.hasRuntimeConfig)),
|
|
84
85
|
};
|
|
85
86
|
reports.push((0, track_1.trackGA4)("function_deploy_group", fnDeployGroupEvent));
|
|
86
87
|
const avgTime = totalTime / (totalSuccesses + totalErrors);
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.detectFromOutputPath = exports.detectFromPort = exports.detectFromYaml = exports.yamlToBuild = exports.getFunctionDiscoveryTimeout = void 0;
|
|
4
4
|
const node_fetch_1 = require("node-fetch");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const yaml = require("yaml");
|
|
8
|
-
const util_1 = require("util");
|
|
9
8
|
const logger_1 = require("../../../../logger");
|
|
10
9
|
const api = require("../../.../../../../api");
|
|
11
10
|
const v1alpha1 = require("./v1alpha1");
|
|
12
11
|
const error_1 = require("../../../../error");
|
|
13
|
-
exports.readFileAsync = (0, util_1.promisify)(fs.readFile);
|
|
14
12
|
const TIMEOUT_OVERRIDE_ENV_VAR = "FUNCTIONS_DISCOVERY_TIMEOUT";
|
|
15
13
|
function getFunctionDiscoveryTimeout() {
|
|
16
14
|
return +(process.env[TIMEOUT_OVERRIDE_ENV_VAR] || 0) * 1000;
|
|
@@ -34,7 +32,7 @@ exports.yamlToBuild = yamlToBuild;
|
|
|
34
32
|
async function detectFromYaml(directory, project, runtime) {
|
|
35
33
|
let text;
|
|
36
34
|
try {
|
|
37
|
-
text = await
|
|
35
|
+
text = await fs.promises.readFile(path.join(directory, "functions.yaml"), "utf8");
|
|
38
36
|
}
|
|
39
37
|
catch (err) {
|
|
40
38
|
if (err.code === "ENOENT") {
|
|
@@ -96,3 +94,54 @@ async function detectFromPort(port, project, runtime, initialDelay = 0, timeout
|
|
|
96
94
|
return yamlToBuild(parsed, project, api.functionsDefaultRegion(), runtime);
|
|
97
95
|
}
|
|
98
96
|
exports.detectFromPort = detectFromPort;
|
|
97
|
+
async function detectFromOutputPath(childProcess, manifestPath, project, runtime, timeout = 10000) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
var _a;
|
|
100
|
+
let stderrBuffer = "";
|
|
101
|
+
let resolved = false;
|
|
102
|
+
const discoveryTimeout = getFunctionDiscoveryTimeout() || timeout;
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
if (!resolved) {
|
|
105
|
+
resolved = true;
|
|
106
|
+
reject(new error_1.FirebaseError(`User code failed to load. Cannot determine backend specification. Timeout after ${discoveryTimeout}ms`));
|
|
107
|
+
}
|
|
108
|
+
}, discoveryTimeout);
|
|
109
|
+
(_a = childProcess.stderr) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
|
|
110
|
+
stderrBuffer += chunk.toString();
|
|
111
|
+
});
|
|
112
|
+
childProcess.on("exit", async (code) => {
|
|
113
|
+
var _a;
|
|
114
|
+
if (!resolved) {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
resolved = true;
|
|
117
|
+
if (code !== 0 && code !== null) {
|
|
118
|
+
const errorMessage = (_a = stderrBuffer.trim()) !== null && _a !== void 0 ? _a : `Discovery process exited with code ${code}`;
|
|
119
|
+
reject(new error_1.FirebaseError(`User code failed to load. Cannot determine backend specification.\n${errorMessage}`));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
try {
|
|
123
|
+
const manifestContent = await fs.promises.readFile(manifestPath, "utf8");
|
|
124
|
+
const parsed = yaml.parse(manifestContent);
|
|
125
|
+
resolve(yamlToBuild(parsed, project, api.functionsDefaultRegion(), runtime));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err.code === "ENOENT") {
|
|
129
|
+
reject(new error_1.FirebaseError(`Discovery process completed but no function manifest was found at ${manifestPath}`));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
reject(new error_1.FirebaseError(`Failed to read or parse manifest file: ${err.message}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
childProcess.on("error", (err) => {
|
|
139
|
+
if (!resolved) {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolved = true;
|
|
142
|
+
reject(new error_1.FirebaseError(`Discovery process failed: ${err.message}`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
exports.detectFromOutputPath = detectFromOutputPath;
|