firebase-tools 15.11.0 → 15.12.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.
Files changed (36) hide show
  1. package/lib/agentSkills.js +70 -0
  2. package/lib/api.js +3 -1
  3. package/lib/apphosting/backend.js +22 -3
  4. package/lib/bin/mcp.js +5 -1
  5. package/lib/commands/apphosting-backends-create.js +19 -2
  6. package/lib/commands/apphosting-backends-list.js +21 -5
  7. package/lib/commands/functions-delete.js +1 -0
  8. package/lib/commands/functions-export.js +40 -0
  9. package/lib/commands/index.js +3 -0
  10. package/lib/commands/init.js +1 -0
  11. package/lib/deploy/apphosting/deploy.js +11 -6
  12. package/lib/deploy/apphosting/prepare.js +21 -1
  13. package/lib/deploy/apphosting/release.js +2 -5
  14. package/lib/deploy/apphosting/util.js +45 -2
  15. package/lib/deploy/functions/prepare.js +4 -1
  16. package/lib/deploy/functions/release/fabricator.js +4 -3
  17. package/lib/deploy/functions/release/index.js +5 -0
  18. package/lib/deploy/functions/services/ailogic.js +68 -0
  19. package/lib/deploy/functions/services/index.js +4 -0
  20. package/lib/emulator/downloadableEmulatorInfo.json +24 -24
  21. package/lib/emulator/storage/rules/manager.js +10 -3
  22. package/lib/emulator/storage/rules/runtime.js +9 -7
  23. package/lib/experiments.js +22 -0
  24. package/lib/firebase_studio/migrate.js +30 -61
  25. package/lib/functions/iac/export.js +36 -0
  26. package/lib/functions/iac/terraform.js +146 -0
  27. package/lib/gcp/ailogic.js +108 -0
  28. package/lib/gcp/cloudfunctionsv2.js +24 -0
  29. package/lib/init/features/agentSkills.js +26 -0
  30. package/lib/init/features/dataconnect/sdk.js +26 -12
  31. package/lib/init/features/index.js +4 -1
  32. package/lib/init/index.js +6 -0
  33. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  34. package/lib/utils.js +8 -0
  35. package/package.json +5 -3
  36. package/standalone/package.json +1 -1
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptForAgentSkills = promptForAgentSkills;
4
+ exports.installAgentSkills = installAgentSkills;
5
+ const child_process_1 = require("child_process");
6
+ const utils = require("./utils");
7
+ const prompt = require("./prompt");
8
+ const error_1 = require("./error");
9
+ async function promptForAgentSkills() {
10
+ return prompt.confirm({
11
+ message: "Would you like to install agent skills for Firebase?",
12
+ default: true,
13
+ });
14
+ }
15
+ async function installAgentSkills(options) {
16
+ if (!utils.commandExistsSync("npx")) {
17
+ return;
18
+ }
19
+ const args = [
20
+ "-y",
21
+ "skills",
22
+ "add",
23
+ "firebase/agent-skills",
24
+ "--skill",
25
+ "*",
26
+ "-y",
27
+ ];
28
+ if (options.agentName) {
29
+ args.push("-a", options.agentName);
30
+ }
31
+ if (options.global) {
32
+ args.push("-g");
33
+ }
34
+ if (options.background) {
35
+ utils.logBullet("Installing Agent skills in the background...");
36
+ try {
37
+ const child = (0, child_process_1.spawn)("npx", args, {
38
+ cwd: options.cwd,
39
+ stdio: "ignore",
40
+ detached: true,
41
+ shell: process.platform === "win32",
42
+ });
43
+ child.unref();
44
+ utils.logSuccess("Agent skills installation started");
45
+ }
46
+ catch (err) {
47
+ utils.logWarning(`Could not start Agent skills installation: ${(0, error_1.getErrMsg)(err)}`);
48
+ }
49
+ }
50
+ else {
51
+ utils.logBullet("Adding Agent skills...");
52
+ try {
53
+ const result = (0, child_process_1.spawnSync)("npx", args, {
54
+ cwd: options.cwd,
55
+ stdio: "ignore",
56
+ shell: process.platform === "win32",
57
+ });
58
+ if (result.error) {
59
+ throw result.error;
60
+ }
61
+ if (result.status !== 0) {
62
+ throw new Error(`npx skills add exited with code ${result.status}`);
63
+ }
64
+ utils.logSuccess("Added Agent skills");
65
+ }
66
+ catch (err) {
67
+ utils.logWarning(`Could not add Agent skills: ${(0, error_1.getErrMsg)(err)}`);
68
+ }
69
+ }
70
+ }
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.developerKnowledgeOrigin = exports.cloudTestingOrigin = 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.studioApiOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
4
+ exports.developerKnowledgeOrigin = exports.cloudTestingOrigin = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = 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 = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
5
5
  exports.getScopes = getScopes;
6
6
  exports.setScopes = setScopes;
7
7
  const constants_1 = require("./emulator/constants");
@@ -146,6 +146,8 @@ const cloudSQLAdminOrigin = () => utils.envOverride("CLOUD_SQL_URL", "https://sq
146
146
  exports.cloudSQLAdminOrigin = cloudSQLAdminOrigin;
147
147
  const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatform.googleapis.com");
148
148
  exports.vertexAIOrigin = vertexAIOrigin;
149
+ const aiLogicProxyOrigin = () => utils.envOverride("AI_LOGIC_PROXY_URL", "https://firebasevertexai.googleapis.com");
150
+ exports.aiLogicProxyOrigin = aiLogicProxyOrigin;
149
151
  const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
150
152
  exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
151
153
  const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
@@ -34,6 +34,8 @@ const ora = require("ora");
34
34
  const node_fetch_1 = require("node-fetch");
35
35
  const rollout_1 = require("./rollout");
36
36
  const fuzzy = require("fuzzy");
37
+ const experiments_1 = require("../experiments");
38
+ const DEFAULT_RUNTIME = "nodejs";
37
39
  const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
38
40
  const apphostingPollerOptions = {
39
41
  apiOrigin: (0, api_1.apphostingOrigin)(),
@@ -64,7 +66,7 @@ async function awaitTlsReady(url) {
64
66
  }
65
67
  } while (!ready);
66
68
  }
67
- async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir) {
69
+ async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir, runtime, automaticBaseImageUpdatesDisabled) {
68
70
  await ensureRequiredApisEnabled(projectId);
69
71
  await ensureAppHostingComputeServiceAccount(projectId, serviceAccount ? serviceAccount : null);
70
72
  let location = primaryRegion;
@@ -97,12 +99,27 @@ async function doSetup(projectId, nonInteractive, webAppName, backendId, service
97
99
  if (!location || !backendId) {
98
100
  throw new error_1.FirebaseError("Internal error: location or backendId is not defined.");
99
101
  }
102
+ if (runtime === undefined && (0, experiments_1.isEnabled)("abiu")) {
103
+ if (nonInteractive) {
104
+ runtime = DEFAULT_RUNTIME;
105
+ }
106
+ else {
107
+ runtime = await (0, prompt_1.select)({
108
+ message: "Which runtime do you want to use?",
109
+ choices: [
110
+ { name: "Node.js (default)", value: DEFAULT_RUNTIME },
111
+ { name: "Node.js 22", value: "nodejs22" },
112
+ ],
113
+ default: DEFAULT_RUNTIME,
114
+ });
115
+ }
116
+ }
100
117
  const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName ? webAppName : null, backendId);
101
118
  if (!webApp) {
102
119
  (0, utils_1.logWarning)(`Firebase web app not set`);
103
120
  }
104
121
  const createBackendSpinner = ora("Creating your new backend...").start();
105
- const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir);
122
+ const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir, runtime, automaticBaseImageUpdatesDisabled);
106
123
  createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
107
124
  if (nonInteractive) {
108
125
  return;
@@ -227,7 +244,7 @@ async function promptNewBackendId(projectId, location) {
227
244
  function defaultComputeServiceAccountEmail(projectId) {
228
245
  return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
229
246
  }
230
- async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/") {
247
+ async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/", runtime, automaticBaseImageUpdatesDisabled) {
231
248
  const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
232
249
  const backendReqBody = {
233
250
  servingLocality: "GLOBAL_ACCESS",
@@ -240,6 +257,8 @@ async function createBackend(projectId, location, backendId, serviceAccount, rep
240
257
  labels: deploymentTool.labels(),
241
258
  serviceAccount: serviceAccount || defaultServiceAccount,
242
259
  appId: webAppId,
260
+ runtime: { value: runtime ?? "" },
261
+ automaticBaseImageUpdatesDisabled,
243
262
  };
244
263
  async function createBackendAndPoll() {
245
264
  const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
package/lib/bin/mcp.js CHANGED
@@ -3,6 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.mcp = mcp;
5
5
  const path_1 = require("path");
6
+ const promises_1 = require("fs/promises");
7
+ const os_1 = require("os");
6
8
  const util_1 = require("util");
7
9
  const logger_1 = require("../logger");
8
10
  const index_1 = require("../mcp/index");
@@ -82,7 +84,9 @@ async function mcp() {
82
84
  if (earlyExit)
83
85
  return;
84
86
  (0, env_1.setFirebaseMcp)(true);
85
- (0, logger_1.useFileLogger)();
87
+ const mcpLogDir = (0, path_1.join)((0, os_1.homedir)(), ".cache", "firebase");
88
+ await (0, promises_1.mkdir)(mcpLogDir, { recursive: true });
89
+ (0, logger_1.useFileLogger)((0, path_1.join)(mcpLogDir, "firebase-debug.log"));
86
90
  const activeFeatures = (values.only || "")
87
91
  .split(",")
88
92
  .map((f) => f.trim())
@@ -7,6 +7,7 @@ const projectUtils_1 = require("../projectUtils");
7
7
  const requireAuth_1 = require("../requireAuth");
8
8
  const backend_1 = require("../apphosting/backend");
9
9
  const apphosting_1 = require("../gcp/apphosting");
10
+ const experiments = require("../experiments");
10
11
  const firedata_1 = require("../gcp/firedata");
11
12
  const requireTosAcceptance_1 = require("../requireTosAcceptance");
12
13
  exports.command = new command_1.Command("apphosting:backends:create")
@@ -15,7 +16,15 @@ exports.command = new command_1.Command("apphosting:backends:create")
15
16
  .option("--backend <backend>", "specify the name of the new backend. Required with --non-interactive.")
16
17
  .option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
17
18
  .option("--primary-region <primaryRegion>", "specify the primary region for the backend. Required with --non-interactive.")
18
- .option("--root-dir <rootDir>", "specify the root directory for the backend.")
19
+ .option("--root-dir <rootDir>", "specify the root directory for the backend.");
20
+ const abiuEnabled = experiments.isEnabled("abiu");
21
+ if (abiuEnabled) {
22
+ exports.command
23
+ .option("--runtime [runtime]", "specify the runtime for the backend (e.g., nodejs, nodejs22)")
24
+ .option("--automatic-base-image-updates", "enable automatic base image updates")
25
+ .option("--no-automatic-base-image-updates", "disable automatic base image updates");
26
+ }
27
+ exports.command
19
28
  .before(requireAuth_1.requireAuth)
20
29
  .before(apphosting_1.ensureApiEnabled)
21
30
  .before((0, requireTosAcceptance_1.requireTosAcceptance)(firedata_1.APPHOSTING_TOS_ID))
@@ -24,5 +33,13 @@ exports.command = new command_1.Command("apphosting:backends:create")
24
33
  if (options.nonInteractive && (options.backend == null || options.primaryRegion == null)) {
25
34
  throw new error_1.FirebaseError(`--non-interactive option requires --backend and --primary-region`);
26
35
  }
27
- await (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir);
36
+ const abiuAllowed = experiments.isEnabled("abiu");
37
+ if (!abiuAllowed && (options.runtime || options.automaticBaseImageUpdates !== undefined)) {
38
+ throw new error_1.FirebaseError("The --runtime and --automatic-base-image-updates flags are only available when the 'abiu' experiment is enabled. To enable it, run 'firebase experiments:enable abiu'.");
39
+ }
40
+ const runtime = abiuAllowed && typeof options.runtime === "string" ? options.runtime : undefined;
41
+ const automaticBaseImageUpdatesDisabled = abiuAllowed
42
+ ? options.automaticBaseImageUpdates === false
43
+ : undefined;
44
+ return (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir, runtime, automaticBaseImageUpdatesDisabled);
28
45
  });
@@ -9,8 +9,8 @@ const logger_1 = require("../logger");
9
9
  const projectUtils_1 = require("../projectUtils");
10
10
  const requireAuth_1 = require("../requireAuth");
11
11
  const apphosting = require("../gcp/apphosting");
12
+ const experiments_1 = require("../experiments");
12
13
  const Table = require("cli-table3");
13
- const TABLE_HEAD = ["Backend", "Repository", "URL", "Primary Region", "Updated Date"];
14
14
  exports.command = new command_1.Command("apphosting:backends:list")
15
15
  .description("list Firebase App Hosting backends")
16
16
  .before(requireAuth_1.requireAuth)
@@ -29,19 +29,35 @@ exports.command = new command_1.Command("apphosting:backends:list")
29
29
  return backends;
30
30
  });
31
31
  function printBackendsTable(backends) {
32
+ const abiuEnabled = (0, experiments_1.isEnabled)("abiu");
33
+ const head = ["Backend", "Repository", "URL", "Primary Region"];
34
+ if (abiuEnabled) {
35
+ head.push("ABIU");
36
+ head.push("Runtime");
37
+ }
38
+ head.push("Updated Date");
32
39
  const table = new Table({
33
- head: TABLE_HEAD,
40
+ head: head,
34
41
  style: { head: ["green"] },
35
42
  });
36
43
  for (const backend of backends) {
37
44
  const { location, id } = apphosting.parseBackendName(backend.name);
38
- table.push([
45
+ const row = [
39
46
  id,
40
47
  backend.codebase?.repository?.split("/").pop() ?? "",
41
48
  backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
42
49
  location,
43
- (0, utils_1.datetimeString)(new Date(backend.updateTime)),
44
- ]);
50
+ ];
51
+ if (abiuEnabled) {
52
+ let abiuStatus = "N/A";
53
+ if (backend.automaticBaseImageUpdatesDisabled !== undefined) {
54
+ abiuStatus = backend.automaticBaseImageUpdatesDisabled ? "Disabled" : "Enabled";
55
+ }
56
+ row.push(abiuStatus);
57
+ row.push(backend.runtime?.value ?? "N/A");
58
+ }
59
+ row.push((0, utils_1.datetimeString)(new Date(backend.updateTime)));
60
+ table.push(row);
45
61
  }
46
62
  logger_1.logger.info(table.toString());
47
63
  }
@@ -75,6 +75,7 @@ exports.command = new command_1.Command("functions:delete [filters...]")
75
75
  try {
76
76
  const fab = new fabricator.Fabricator({
77
77
  functionExecutor,
78
+ runFunctionExecutor: functionExecutor,
78
79
  appEngineLocation,
79
80
  executor: new executor.QueueExecutor({}),
80
81
  sources: {},
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const error_1 = require("../error");
6
+ const iac = require("../functions/iac/export");
7
+ const projectConfig_1 = require("../functions/projectConfig");
8
+ const clc = require("colorette");
9
+ const logger_1 = require("../logger");
10
+ const EXPORTERS = {
11
+ internal: iac.getInternalIac,
12
+ };
13
+ exports.command = new command_1.Command("functions:export")
14
+ .description("export Cloud Functions code and configuration")
15
+ .option("--format <format>", `Format of the output. Can be ${Object.keys(EXPORTERS).join(", ")}.`)
16
+ .option("--codebase <codebase>", "Optional codebase to export. If not specified, exports the default or only codebase.")
17
+ .action(async (options) => {
18
+ if (!options.format || !Object.keys(EXPORTERS).includes(options.format)) {
19
+ throw new error_1.FirebaseError(`Must specify --format as ${Object.keys(EXPORTERS).join(", ")}.`);
20
+ }
21
+ const config = (0, projectConfig_1.normalizeAndValidate)(options.config?.src?.functions);
22
+ let codebaseConfig;
23
+ if (options.codebase) {
24
+ codebaseConfig = (0, projectConfig_1.configForCodebase)(config, options.codebase);
25
+ }
26
+ else if (config.length === 1) {
27
+ codebaseConfig = config[0];
28
+ }
29
+ else {
30
+ codebaseConfig = (0, projectConfig_1.configForCodebase)(config, "default");
31
+ }
32
+ if (!codebaseConfig.source) {
33
+ throw new error_1.FirebaseError("Codebase does not have a local source directory.");
34
+ }
35
+ const manifest = await EXPORTERS[options.format](options, codebaseConfig);
36
+ for (const [file, contents] of Object.entries(manifest)) {
37
+ logger_1.logger.info(`Manifest file: ${clc.bold(file)}`);
38
+ logger_1.logger.info(contents);
39
+ }
40
+ });
@@ -141,6 +141,9 @@ function load(client) {
141
141
  client.functions.config.set = loadCommand("functions-config-set");
142
142
  client.functions.config.unset = loadCommand("functions-config-unset");
143
143
  client.functions.delete = loadCommand("functions-delete");
144
+ if (experiments.isEnabled("functionsiac")) {
145
+ client.functions.export = loadCommand("functions-export");
146
+ }
144
147
  client.functions.log = loadCommand("functions-log");
145
148
  client.functions.shell = loadCommand("functions-shell");
146
149
  client.functions.list = loadCommand("functions-list");
@@ -228,6 +228,7 @@ async function initAction(feature, options) {
228
228
  if (setup.features.includes("dataconnect") && setup.features.includes("dataconnect:sdk")) {
229
229
  setup.features = setup.features.filter((f) => f !== "dataconnect:sdk");
230
230
  }
231
+ setup.features.push("agentSkills");
231
232
  await (0, init_1.init)(setup, config, options);
232
233
  await postInitSaves(setup, config);
233
234
  if (setup.instructions.length) {
@@ -8,7 +8,8 @@ const gcs = require("../../gcp/storage");
8
8
  const getProjectNumber_1 = require("../../getProjectNumber");
9
9
  const projectUtils_1 = require("../../projectUtils");
10
10
  const utils_1 = require("../../utils");
11
- const util_1 = require("./util");
11
+ const util = require("./util");
12
+ const experiments = require("../../experiments");
12
13
  async function default_1(context, options) {
13
14
  if (Object.entries(context.backendConfigs).length === 0) {
14
15
  return;
@@ -52,24 +53,28 @@ async function default_1(context, options) {
52
53
  await Promise.all(Object.values(context.backendConfigs).map(async (cfg) => {
53
54
  const rootDir = options.projectRoot ?? process.cwd();
54
55
  let builtAppDir;
55
- if (cfg.localBuild) {
56
+ const isLocalBuild = !!cfg.localBuild;
57
+ if (isLocalBuild) {
58
+ experiments.assertEnabled("apphostinglocalbuilds", "App Hosting local builds");
56
59
  builtAppDir = context.backendLocalBuilds[cfg.backendId].buildDir;
57
60
  if (!builtAppDir) {
58
61
  throw new error_1.FirebaseError(`No local build dir found for ${cfg.backendId}`);
59
62
  }
60
63
  }
61
- const zippedSourcePath = await (0, util_1.createArchive)(cfg, rootDir, builtAppDir);
62
- (0, utils_1.logLabeledBullet)("apphosting", `Zipped ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}`);
64
+ const zippedSourcePath = isLocalBuild
65
+ ? await util.createLocalBuildTarArchive(cfg, rootDir, builtAppDir)
66
+ : await util.createSourceDeployArchive(cfg, rootDir);
67
+ (0, utils_1.logLabeledBullet)("apphosting", `Zipped ${isLocalBuild ? "built app" : "source"} for backend ${cfg.backendId}`);
63
68
  const backendLocation = context.backendLocations[cfg.backendId];
64
69
  if (!backendLocation) {
65
70
  throw new error_1.FirebaseError(`Failed to find location for backend ${cfg.backendId}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
66
71
  }
67
- (0, utils_1.logLabeledBullet)("apphosting", `Uploading ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}...`);
72
+ (0, utils_1.logLabeledBullet)("apphosting", `Uploading ${isLocalBuild ? "built app" : "source"} for backend ${cfg.backendId}...`);
68
73
  const bucketName = bucketsPerLocation[backendLocation];
69
74
  const { bucket, object } = await gcs.uploadObject({
70
75
  file: zippedSourcePath,
71
76
  stream: fs.createReadStream(zippedSourcePath),
72
- }, bucketName);
77
+ }, bucketName, isLocalBuild ? gcs.ContentType.TAR : gcs.ContentType.ZIP);
73
78
  (0, utils_1.logLabeledBullet)("apphosting", `Uploaded at gs://${bucket}/${object}`);
74
79
  context.backendStorageUris[cfg.backendId] =
75
80
  `gs://${bucketName}/${path.basename(zippedSourcePath)}`;
@@ -6,11 +6,15 @@ const path = require("path");
6
6
  const backend_1 = require("../../apphosting/backend");
7
7
  const apphosting_1 = require("../../gcp/apphosting");
8
8
  const devConnect_1 = require("../../gcp/devConnect");
9
+ const resourceManager_1 = require("../../gcp/resourceManager");
9
10
  const projectUtils_1 = require("../../projectUtils");
11
+ const getProjectNumber_1 = require("../../getProjectNumber");
10
12
  const prompt_1 = require("../../prompt");
11
13
  const utils_1 = require("../../utils");
12
14
  const localbuilds_1 = require("../../apphosting/localbuilds");
13
15
  const error_1 = require("../../error");
16
+ const experiments = require("../../experiments");
17
+ const logger_1 = require("../../logger");
14
18
  async function default_1(context, options) {
15
19
  const projectId = (0, projectUtils_1.needProjectId)(options);
16
20
  await (0, apphosting_1.ensureApiEnabled)(options);
@@ -21,6 +25,10 @@ async function default_1(context, options) {
21
25
  context.backendStorageUris = {};
22
26
  context.backendLocalBuilds = {};
23
27
  const configs = getBackendConfigs(options);
28
+ if (configs.some((cfg) => cfg.localBuild) && experiments.isEnabled("apphostinglocalbuilds")) {
29
+ const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(options);
30
+ await ensureAppHostingServiceAgentRoles(projectId, projectNumber);
31
+ }
24
32
  const { backends } = await (0, apphosting_1.listBackends)(projectId, "-");
25
33
  const foundBackends = [];
26
34
  const notFoundBackends = [];
@@ -112,6 +120,7 @@ async function default_1(context, options) {
112
120
  if (!cfg.localBuild) {
113
121
  continue;
114
122
  }
123
+ experiments.assertEnabled("apphostinglocalbuilds", "locally build App Hosting backends");
115
124
  (0, utils_1.logLabeledBullet)("apphosting", `Starting local build for backend ${cfg.backendId}`);
116
125
  try {
117
126
  const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(options.projectRoot || "./", "nextjs");
@@ -125,7 +134,8 @@ async function default_1(context, options) {
125
134
  };
126
135
  }
127
136
  catch (e) {
128
- throw new error_1.FirebaseError(`Local Build for backend ${cfg.backendId} failed: ${e}`);
137
+ const errorMsg = e instanceof Error ? e.message : String(e);
138
+ throw new error_1.FirebaseError(`Local Build for backend ${cfg.backendId} failed: ${errorMsg}`);
129
139
  }
130
140
  }
131
141
  }
@@ -157,3 +167,13 @@ function getBackendConfigs(options) {
157
167
  }
158
168
  return backendConfigs.filter((cfg) => backendIds.includes(cfg.backendId));
159
169
  }
170
+ async function ensureAppHostingServiceAgentRoles(projectId, projectNumber) {
171
+ const p4saEmail = (0, apphosting_1.serviceAgentEmail)(projectNumber);
172
+ try {
173
+ await (0, resourceManager_1.addServiceAccountToRoles)(projectId, p4saEmail, ["roles/storage.objectViewer"], true);
174
+ }
175
+ catch (err) {
176
+ logger_1.logger.debug(`Failed to grant storage.objectViewer to ${p4saEmail}: ${String(err)}`);
177
+ (0, utils_1.logLabeledWarning)("apphosting", `Unable to verify App Hosting service agent permissions for ${p4saEmail}. If you encounter a PERMISSION_DENIED error during rollout, please ensure the service agent has the "Storage Object Viewer" role.`);
178
+ }
179
+ }
@@ -15,11 +15,6 @@ async function default_1(context, options) {
15
15
  (0, utils_1.logLabeledWarning)("apphosting", `Failed to find metadata for backend(s) ${backendIds.join(", ")}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
16
16
  backendIds = backendIds.filter((id) => !missingBackends.includes(id));
17
17
  }
18
- const localBuildBackends = backendIds.filter((id) => context.backendLocalBuilds[id]);
19
- if (localBuildBackends.length > 0) {
20
- (0, utils_1.logLabeledWarning)("apphosting", `Skipping backend(s) ${localBuildBackends.join(", ")}. Local Builds are not supported yet.`);
21
- backendIds = backendIds.filter((id) => !localBuildBackends.includes(id));
22
- }
23
18
  if (backendIds.length === 0) {
24
19
  return;
25
20
  }
@@ -29,10 +24,12 @@ async function default_1(context, options) {
29
24
  backendId,
30
25
  location: context.backendLocations[backendId],
31
26
  buildInput: {
27
+ config: context.backendLocalBuilds[backendId]?.buildConfig,
32
28
  source: {
33
29
  archive: {
34
30
  userStorageUri: context.backendStorageUris[backendId],
35
31
  rootDirectory: context.backendConfigs[backendId].rootDir,
32
+ locallyBuiltSource: !!context.backendLocalBuilds[backendId],
36
33
  },
37
34
  },
38
35
  },
@@ -1,13 +1,56 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createArchive = createArchive;
3
+ exports.createLocalBuildTarArchive = createLocalBuildTarArchive;
4
+ exports.createSourceDeployArchive = createSourceDeployArchive;
4
5
  const archiver = require("archiver");
5
6
  const fs = require("fs");
6
7
  const path = require("path");
8
+ const tar = require("tar");
7
9
  const tmp = require("tmp");
8
10
  const error_1 = require("../../error");
9
11
  const fsAsync = require("../../fsAsync");
10
- async function createArchive(config, rootDir, targetSubDir) {
12
+ const config_1 = require("../../apphosting/config");
13
+ async function createLocalBuildTarArchive(config, rootDir, targetSubDir) {
14
+ const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".tar.gz" }).name;
15
+ const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir;
16
+ const ignore = ["firebase-debug.log", "firebase-debug.*.log", ".git"];
17
+ const rdrFiles = await fsAsync.readdirRecursive({
18
+ path: targetDir,
19
+ ignore: ignore,
20
+ isGitIgnore: true,
21
+ });
22
+ const allFiles = rdrFiles.map((rdrf) => path.relative(rootDir, rdrf.name));
23
+ if (targetSubDir) {
24
+ const defaultFiles = fs.readdirSync(rootDir).filter((file) => {
25
+ return config_1.APPHOSTING_YAML_FILE_REGEX.test(file);
26
+ });
27
+ for (const file of defaultFiles) {
28
+ if (!allFiles.includes(file)) {
29
+ allFiles.push(file);
30
+ }
31
+ }
32
+ }
33
+ try {
34
+ fs.statSync(rootDir);
35
+ }
36
+ catch (err) {
37
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
38
+ throw new error_1.FirebaseError(`Could not read directory "${rootDir}"`);
39
+ }
40
+ throw err;
41
+ }
42
+ if (!allFiles.length) {
43
+ throw new error_1.FirebaseError(`Cannot create a tar archive with 0 files from directory "${rootDir}"`);
44
+ }
45
+ await tar.create({
46
+ gzip: true,
47
+ file: tmpFile,
48
+ cwd: rootDir,
49
+ portable: true,
50
+ }, allFiles);
51
+ return tmpFile;
52
+ }
53
+ async function createSourceDeployArchive(config, rootDir, targetSubDir) {
11
54
  const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".zip" }).name;
12
55
  const fileStream = fs.createWriteStream(tmpFile, {
13
56
  flags: "w",
@@ -13,6 +13,7 @@ const clc = require("colorette");
13
13
  const proto = require("../../gcp/proto");
14
14
  const backend = require("./backend");
15
15
  const build = require("./build");
16
+ const experiments = require("../../experiments");
16
17
  const ensureApiEnabled = require("../../ensureApiEnabled");
17
18
  const functionsConfig = require("../../functionsConfig");
18
19
  const functionsEnv = require("../../functions/env");
@@ -322,7 +323,9 @@ async function loadCodebases(config, options, firebaseConfig, runtimeConfig, fil
322
323
  }
323
324
  const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext);
324
325
  logger_1.logger.debug(`Validating ${runtimeDelegate.language} source`);
325
- supported.guardVersionSupport(runtimeDelegate.runtime);
326
+ if (!experiments.isEnabled("bypassfunctionsdeprecationcheck")) {
327
+ supported.guardVersionSupport(runtimeDelegate.runtime);
328
+ }
326
329
  await runtimeDelegate.validate();
327
330
  logger_1.logger.debug(`Building ${runtimeDelegate.language} source`);
328
331
  await runtimeDelegate.build();
@@ -57,6 +57,7 @@ class Fabricator {
57
57
  constructor(args) {
58
58
  this.executor = args.executor;
59
59
  this.functionExecutor = args.functionExecutor;
60
+ this.runFunctionExecutor = args.runFunctionExecutor;
60
61
  this.sources = args.sources;
61
62
  this.appEngineLocation = args.appEngineLocation;
62
63
  this.projectNumber = args.projectNumber;
@@ -521,7 +522,7 @@ class Fabricator {
521
522
  generation: storageSource.generation ? String(storageSource.generation) : undefined,
522
523
  },
523
524
  };
524
- await this.executor
525
+ await this.runFunctionExecutor
525
526
  .run(async () => {
526
527
  const op = await runV2.createService(endpoint.project, endpoint.region, endpoint.id, service);
527
528
  endpoint.uri = op.uri;
@@ -546,7 +547,7 @@ class Fabricator {
546
547
  generation: storageSource.generation ? String(storageSource.generation) : undefined,
547
548
  },
548
549
  };
549
- await this.executor
550
+ await this.runFunctionExecutor
550
551
  .run(async () => {
551
552
  const op = await runV2.updateService(service);
552
553
  endpoint.uri = op.uri;
@@ -556,7 +557,7 @@ class Fabricator {
556
557
  await this.setInvoker(endpoint);
557
558
  }
558
559
  async deleteRunFunction(endpoint) {
559
- await this.executor
560
+ await this.runFunctionExecutor
560
561
  .run(async () => {
561
562
  try {
562
563
  await runV2.deleteService(endpoint.project, endpoint.region, endpoint.id);
@@ -70,9 +70,14 @@ async function release(context, options, payload) {
70
70
  concurrency: 40,
71
71
  maxBackoff: 100000,
72
72
  };
73
+ const runThrottlerOptions = {
74
+ ...throttlerOptions,
75
+ concurrency: 2,
76
+ };
73
77
  const projectNumber = options.projectNumber || (await (0, getProjectNumber_1.getProjectNumber)(context.projectId));
74
78
  const fab = new fabricator.Fabricator({
75
79
  functionExecutor: new executor.QueueExecutor(throttlerOptions),
80
+ runFunctionExecutor: new executor.QueueExecutor(runThrottlerOptions),
76
81
  executor: new executor.QueueExecutor(throttlerOptions),
77
82
  sources: context.sources,
78
83
  appEngineLocation: (0, functionsConfig_1.getAppEngineLocation)(context.firebaseConfig),