Repository: firebase/firebase-tools (ts)

Issue:
Firebase Hosting: `firebase deploy` ignores the -m argument in version 11.14.1
In version 11.14.1 of `firebase-tools`, the `firebase deploy` command seems to completely ignore the -m argument (description), and the description in empty in the Firebase Hosting dashboard.
Version 11.13.0 was working fine in that regard.

I believe this change to be the offending one, where it seems that the release message was dropped in the new api.createRelease function

https://github.com/firebase/firebase-tools/commit/3a92daf7d663afb3d08790640f44bc77435c9d4e#diff-7d0f3d70809a7e16334fc0ebd9d0b60757f72a416dd11397b62c37ef41b5ed1fL51

Pull request:
fix missing message on deploy
<!--

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

The message was dropped (because Options was removed?) on release. This restores that functionality and adds tests!

### Scenarios Tested

<!-- Write a list of all the user journeys and edge cases you've tested. Instructions for manual testing can be found at https://github.com/firebase/firebase-tools/blob/master/.github/CONTRIBUTING.md#development-setup -->

![image](https://user-images.githubusercontent.com/160236/195192082-2aa14947-3997-4ef7-a666-5e3ae84830a1.png)

Fixes #5107

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7d253d7a..5cb75b84c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,1 +1,2 @@
 - Fixes issue where errors were not properly propagating when listing backends. (#5071)
+- Fixes issue where message from `-m` on deploy was not being properly applied. (#5107)
diff --git a/src/deploy/hosting/release.ts b/src/deploy/hosting/release.ts
index 7ea45f8df..859ae2184 100644
--- a/src/deploy/hosting/release.ts
+++ b/src/deploy/hosting/release.ts
@@ -8,7 +8,7 @@ import { FirebaseError } from "../../error";
 /**
  *  Release finalized a Hosting release.
  */
-export async function release(context: Context): Promise<void> {
+export async function release(context: Context, options: { message?: string }): Promise<void> {
   if (!context.hosting || !context.hosting.deploys) {
     return;
   }
@@ -40,10 +40,15 @@ export async function release(context: Context): Promise<void> {
         logger.debug("[hosting] releasing to channel:", context.hostingChannel);
       }
 
+      const otherReleaseOpts: Partial<Pick<api.Release, "message">> = {};
+      if (options.message) {
+        otherReleaseOpts.message = options.message;
+      }
       const release = await api.createRelease(
         deploy.config.site,
         context.hostingChannel || "live",
-        deploy.version
+        deploy.version,
+        otherReleaseOpts
       );
       logger.debug("[hosting] release:", release);
       utils.logLabeledSuccess(`hosting[${deploy.config.site}]`, "release complete");
diff --git a/src/hosting/api.ts b/src/hosting/api.ts
index bfa99fabe..101d83d3d 100644
--- a/src/hosting/api.ts
+++ b/src/hosting/api.ts
@@ -31,7 +31,7 @@ enum ReleaseType {
   SITE_DISABLE = "SITE_DISABLE",
 }
 
-interface Release {
+export interface Release {
   // The unique identifier for the release, in the format:
   // <code>sites/<var>site-name</var>/releases/<var>releaseID</var></code>
   readonly name: string;
@@ -451,6 +451,8 @@ export async function cloneVersion(
   return pollRes;
 }
 
+type PartialRelease = Partial<Pick<Release, "message" | "type">>;
+
 /**
  * Create a release on a channel.
  * @param site the site for the version.
@@ -460,13 +462,14 @@ export async function cloneVersion(
 export async function createRelease(
   site: string,
   channel: string,
-  version: string
+  version: string,
+  partialRelease?: PartialRelease
 ): Promise<Release> {
-  const res = await apiClient.request<void, Release>({
-    method: "POST",
-    path: `/projects/-/sites/${site}/channels/${channel}/releases`,
-    queryParams: { versionName: version },
-  });
+  const res = await apiClient.post<PartialRelease, Release>(
+    `/projects/-/sites/${site}/channels/${channel}/releases`,
+    partialRelease,
+    { queryParams: { versionName: version } }
+  );
   return res.body;
 }
 


Test patch:
diff --git a/src/test/deploy/hosting/release.spec.ts b/src/test/deploy/hosting/release.spec.ts
new file mode 100644
index 000000000..d8ed13d6b
--- /dev/null
+++ b/src/test/deploy/hosting/release.spec.ts
@@ -0,0 +1,156 @@
+import { expect } from "chai";
+import * as sinon from "sinon";
+
+import * as api from "../../../hosting/api";
+import { Context } from "../../../deploy/hosting/context";
+import * as convertConfigPkg from "../../../deploy/hosting/convertConfig";
+
+import { release } from "../../../deploy/hosting/release";
+import { last } from "../../../utils";
+
+describe("release", () => {
+  const PROJECT = "fake-project";
+  const SITE = "my-site";
+  const VERSION = "it/ends/up/like/this/version-id";
+  const FAKE_CONFIG = {};
+
+  let updateVersionStub: sinon.SinonStub;
+  let createReleaseStub: sinon.SinonStub;
+
+  beforeEach(() => {
+    updateVersionStub = sinon.stub(api, "updateVersion").rejects("updateVersion unstubbed");
+    createReleaseStub = sinon.stub(api, "createRelease").rejects("createRelease unstubbed");
+    sinon.stub(convertConfigPkg, "convertConfig").resolves(FAKE_CONFIG);
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe("with no Hosting deploys", () => {
+    it("should bail", async () => {
+      await release({ projectId: "foo" }, {});
+
+      expect(updateVersionStub).to.have.been.not.called;
+      expect(createReleaseStub).to.have.been.not.called;
+    });
+  });
+
+  describe("a single site", () => {
+    const CONTEXT: Context = {
+      projectId: PROJECT,
+      hosting: {
+        deploys: [{ config: { site: SITE }, version: VERSION }],
+      },
+    };
+
+    const UPDATE: Partial<api.Version> = {
+      status: "FINALIZED",
+      config: FAKE_CONFIG,
+    };
+
+    it("should update a version and make a release", async () => {
+      updateVersionStub.resolves({});
+      createReleaseStub.resolves({});
+
+      await release(CONTEXT, {});
+
+      expect(updateVersionStub).to.have.been.calledOnceWithExactly(
+        SITE,
+        last(VERSION.split("/")),
+        UPDATE
+      );
+      expect(createReleaseStub).to.have.been.calledOnceWithExactly(SITE, "live", VERSION, {});
+    });
+
+    it("should update a version and make a release with a message", async () => {
+      updateVersionStub.resolves({});
+      createReleaseStub.resolves({});
+
+      await release(CONTEXT, { message: "hello world" });
+
+      expect(updateVersionStub).to.have.been.calledOnceWithExactly(
+        SITE,
+        last(VERSION.split("/")),
+        UPDATE
+      );
+      expect(createReleaseStub).to.have.been.calledOnceWithExactly(SITE, "live", VERSION, {
+        message: "hello world",
+      });
+    });
+  });
+
+  describe("multiple sites", () => {
+    const CONTEXT: Context = {
+      projectId: PROJECT,
+      hosting: {
+        deploys: [
+          { config: { site: SITE }, version: VERSION },
+          { config: { site: `${SITE}-2` }, version: `${VERSION}-2` },
+        ],
+      },
+    };
+
+    const UPDATE: Partial<api.Version> = {
+      status: "FINALIZED",
+      config: FAKE_CONFIG,
+    };
+
+    it("should update a version and make a release", async () => {
+      updateVersionStub.resolves({});
+      createReleaseStub.resolves({});
+
+      await release(CONTEXT, {});
+
+      expect(updateVersionStub).to.have.been.calledTwice;
+      expect(updateVersionStub).to.have.been.calledWithExactly(
+        SITE,
+        last(VERSION.split("/")),
+        UPDATE
+      );
+      expect(updateVersionStub).to.have.been.calledWithExactly(
+        `${SITE}-2`,
+        `${last(VERSION.split("/"))}-2`,
+        UPDATE
+      );
+      expect(createReleaseStub).to.have.been.calledTwice;
+      expect(createReleaseStub).to.have.been.calledWithExactly(SITE, "live", VERSION, {});
+      expect(createReleaseStub).to.have.been.calledWithExactly(
+        `${SITE}-2`,
+        "live",
+        `${VERSION}-2`,
+        {}
+      );
+    });
+  });
+
+  describe("to a hosting channel", () => {
+    const CHANNEL = "my-channel";
+    const CONTEXT: Context = {
+      projectId: PROJECT,
+      hostingChannel: CHANNEL,
+      hosting: {
+        deploys: [{ config: { site: SITE }, version: VERSION }],
+      },
+    };
+
+    const UPDATE: Partial<api.Version> = {
+      status: "FINALIZED",
+      config: FAKE_CONFIG,
+    };
+
+    it("should update a version and make a release", async () => {
+      updateVersionStub.resolves({});
+      createReleaseStub.resolves({});
+
+      await release(CONTEXT, {});
+
+      expect(updateVersionStub).to.have.been.calledOnceWithExactly(
+        SITE,
+        last(VERSION.split("/")),
+        UPDATE
+      );
+      expect(createReleaseStub).to.have.been.calledOnceWithExactly(SITE, CHANNEL, VERSION, {});
+    });
+  });
+});
diff --git a/src/test/hosting/api.spec.ts b/src/test/hosting/api.spec.ts
index 5f00b7fb8..af80ef7f7 100644
--- a/src/test/hosting/api.spec.ts
+++ b/src/test/hosting/api.spec.ts
@@ -448,6 +448,27 @@ describe("hosting", () => {
       expect(nock.isDone()).to.be.true;
     });
 
+    it("should include a message, if provided", async () => {
+      const CHANNEL_ID = "my-channel";
+      const RELEASE = { name: "my-new-release" };
+      const VERSION = "version";
+      const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`;
+      const MESSAGE = "yo dawg";
+      nock(hostingApiOrigin)
+        .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`, {
+          message: MESSAGE,
+        })
+        .query({ versionName: VERSION_NAME })
+        .reply(201, RELEASE);
+
+      const res = await hostingApi.createRelease(SITE, CHANNEL_ID, VERSION_NAME, {
+        message: MESSAGE,
+      });
+
+      expect(res).to.deep.equal(RELEASE);
+      expect(nock.isDone()).to.be.true;
+    });
+
     it("should throw an error if the server returns an error", async () => {
       const CHANNEL_ID = "my-channel";
       const VERSION = "VERSION";