Repository: firebase/firebase-tools (ts)

Issue:
Failed to deploy with serviceAccount as StringParam defined in .env file
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

<!--
Thank you for contributing to the Firebase community!

Think you found a bug?
=======================
Yeah, we're definitely not perfect! Please use this template and include a minimal repro when opening the issue. If you know how to solve the issue, please create a Pull Request, and we'd be happy to review it!

Have a feature request?
========================
Great, we love hearing how we can improve our products! However, do not use this template to submit a feature request. Please submit your feature requests to: https://firebase.google.com/support/contact/bugs-features/

Have a usage question?
=======================
We get lots of those and we love helping you, but GitHub is not the best place for them and they will be closed. Please take a look at the guide first: https://firebase.google.com/docs/cli/

If the official documentation doesn't help, try asking through our official support channel: https://firebase.google.com/support/

Additional locations to check for solutions or assistance from the community:
- Stack Overflow: https://stackoverflow.com/
- Firebase Slack Community: https://firebase.community/

*Please avoid duplicate posting across multiple channels!*
-->

### Environment info

<!-- What version of the Firebase CLI (`firebase-tools`) are you using? Note that your issue may already be fixed in the latest versions. The latest version can be found at https://github.com/firebase/firebase-tools/releases -->

<!-- Output of `firebase --version` -->

**firebase-tools:**
12.5.4

<!-- e.g. macOS, Windows, Ubuntu -->

**Platform:**
macOS

### Test case

<!-- Provide a minimal, complete, and verifiable example (http://stackoverflow.com/help/mcve) -->
.env
```
SERVICE_ACCOUNT=(service account here)
```

index.js
```
const {defineString} = require("firebase-functions/params");
const {setGlobalOptions} = require("firebase-functions/v2/options");
const {onRequest} = require("firebase-functions/v2/https");

const serviceAccount = defineString("SERVICE_ACCOUNT");

setGlobalOptions({
  serviceAccount: serviceAccount,
});

exports.sayHello = onRequest(
  (req, res) => {
    res.status(200).send("Hello world!");
  }
);

```
&uarr; this does not work, but this works &darr;

workaround.js
```
...

setGlobalOptions({
  serviceAccount: serviceAccount.value(),
});

exports.sayHello = onRequest(
  (req, res) => {
    res.status(200).send("Hello world!");
  }
);

```

### Steps to reproduce

<!-- Provide the steps needed to reproduce the issue with the above test case. -->
```
firebase deploy
```

### Expected behavior

<!-- What is the expected behavior? -->
to just deploy normally

### Actual behavior

<!-- Run the command with --debug flag, and include the logs below. -->
for index.js, the output:
```
...
⚠  functions: HTTP Error: 400, {{ params.SERVICE_ACCOUNT }} should be a valid email or unique id.
⚠  functions:  failed to update function projects/***/locations/***/functions/sayHello
Failed to update function projects/***/locations/***/functions/sayHello
...
```

for workaround.js, the output:
```
...
i  functions: Loading and analyzing source code for codebase default to determine what to deploy
Serving at port 8079

{"severity":"WARNING","message":"params.SERVICE_ACCOUNT.value() invoked during function deployment, instead of during runtime."}

{"severity":"WARNING","message":"This is usually a mistake. In configs, use Params directly without calling .value()."}
{"severity":"WARNING","message":"example: { memory: memoryParam } not { memory: memoryParam.value() }"}
...
```
workaround deploys sucessfully, but its suggestions (without `.value()`) fails, as shown in index.js

On error, hard-coded values are printed out as is, while StringParam is shown as `{{ params.SERVICE_ACCOUNT }}`, suggesting the CLI may not have evaluated the Param as a string before validating and using the value.

Pull request description:
Fix service account option to be a param
Fixes #6365 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aac70563f..1ea59aee1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,2 +1,3 @@
+- Fixed an issue where the functions service account option was not treated as a param (#6389).
 - Fixed an issue with deploying function groups containing v2 functions. (#6408)
 - Use GetDefaultBucket endpoint to fetch Storage Default Bucket.
diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts
index 846a49043..573a199ab 100644
--- a/src/deploy/functions/build.ts
+++ b/src/deploy/functions/build.ts
@@ -235,7 +235,7 @@ export type Endpoint = Triggered & {
   // The services account that this function should run as.
   // defaults to the GAE service account when a function is first created as a GCF gen 1 function.
   // Defaults to the compute service account when a function is first created as a GCF gen 2 function.
-  serviceAccount?: ServiceAccount | null;
+  serviceAccount?: Field<string> | ServiceAccount | null;
 
   // defaults to ["us-central1"], overridable in firebase-tools with
   //  process.env.FIREBASE_FUNCTIONS_DEFAULT_REGION
@@ -457,8 +457,7 @@ export function toBackend(
         bdEndpoint,
         "environmentVariables",
         "labels",
-        "secretEnvironmentVariables",
-        "serviceAccount"
+        "secretEnvironmentVariables"
       );
 
       proto.convertIfPresent(bkEndpoint, bdEndpoint, "ingressSettings", (from) => {
@@ -479,6 +478,7 @@ export function toBackend(
         return (mem as backend.MemoryOptions) || null;
       });
 
+      r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");
       r.resolveInts(
         bkEndpoint,
         bdEndpoint,


Test patch:
diff --git a/src/test/deploy/functions/build.spec.ts b/src/test/deploy/functions/build.spec.ts
index 873c9a2a3..0f29dd55f 100644
--- a/src/test/deploy/functions/build.spec.ts
+++ b/src/test/deploy/functions/build.spec.ts
@@ -1,5 +1,6 @@
 import { expect } from "chai";
 import * as build from "../../../deploy/functions/build";
+import { ParamValue } from "../../../deploy/functions/params";
 
 describe("toBackend", () => {
   it("populates backend info from Build", () => {
@@ -110,4 +111,49 @@ describe("toBackend", () => {
       ).to.have.members(["service-account-1@", "service-account-2@"]);
     }
   });
+
+  it("populates multiple param values", () => {
+    const desiredBuild: build.Build = build.of({
+      func: {
+        platform: "gcfv2",
+        region: ["us-central1"],
+        project: "project",
+        runtime: "nodejs16",
+        entryPoint: "func",
+        maxInstances: "{{ params.maxinstances }}",
+        minInstances: "{{ params.mininstances }}",
+        serviceAccount: "{{ params.serviceaccount }}",
+        vpc: {
+          connector: "projects/project/locations/region/connectors/connector",
+          egressSettings: "PRIVATE_RANGES_ONLY",
+        },
+        ingressSettings: "ALLOW_ALL",
+        labels: {
+          test: "testing",
+        },
+        httpsTrigger: {
+          invoker: ["service-account-2@", "service-account-3@"],
+        },
+      },
+    });
+    const backend = build.toBackend(desiredBuild, {
+      maxinstances: new ParamValue("42", false, { number: true }),
+      mininstances: new ParamValue("1", false, { number: true }),
+      serviceaccount: new ParamValue("service-account-1@", false, { string: true }),
+    });
+    expect(Object.keys(backend.endpoints).length).to.equal(1);
+    const endpointDef = Object.values(backend.endpoints)[0];
+    expect(endpointDef).to.not.equal(undefined);
+    if (endpointDef) {
+      expect(endpointDef.func.id).to.equal("func");
+      expect(endpointDef.func.project).to.equal("project");
+      expect(endpointDef.func.region).to.equal("us-central1");
+      expect(endpointDef.func.maxInstances).to.equal(42);
+      expect(endpointDef.func.minInstances).to.equal(1);
+      expect(endpointDef.func.serviceAccount).to.equal("service-account-1@");
+      expect(
+        "httpsTrigger" in endpointDef.func ? endpointDef.func.httpsTrigger.invoker : []
+      ).to.have.members(["service-account-2@", "service-account-3@"]);
+    }
+  });
 });