firebase-tools 11.6.0 → 11.8.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.
Files changed (52) hide show
  1. package/lib/auth.js +1 -1
  2. package/lib/commands/crashlytics-mappingfile-generateid.js +26 -0
  3. package/lib/commands/crashlytics-mappingfile-upload.js +46 -0
  4. package/lib/commands/crashlytics-symbols-upload.js +18 -87
  5. package/lib/commands/functions-delete.js +2 -0
  6. package/lib/commands/functions-secrets-get.js +2 -0
  7. package/lib/commands/index.js +3 -0
  8. package/lib/crashlytics/buildToolsJarHelper.js +51 -0
  9. package/lib/deploy/functions/backend.js +4 -4
  10. package/lib/deploy/functions/build.js +98 -17
  11. package/lib/deploy/functions/cache/applyHash.js +29 -0
  12. package/lib/deploy/functions/cache/hash.js +30 -0
  13. package/lib/deploy/functions/cel.js +249 -0
  14. package/lib/deploy/functions/checkIam.js +6 -5
  15. package/lib/deploy/functions/functionsDeployHelper.js +12 -1
  16. package/lib/deploy/functions/params.js +262 -105
  17. package/lib/deploy/functions/prepare.js +34 -4
  18. package/lib/deploy/functions/prepareFunctionsUpload.js +12 -4
  19. package/lib/deploy/functions/release/fabricator.js +39 -6
  20. package/lib/deploy/functions/release/index.js +2 -0
  21. package/lib/deploy/functions/release/planner.js +17 -0
  22. package/lib/deploy/functions/runtimes/discovery/index.js +1 -16
  23. package/lib/deploy/functions/runtimes/discovery/parsing.js +16 -0
  24. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +59 -131
  25. package/lib/deploy/functions/runtimes/node/parseTriggers.js +10 -1
  26. package/lib/emulator/constants.js +1 -1
  27. package/lib/emulator/controller.js +6 -11
  28. package/lib/emulator/extensionsEmulator.js +1 -0
  29. package/lib/emulator/functionsEmulator.js +18 -59
  30. package/lib/emulator/functionsEmulatorRuntime.js +12 -23
  31. package/lib/emulator/functionsRuntimeWorker.js +38 -7
  32. package/lib/emulator/storage/apis/firebase.js +145 -129
  33. package/lib/emulator/storage/apis/gcloud.js +102 -42
  34. package/lib/emulator/storage/files.js +25 -15
  35. package/lib/emulator/storage/metadata.js +86 -56
  36. package/lib/emulator/storage/multipart.js +2 -2
  37. package/lib/emulator/storage/rules/runtime.js +10 -2
  38. package/lib/emulator/storage/upload.js +45 -9
  39. package/lib/extensions/extensionsHelper.js +1 -1
  40. package/lib/functions/constants.js +14 -0
  41. package/lib/functions/env.js +9 -9
  42. package/lib/functions/secrets.js +8 -1
  43. package/lib/gcp/cloudfunctions.js +15 -18
  44. package/lib/gcp/cloudfunctionsv2.js +15 -18
  45. package/lib/gcp/cloudscheduler.js +32 -14
  46. package/lib/gcp/secretManager.js +15 -1
  47. package/lib/gcp/storage.js +15 -1
  48. package/lib/previews.js +1 -1
  49. package/lib/track.js +3 -0
  50. package/npm-shrinkwrap.json +563 -30
  51. package/package.json +7 -5
  52. package/templates/init/storage/storage.rules +1 -1
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveParams = exports.resolveBoolean = exports.resolveString = exports.resolveInt = void 0;
3
+ exports.resolveParams = exports.ParamValue = exports.isResourceInput = exports.isSelectInput = exports.isTextInput = exports.resolveBoolean = exports.resolveString = exports.resolveInt = void 0;
4
4
  const logger_1 = require("../../logger");
5
5
  const error_1 = require("../../error");
6
6
  const prompt_1 = require("../../prompt");
7
7
  const functional_1 = require("../../functional");
8
- function isCEL(expr) {
9
- return typeof expr === "string" && expr.includes("{{") && expr.includes("}}");
10
- }
8
+ const secretManager = require("../../gcp/secretManager");
9
+ const storage_1 = require("../../gcp/storage");
10
+ const cel_1 = require("./cel");
11
11
  function dependenciesCEL(expr) {
12
12
  const deps = [];
13
13
  const paramCapture = /{{ params\.(\w+) }}/g;
@@ -21,70 +21,76 @@ function resolveInt(from, paramValues) {
21
21
  if (typeof from === "number") {
22
22
  return from;
23
23
  }
24
- const match = /\A{{ params\.(\w+) }}\z/.exec(from);
25
- if (!match) {
26
- throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
27
- }
28
- const referencedParamValue = paramValues[match[1]];
29
- if (typeof referencedParamValue !== "number") {
30
- throw new error_1.FirebaseError("Referenced numeric parameter '" +
31
- match +
32
- "' resolved to non-number value " +
33
- referencedParamValue);
34
- }
35
- return referencedParamValue;
24
+ return (0, cel_1.resolveExpression)("number", from, paramValues);
36
25
  }
37
26
  exports.resolveInt = resolveInt;
38
27
  function resolveString(from, paramValues) {
39
- if (!isCEL(from)) {
40
- return from;
41
- }
42
28
  let output = from;
43
- const paramCapture = /{{ params\.(\w+) }}/g;
44
- let match;
45
- while ((match = paramCapture.exec(from)) != null) {
46
- const referencedParamValue = paramValues[match[1]];
47
- if (typeof referencedParamValue !== "string") {
48
- throw new error_1.FirebaseError("Referenced string parameter '" +
49
- match[1] +
50
- "' resolved to non-string value " +
51
- referencedParamValue);
52
- }
53
- output = output.replace(`{{ params.${match[1]} }}`, referencedParamValue);
29
+ const celCapture = /{{ .+? }}/g;
30
+ const subExprs = from.match(celCapture);
31
+ if (!subExprs || subExprs.length === 0) {
32
+ return output;
54
33
  }
55
- if (isCEL(output)) {
56
- throw new error_1.FirebaseError("CEL evaluation of non-identity expression '" + from + "' not yet supported");
34
+ for (const expr of subExprs) {
35
+ const resolved = (0, cel_1.resolveExpression)("string", expr, paramValues);
36
+ output = output.replace(expr, resolved);
57
37
  }
58
38
  return output;
59
39
  }
60
40
  exports.resolveString = resolveString;
61
41
  function resolveBoolean(from, paramValues) {
62
- if (typeof from === "string" && /{{ params\.(\w+) }}/.test(from)) {
63
- const match = /{{ params\.(\w+) }}/.exec(from);
64
- const referencedParamValue = paramValues[match[1]];
65
- if (typeof referencedParamValue !== "boolean") {
66
- throw new error_1.FirebaseError("Referenced boolean parameter '" +
67
- match +
68
- "' resolved to non-boolean value " +
69
- referencedParamValue);
70
- }
71
- return referencedParamValue;
72
- }
73
- else if (typeof from === "string") {
74
- throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
42
+ if (typeof from === "boolean") {
43
+ return from;
75
44
  }
76
- return from;
45
+ return (0, cel_1.resolveExpression)("boolean", from, paramValues);
77
46
  }
78
47
  exports.resolveBoolean = resolveBoolean;
48
+ function isTextInput(input) {
49
+ return {}.hasOwnProperty.call(input, "text");
50
+ }
51
+ exports.isTextInput = isTextInput;
52
+ function isSelectInput(input) {
53
+ return {}.hasOwnProperty.call(input, "select");
54
+ }
55
+ exports.isSelectInput = isSelectInput;
56
+ function isResourceInput(input) {
57
+ return {}.hasOwnProperty.call(input, "resource");
58
+ }
59
+ exports.isResourceInput = isResourceInput;
60
+ class ParamValue {
61
+ constructor(rawValue, secret, types) {
62
+ this.rawValue = rawValue;
63
+ this.secret = secret;
64
+ this.legalString = types.string || false;
65
+ this.legalBoolean = types.boolean || false;
66
+ this.legalNumber = types.number || false;
67
+ }
68
+ toString() {
69
+ return this.rawValue;
70
+ }
71
+ asString() {
72
+ return this.rawValue;
73
+ }
74
+ asBoolean() {
75
+ return ["true", "y", "yes", "1"].includes(this.rawValue);
76
+ }
77
+ asNumber() {
78
+ return +this.rawValue;
79
+ }
80
+ }
81
+ exports.ParamValue = ParamValue;
79
82
  function resolveDefaultCEL(type, expr, currentEnv) {
80
83
  const deps = dependenciesCEL(expr);
81
84
  const allDepsFound = deps.every((dep) => !!currentEnv[dep]);
82
- if (!allDepsFound) {
85
+ const dependsOnSecret = deps.some((dep) => currentEnv[dep].secret);
86
+ if (!allDepsFound || dependsOnSecret) {
83
87
  throw new error_1.FirebaseError("Build specified parameter with un-resolvable default value " +
84
88
  expr +
85
89
  "; dependencies missing.");
86
90
  }
87
91
  switch (type) {
92
+ case "boolean":
93
+ return resolveBoolean(expr, currentEnv);
88
94
  case "string":
89
95
  return resolveString(expr, currentEnv);
90
96
  case "int":
@@ -100,93 +106,244 @@ function canSatisfyParam(param, value) {
100
106
  else if (param.type === "int") {
101
107
  return typeof value === "number" && Number.isInteger(value);
102
108
  }
109
+ else if (param.type === "boolean") {
110
+ return typeof value === "boolean";
111
+ }
112
+ else if (param.type === "secret") {
113
+ return false;
114
+ }
103
115
  (0, functional_1.assertExhaustive)(param);
104
116
  }
105
- async function resolveParams(params, projectId, userEnvs) {
117
+ async function resolveParams(params, projectId, userEnvs, nonInteractive) {
106
118
  const paramValues = {};
107
- const [provided, outstanding] = (0, functional_1.partition)(params, (param) => {
108
- return {}.hasOwnProperty.call(userEnvs, param.param);
119
+ const [resolved, outstanding] = (0, functional_1.partition)(params, (param) => {
120
+ return {}.hasOwnProperty.call(userEnvs, param.name);
109
121
  });
110
- for (const param of provided) {
111
- if (!canSatisfyParam(param, userEnvs[param.param])) {
112
- throw new error_1.FirebaseError("Parameter " +
113
- param.param +
114
- " resolved to value from dotenv files " +
115
- userEnvs[param.param] +
116
- " of wrong type");
117
- }
118
- paramValues[param.param] = userEnvs[param.param];
122
+ for (const param of resolved) {
123
+ paramValues[param.name] = userEnvs[param.name];
119
124
  }
120
- for (const param of outstanding) {
121
- let paramDefault = param.default;
122
- if (paramDefault && isCEL(paramDefault)) {
125
+ const [needSecret, needPrompt] = (0, functional_1.partition)(outstanding, (param) => param.type === "secret");
126
+ for (const param of needSecret) {
127
+ await handleSecret(param, projectId);
128
+ }
129
+ if (nonInteractive && needPrompt.length > 0) {
130
+ const envNames = outstanding.map((p) => p.name).join(", ");
131
+ throw new error_1.FirebaseError(`In non-interactive mode but have no value for the following environment variables: ${envNames}\n` +
132
+ "To continue, either run `firebase deploy` with an interactive terminal, or add values to a dotenv file. " +
133
+ "For information regarding how to use dotenv files, see https://firebase.google.com/docs/functions/config-env");
134
+ }
135
+ for (const param of needPrompt) {
136
+ const promptable = param;
137
+ let paramDefault = promptable.default;
138
+ if (paramDefault && (0, cel_1.isCelExpression)(paramDefault)) {
123
139
  paramDefault = resolveDefaultCEL(param.type, paramDefault, paramValues);
124
140
  }
125
141
  if (paramDefault && !canSatisfyParam(param, paramDefault)) {
126
- throw new error_1.FirebaseError("Parameter " + param.param + " has default value " + paramDefault + " of wrong type");
142
+ throw new error_1.FirebaseError("Parameter " + param.name + " has default value " + paramDefault + " of wrong type");
127
143
  }
128
- paramValues[param.param] = await promptParam(param, paramDefault);
144
+ paramValues[param.name] = await promptParam(param, projectId, paramDefault);
129
145
  }
130
146
  return paramValues;
131
147
  }
132
148
  exports.resolveParams = resolveParams;
133
- async function promptParam(param, resolvedDefault) {
149
+ async function handleSecret(secretParam, projectId) {
150
+ const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest");
151
+ if (!metadata.secret) {
152
+ throw new error_1.FirebaseError(`Your project currently doesn't have any secret named ${secretParam.name}. Create one by running firebase functions:secrets:set ${secretParam.name} command and try the deploy again.`);
153
+ }
154
+ else if (!metadata.secretVersion) {
155
+ throw new error_1.FirebaseError(`Cloud Secret Manager has no latest version of the secret defined by param ${secretParam.label || secretParam.name}`);
156
+ }
157
+ else if (metadata.secretVersion.state === "DESTROYED" ||
158
+ metadata.secretVersion.state === "DISABLED") {
159
+ throw new error_1.FirebaseError(`Cloud Secret Manager's latest version of secret '${secretParam.label || secretParam.name} is in illegal state ${metadata.secretVersion.state}`);
160
+ }
161
+ }
162
+ async function promptParam(param, projectId, resolvedDefault) {
134
163
  if (param.type === "string") {
135
- return promptStringParam(param, resolvedDefault);
164
+ const provided = await promptStringParam(param, projectId, resolvedDefault);
165
+ return new ParamValue(provided.toString(), false, { string: true });
136
166
  }
137
167
  else if (param.type === "int") {
138
- return promptIntParam(param, resolvedDefault);
168
+ const provided = await promptIntParam(param, resolvedDefault);
169
+ return new ParamValue(provided.toString(), false, { number: true });
170
+ }
171
+ else if (param.type === "boolean") {
172
+ const provided = await promptBooleanParam(param, resolvedDefault);
173
+ return new ParamValue(provided.toString(), false, { boolean: true });
174
+ }
175
+ else if (param.type === "secret") {
176
+ throw new error_1.FirebaseError(`Somehow ended up trying to interactively prompt for secret parameter ${param.name}, which should never happen.`);
139
177
  }
140
178
  (0, functional_1.assertExhaustive)(param);
141
179
  }
142
- async function promptStringParam(param, resolvedDefault) {
180
+ async function promptBooleanParam(param, resolvedDefault) {
143
181
  if (!param.input) {
144
- const defaultToText = { type: "text", text: {} };
182
+ const defaultToText = { text: {} };
145
183
  param.input = defaultToText;
146
184
  }
147
- switch (param.input.type) {
148
- case "select":
149
- throw new error_1.FirebaseError("Build specified string parameter " + param.param + " with unsupported input type 'select'");
150
- case "text":
151
- default:
152
- let prompt = `Enter a value for ${param.label || param.param}`;
153
- if (param.description) {
154
- prompt += ` \n(${param.description})`;
155
- }
156
- return await (0, prompt_1.promptOnce)({
157
- name: param.param,
158
- type: "input",
159
- default: resolvedDefault,
160
- message: prompt,
161
- });
185
+ const isTruthyInput = (res) => ["true", "y", "yes", "1"].includes(res.toLowerCase());
186
+ let prompt;
187
+ if (isSelectInput(param.input)) {
188
+ prompt = `Select a value for ${param.label || param.name}:`;
189
+ if (param.description) {
190
+ prompt += ` \n(${param.description})`;
191
+ }
192
+ prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
193
+ return promptSelect(prompt, param.input, resolvedDefault, isTruthyInput);
194
+ }
195
+ else if (isTextInput(param.input)) {
196
+ prompt = `Enter a boolean value for ${param.label || param.name}:`;
197
+ if (param.description) {
198
+ prompt += ` \n(${param.description})`;
199
+ }
200
+ return promptText(prompt, param.input, resolvedDefault, isTruthyInput);
201
+ }
202
+ else if (isResourceInput(param.input)) {
203
+ throw new error_1.FirebaseError("Boolean params cannot have Cloud Resource selector inputs");
204
+ }
205
+ else {
206
+ (0, functional_1.assertExhaustive)(param.input);
207
+ }
208
+ }
209
+ async function promptStringParam(param, projectId, resolvedDefault) {
210
+ if (!param.input) {
211
+ const defaultToText = { text: {} };
212
+ param.input = defaultToText;
213
+ }
214
+ let prompt;
215
+ if (isResourceInput(param.input)) {
216
+ prompt = `Select a value for ${param.label || param.name}:`;
217
+ if (param.description) {
218
+ prompt += ` \n(${param.description})`;
219
+ }
220
+ return promptResourceString(prompt, param.input, projectId, resolvedDefault);
221
+ }
222
+ else if (isSelectInput(param.input)) {
223
+ prompt = `Select a value for ${param.label || param.name}:`;
224
+ if (param.description) {
225
+ prompt += ` \n(${param.description})`;
226
+ }
227
+ prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
228
+ return promptSelect(prompt, param.input, resolvedDefault, (res) => res);
229
+ }
230
+ else if (isTextInput(param.input)) {
231
+ prompt = `Enter a string value for ${param.label || param.name}:`;
232
+ if (param.description) {
233
+ prompt += ` \n(${param.description})`;
234
+ }
235
+ return promptText(prompt, param.input, resolvedDefault, (res) => res);
236
+ }
237
+ else {
238
+ (0, functional_1.assertExhaustive)(param.input);
162
239
  }
163
240
  }
164
241
  async function promptIntParam(param, resolvedDefault) {
165
242
  if (!param.input) {
166
- const defaultToText = { type: "text", text: {} };
243
+ const defaultToText = { text: {} };
167
244
  param.input = defaultToText;
168
245
  }
169
- switch (param.input.type) {
170
- case "select":
171
- throw new error_1.FirebaseError("Build specified int parameter " + param.param + " with unsupported input type 'select'");
172
- case "text":
173
- default:
174
- let prompt = `Enter a value for ${param.label || param.param}`;
175
- if (param.description) {
176
- prompt += ` \n(${param.description})`;
246
+ let prompt;
247
+ if (isSelectInput(param.input)) {
248
+ prompt = `Select a value for ${param.label || param.name}:`;
249
+ if (param.description) {
250
+ prompt += ` \n(${param.description})`;
251
+ }
252
+ prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
253
+ return promptSelect(prompt, param.input, resolvedDefault, (res) => {
254
+ if (isNaN(+res)) {
255
+ return { message: `"${res}" could not be converted to a number.` };
256
+ }
257
+ if (res.includes(".")) {
258
+ return { message: `${res} is not an integer value.` };
177
259
  }
178
- let res;
179
- while (true) {
180
- res = await (0, prompt_1.promptOnce)({
181
- name: param.param,
182
- type: "number",
183
- default: resolvedDefault,
184
- message: prompt,
185
- });
186
- if (Number.isInteger(res)) {
187
- return res;
188
- }
189
- logger_1.logger.error(`${param.label || param.param} must be an integer; retrying...`);
260
+ return +res;
261
+ });
262
+ }
263
+ if (isTextInput(param.input)) {
264
+ prompt = `Enter an integer value for ${param.label || param.name}:`;
265
+ if (param.description) {
266
+ prompt += ` \n(${param.description})`;
267
+ }
268
+ return promptText(prompt, param.input, resolvedDefault, (res) => {
269
+ if (isNaN(+res)) {
270
+ return { message: `"${res}" could not be converted to a number.` };
271
+ }
272
+ if (res.includes(".")) {
273
+ return { message: `${res} is not an integer value.` };
190
274
  }
275
+ return +res;
276
+ });
277
+ }
278
+ else if (isResourceInput(param.input)) {
279
+ throw new error_1.FirebaseError("Numeric params cannot have Cloud Resource selector inputs");
280
+ }
281
+ else {
282
+ (0, functional_1.assertExhaustive)(param.input);
283
+ }
284
+ }
285
+ async function promptResourceString(prompt, input, projectId, resolvedDefault) {
286
+ const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
287
+ switch (input.resource.type) {
288
+ case "storage.googleapis.com/Bucket":
289
+ const buckets = await (0, storage_1.listBuckets)(projectId);
290
+ if (buckets.length === 0) {
291
+ throw notFound;
292
+ }
293
+ const forgedInput = {
294
+ select: {
295
+ options: buckets.map((bucketName) => {
296
+ return { label: bucketName, value: bucketName };
297
+ }),
298
+ },
299
+ };
300
+ return promptSelect(prompt, forgedInput, resolvedDefault, (res) => res);
301
+ default:
302
+ logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
303
+ return promptText(prompt, { text: {} }, resolvedDefault, (res) => res);
304
+ }
305
+ }
306
+ async function promptText(prompt, input, resolvedDefault, converter) {
307
+ const res = await (0, prompt_1.promptOnce)({
308
+ type: "input",
309
+ default: resolvedDefault,
310
+ message: prompt,
311
+ });
312
+ if (input.text.validationRegex) {
313
+ const userRe = new RegExp(input.text.validationRegex);
314
+ if (!userRe.test(res)) {
315
+ logger_1.logger.error(input.text.validationErrorMessage ||
316
+ `Input did not match provided validator ${userRe.toString()}, retrying...`);
317
+ return promptText(prompt, input, resolvedDefault, converter);
318
+ }
319
+ }
320
+ const converted = converter(res);
321
+ if (typeof converted === "object") {
322
+ logger_1.logger.error(converted.message);
323
+ return promptText(prompt, input, resolvedDefault, converter);
324
+ }
325
+ return converted;
326
+ }
327
+ async function promptSelect(prompt, input, resolvedDefault, converter) {
328
+ const response = await (0, prompt_1.promptOnce)({
329
+ name: "input",
330
+ type: "list",
331
+ default: () => {
332
+ resolvedDefault;
333
+ },
334
+ message: prompt,
335
+ choices: input.select.options.map((option) => {
336
+ return {
337
+ checked: false,
338
+ name: option.label,
339
+ value: option.value.toString(),
340
+ };
341
+ }),
342
+ });
343
+ const converted = converter(response);
344
+ if (typeof converted === "object") {
345
+ logger_1.logger.error(converted.message);
346
+ return promptSelect(prompt, input, resolvedDefault, converter);
191
347
  }
348
+ return converted;
192
349
  }
@@ -23,10 +23,13 @@ const error_1 = require("../../error");
23
23
  const projectConfig_1 = require("../../functions/projectConfig");
24
24
  const v1_1 = require("../../functions/events/v1");
25
25
  const serviceusage_1 = require("../../gcp/serviceusage");
26
+ const previews_1 = require("../../previews");
27
+ const applyHash_1 = require("./cache/applyHash");
26
28
  function hasUserConfig(config) {
27
29
  return Object.keys(config).length > 1;
28
30
  }
29
31
  async function prepare(context, options, payload) {
32
+ var _a;
30
33
  const projectId = (0, projectUtils_1.needProjectId)(options);
31
34
  const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
32
35
  context.config = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
@@ -78,16 +81,36 @@ async function prepare(context, options, payload) {
78
81
  const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
79
82
  const envs = Object.assign(Object.assign({}, userEnvs), firebaseEnvs);
80
83
  const wantBuild = await runtimeDelegate.discoverBuild(runtimeConfig, firebaseEnvs);
81
- const wantBackend = await build.resolveBackend(wantBuild, userEnvOpt, userEnvs);
84
+ const { backend: wantBackend, envs: resolvedEnvs } = await build.resolveBackend(wantBuild, userEnvOpt, userEnvs, options.nonInteractive);
85
+ let hasEnvsFromParams = false;
82
86
  wantBackend.environmentVariables = envs;
87
+ for (const envName of Object.keys(resolvedEnvs)) {
88
+ const envValue = (_a = resolvedEnvs[envName]) === null || _a === void 0 ? void 0 : _a.toString();
89
+ if (envValue &&
90
+ !Object.prototype.hasOwnProperty.call(wantBackend.environmentVariables, envName)) {
91
+ wantBackend.environmentVariables[envName] = envValue;
92
+ hasEnvsFromParams = true;
93
+ }
94
+ }
83
95
  for (const endpoint of backend.allEndpoints(wantBackend)) {
84
96
  endpoint.environmentVariables = wantBackend.environmentVariables;
85
97
  endpoint.codebase = codebase;
86
98
  }
87
99
  wantBackends[codebase] = wantBackend;
88
- if (functionsEnv.hasUserEnvs(userEnvOpt)) {
100
+ if (functionsEnv.hasUserEnvs(userEnvOpt) || hasEnvsFromParams) {
89
101
  codebaseUsesEnvs.push(codebase);
90
102
  }
103
+ if (wantBuild.params.length > 0) {
104
+ if (wantBuild.params.every((p) => p.type !== "secret")) {
105
+ void (0, track_1.track)("functions_params_in_build", "env_only");
106
+ }
107
+ else {
108
+ void (0, track_1.track)("functions_params_in_build", "with_secrets");
109
+ }
110
+ }
111
+ else {
112
+ void (0, track_1.track)("functions_params_in_build", "none");
113
+ }
91
114
  }
92
115
  validate.endpointsAreUnique(wantBackends);
93
116
  for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
@@ -99,10 +122,14 @@ async function prepare(context, options, payload) {
99
122
  (0, utils_1.logLabeledBullet)("functions", `preparing ${clc.bold(sourceDirName)} directory for uploading...`);
100
123
  }
101
124
  if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
102
- source.functionsSourceV2 = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config);
125
+ const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config);
126
+ source.functionsSourceV2 = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.pathToSource;
127
+ source.functionsSourceV2Hash = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.hash;
103
128
  }
104
129
  if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) {
105
- source.functionsSourceV1 = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config, runtimeConfig);
130
+ const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config, runtimeConfig);
131
+ source.functionsSourceV1 = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.pathToSource;
132
+ source.functionsSourceV1Hash = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.hash;
106
133
  }
107
134
  context.sources[codebase] = source;
108
135
  }
@@ -160,6 +187,9 @@ async function prepare(context, options, payload) {
160
187
  await (0, checkIam_1.ensureServiceAgentRoles)(projectId, projectNumber, matchingBackend, haveBackend);
161
188
  await validate.secretsAreValid(projectId, matchingBackend);
162
189
  await ensure.secretAccess(projectId, matchingBackend, haveBackend);
190
+ if (previews_1.previews.skipdeployingnoopfunctions) {
191
+ (0, applyHash_1.applyBackendHashToBackends)(wantBackends, context);
192
+ }
163
193
  }
164
194
  exports.prepare = prepare;
165
195
  function inferDetailsFromExisting(want, have, usedDotenv) {
@@ -9,6 +9,7 @@ const path = require("path");
9
9
  const tmp = require("tmp");
10
10
  const error_1 = require("../../error");
11
11
  const logger_1 = require("../../logger");
12
+ const hash_1 = require("./cache/hash");
12
13
  const functionsConfig = require("../../functionsConfig");
13
14
  const utils = require("../../utils");
14
15
  const fsAsync = require("../../fsAsync");
@@ -49,23 +50,29 @@ async function packageSource(sourceDir, config, runtimeConfig) {
49
50
  encoding: "binary",
50
51
  });
51
52
  const archive = archiver("zip");
53
+ const hashes = [];
52
54
  const ignore = config.ignore || ["node_modules", ".git"];
53
55
  ignore.push("firebase-debug.log", "firebase-debug.*.log", CONFIG_DEST_FILE);
54
56
  try {
55
57
  const files = await fsAsync.readdirRecursive({ path: sourceDir, ignore: ignore });
56
58
  for (const file of files) {
59
+ const name = path.relative(sourceDir, file.name);
60
+ const fileHash = await (0, hash_1.getSourceHash)(file.name);
61
+ hashes.push(fileHash);
57
62
  archive.file(file.name, {
58
- name: path.relative(sourceDir, file.name),
63
+ name,
59
64
  mode: file.mode,
60
65
  });
61
66
  }
62
67
  if (typeof runtimeConfig !== "undefined") {
63
- archive.append(JSON.stringify(runtimeConfig, null, 2), {
68
+ const runtimeConfigString = JSON.stringify(runtimeConfig, null, 2);
69
+ hashes.push(runtimeConfigString);
70
+ archive.append(runtimeConfigString, {
64
71
  name: CONFIG_DEST_FILE,
65
72
  mode: 420,
66
73
  });
67
74
  }
68
- archive.finalize();
75
+ await archive.finalize();
69
76
  await pipeAsync(archive, fileStream);
70
77
  }
71
78
  catch (err) {
@@ -80,7 +87,8 @@ async function packageSource(sourceDir, config, runtimeConfig) {
80
87
  " (" +
81
88
  filesize(archive.pointer()) +
82
89
  ") for uploading");
83
- return tmpFile;
90
+ const hash = hashes.join(".");
91
+ return { pathToSource: tmpFile, hash };
84
92
  }
85
93
  async function prepareFunctionsUpload(sourceDir, config, runtimeConfig) {
86
94
  return packageSource(sourceDir, config, runtimeConfig);