Repository: firebase/firebase-tools (ts)

Issue:
hosting:channel:deploy: Hosting configs should only include either "site" or \"target\", not both.
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

I have a multisite config with targets. Using `npx firebase-tools deploy --only hosting` or `npx firebase-tools deploy --only hosting:bbbb` works fine. But if I use the new GitHub action, it fails on the line:

```
 /usr/local/bin/npx firebase-tools hosting:channel:deploy pr22-blah --expires 30d --project blah-dev --json
```

Running the same thing locally fails in the same way.  The error is:

```sh
$ npx firebase-tools hosting:channel:deploy pr22-blah --only bbb --expires 30d --project blah-dev --json
{
  "status": "error",
  "error": "Hosting configs should only include either \"site\" or \"target\", not both."
}
```

Note that the command I used there adds `--only bbb`. The command fails the same way with or without that.

Also note that my `firebase.json` does NOT list `"site"` at all. See below:

.firebaserc:
```json
{
  "projects": {
    "default": "blah-dev",
    "dev": "blah-dev",
    "prod": "blah-io"
  },
  "targets": {
    "blah-io": {
      "hosting": {
        "aaa": [
          "blah-aaa"
        ],
        "boilerplate": [
          "blah-boilerplate"
        ],
        "bbb": [
          "blah-bingo"
        ]
      }
    },
    "blah-dev": {
      "hosting": {
        "aaa": [
          "blah-aaa-dev"
        ],
        "boilerplate": [
          "blah-boilerplate-dev"
        ],
        "bbb": [
          "blah-bbb-dev"
        ]
      }
    }
  }
}
```

firebase.json
```json
{
  "firestore": {...},
  "database": {...},
  "functions": {...},
  "hosting": [
    {
      "target": "aaa",
      "public": "packages/aaa/build",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "boilerplate",
      "public": "packages/boilerplate/build",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "bbb",
      "public": "packages/bbb/build",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
  ],
  "emulators": {...}
}

```

<!--
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!*
-->

### [REQUIRED] Environment info


**firebase-tools:**. `npx firebase-tools --version` -> `8.14.0`

**Platform:** OSX, but also GitHub Actions default image

### [REQUIRED] Test case

See above.

### [REQUIRED] Steps to reproduce

See above. Run the hosting:channel:deploy command

### [REQUIRED] Expected behavior

It deploys

### [REQUIRED] Actual behavior

```
{
  "status": "error",
  "error": "Hosting configs should only include either \"site\" or \"target\", not both."
}
```

(`--debug` flag adds a lot of HTTP logging noise at the front, but it's all 200s.)


Pull request:
fix use of normalizedHostingConfigs when it is called repeatedly
<!--

Thank you for contributing to the Firebase community! Please fill out the form below.

Run the linter and test suite
==============================
Run `npm test` to make sure your changes compile properly and the tests all pass on your local machine. We've hooked up this repo with continuous integration to double check those things for you.

-->

### Description

Fixes #2746

The `hosting:channel:deploy` path normalizes the configs once before going through the normal deploy path. Unfortunately, the normalizing process modified the underlying config which ended up causing issues when the normalize method was called a second time. Deep cloning the config fixes this issue.

Also, added some functional tests for deploying to a target and deploying to a channel!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29bb..33d3627a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,1 @@
+- Fixes issue in `hosting:channel` commands where a Firebase Hosting target may cause configuration parsing issues (#2746).
diff --git a/src/hosting/normalizedHostingConfigs.ts b/src/hosting/normalizedHostingConfigs.ts
index 2744d1707..1cb9decea 100644
--- a/src/hosting/normalizedHostingConfigs.ts
+++ b/src/hosting/normalizedHostingConfigs.ts
@@ -1,4 +1,5 @@
 import { bold } from "cli-color";
+import { cloneDeep } from "lodash";
 
 import { FirebaseError } from "../error";
 
@@ -65,7 +66,7 @@ export function normalizedHostingConfigs(
   cmdOptions: any, // eslint-disable-line @typescript-eslint/no-explicit-any
   options: { resolveTargets?: boolean } = {}
 ): HostingConfig[] {
-  let configs = cmdOptions.config.get("hosting");
+  let configs = cloneDeep(cmdOptions.config.get("hosting"));
   if (!configs) {
     return [];
   }


Test patch:
diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh
index 631b687e7..e508dad52 100755
--- a/scripts/hosting-tests/run.sh
+++ b/scripts/hosting-tests/run.sh
@@ -13,7 +13,7 @@ echo "Running with Application Creds: ${GOOGLE_APPLICATION_CREDENTIALS}"
 
 echo "Target project: ${FBTOOLS_TARGET_PROJECT}"
 
-echo "Initalizing some variables..."
+echo "Initializing some variables..."
 DATE="$(date)"
 echo "Variables initalized..."
 
@@ -25,7 +25,7 @@ echo "Installing firebase-tools..."
 npm link
 echo "Installed firebase-tools: $(which firebase)"
 
-echo "Initalizing temp directory..."
+echo "Initializing temp directory..."
 cd "${TEMP_DIR}"
 cat > "firebase.json" <<- EOM
 {
@@ -42,7 +42,7 @@ EOM
 mkdir "public"
 touch "public/${TARGET_FILE}"
 echo "${DATE}" > "public/${TARGET_FILE}"
-echo "Initalized temp directory."
+echo "Initialized temp directory."
 
 echo "Testing local serve..."
 PORT=8685
@@ -72,3 +72,53 @@ sleep 5
 VALUE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/${TARGET_FILE})"
 test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false)
 echo "Tested hosting deployment."
+
+# Test more complex scenarios:
+echo "Creating second temp directory..."
+TEMP_DIR="$(mktemp -d)"
+echo "Created second temp directory: ${TEMP_DIR}"
+
+echo "Initializing a new date..."
+DATE="$(date)"
+echo "Initialized a new date."
+
+echo "Initializing second temp directory..."
+cd "${TEMP_DIR}"
+cat > "firebase.json" <<- EOM
+{
+  "hosting": [
+    {
+      "target": "customtarget",
+      "public": "public",
+      "ignore": [
+        "firebase.json",
+        "**/.*",
+        "**/node_modules/**"
+      ]
+    }
+  ]
+}
+EOM
+mkdir "public"
+touch "public/${TARGET_FILE}"
+echo "${DATE}" > "public/${TARGET_FILE}"
+echo "Setting targets..."
+firebase use --add "${FBTOOLS_TARGET_PROJECT}"
+firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}"
+echo "Set targets."
+echo "Initialized second temp directory."
+
+echo "Testing hosting deployment by target..."
+firebase deploy --only hosting:customtarget --project "${FBTOOLS_TARGET_PROJECT}"
+sleep 5
+VALUE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/${TARGET_FILE})"
+test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false)
+echo "Tested hosting deployment by target."
+
+echo "Testing hosting channel deployment by target..."
+firebase hosting:channel:deploy mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --json | tee output.json
+sleep 5
+CHANNEL_URL=$(cat output.json | jq -r ".result.customtarget.url")
+VALUE="$(curl ${CHANNEL_URL}/${TARGET_FILE})"
+test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false)
+echo "Tested hosting channel deployment by target."
\ No newline at end of file
diff --git a/src/test/hosting/normalizedHostingConfigs.spec.ts b/src/test/hosting/normalizedHostingConfigs.spec.ts
index 6fa9a80ed..e8d58bcae 100644
--- a/src/test/hosting/normalizedHostingConfigs.spec.ts
+++ b/src/test/hosting/normalizedHostingConfigs.spec.ts
@@ -26,6 +26,17 @@ describe("normalizedHostingConfigs", () => {
     );
   });
 
+  it("should not modify the config when resolving targets", () => {
+    const singleHostingConfig = { target: "target" };
+    const cmdConfig = {
+      site: "default-site",
+      config: { get: () => singleHostingConfig },
+      rc: { requireTarget: () => ["default-site"] },
+    };
+    normalizedHostingConfigs(cmdConfig, { resolveTargets: true });
+    expect(singleHostingConfig).to.deep.equal({ target: "target" });
+  });
+
   describe("without an only parameter", () => {
     const DEFAULT_SITE = "default-hosting-site";
     const baseConfig = { public: "public", ignore: ["firebase.json"] };