Repository: intuit/auto (ts)

Issue:
--quiet flag
**Is your feature request related to a problem? Please describe.**

Sometimes I only want auto to output the result of an operation so I can use it in other scripts.

**Describe the solution you'd like**

1. `auto canary --quiet` - output only the canary version(s) published
2. `auto changelog --quiet` - output only the changelog
3. `auto latest --quiet` - output only the version(s) published
4. `auto next --quiet` - output only the next version(s) published
4. `auto shipit --quiet` - output only the version(s) published


Pull request description:
Add --quiet flag
# What Changed

Add a new flag to suppress most output.

# Why

Sometimes you want to be able to parse the published versions for other scripts.

closes #1154 

Todo:

- [x] Add tests
- [ ] Add docs

<!-- GITHUB_RELEASE PR BODY: canary-version -->
<details>
  <summary>📦 Published PR as canary version: <code>under canary scope @auto-canary@9.27.4-canary.1155.14985.0</code></summary>
  <br />
  
  :sparkles: Test out this PR locally via:
  
  ```sh
  npm install @auto-canary/bot-list@9.27.4-canary.1155.14985.0
  npm install @auto-canary/auto@9.27.4-canary.1155.14985.0
  npm install @auto-canary/core@9.27.4-canary.1155.14985.0
  npm install @auto-canary/all-contributors@9.27.4-canary.1155.14985.0
  npm install @auto-canary/brew@9.27.4-canary.1155.14985.0
  npm install @auto-canary/chrome@9.27.4-canary.1155.14985.0
  npm install @auto-canary/cocoapods@9.27.4-canary.1155.14985.0
  npm install @auto-canary/conventional-commits@9.27.4-canary.1155.14985.0
  npm install @auto-canary/crates@9.27.4-canary.1155.14985.0
  npm install @auto-canary/exec@9.27.4-canary.1155.14985.0
  npm install @auto-canary/first-time-contributor@9.27.4-canary.1155.14985.0
  npm install @auto-canary/gh-pages@9.27.4-canary.1155.14985.0
  npm install @auto-canary/git-tag@9.27.4-canary.1155.14985.0
  npm install @auto-canary/gradle@9.27.4-canary.1155.14985.0
  npm install @auto-canary/jira@9.27.4-canary.1155.14985.0
  npm install @auto-canary/maven@9.27.4-canary.1155.14985.0
  npm install @auto-canary/npm@9.27.4-canary.1155.14985.0
  npm install @auto-canary/omit-commits@9.27.4-canary.1155.14985.0
  npm install @auto-canary/omit-release-notes@9.27.4-canary.1155.14985.0
  npm install @auto-canary/released@9.27.4-canary.1155.14985.0
  npm install @auto-canary/s3@9.27.4-canary.1155.14985.0
  npm install @auto-canary/slack@9.27.4-canary.1155.14985.0
  npm install @auto-canary/twitter@9.27.4-canary.1155.14985.0
  npm install @auto-canary/upload-assets@9.27.4-canary.1155.14985.0
  # or 
  yarn add @auto-canary/bot-list@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/auto@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/core@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/all-contributors@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/brew@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/chrome@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/cocoapods@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/conventional-commits@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/crates@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/exec@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/first-time-contributor@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/gh-pages@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/git-tag@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/gradle@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/jira@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/maven@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/npm@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/omit-commits@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/omit-release-notes@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/released@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/s3@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/slack@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/twitter@9.27.4-canary.1155.14985.0
  yarn add @auto-canary/upload-assets@9.27.4-canary.1155.14985.0
  ```
</details>
<!-- GITHUB_RELEASE PR BODY: canary-version -->


Patch:
diff --git a/packages/cli/src/parse-args.ts b/packages/cli/src/parse-args.ts
index 23e6705a..764ac581 100644
--- a/packages/cli/src/parse-args.ts
+++ b/packages/cli/src/parse-args.ts
@@ -80,6 +80,14 @@ const onlyPublishWithReleaseLabel: AutoOption = {
   group: "main",
 };
 
+const quiet: AutoOption = {
+  name: "quiet",
+  alias: "q",
+  type: Boolean,
+  description: "Print **only** the result of the command",
+  group: "global",
+};
+
 const defaultOptions: AutoOption[] = [
   {
     name: "verbose",
@@ -215,6 +223,7 @@ const latestCommandArgs: AutoOption[] = [
   prerelease,
   changelogTitle,
   changelogCommitMessage,
+  quiet,
 ];
 
 export const commands: AutoCommand[] = [
@@ -414,6 +423,7 @@ export const commands: AutoCommand[] = [
       changelogTitle,
       changelogCommitMessage,
       baseBranch,
+      quiet,
     ],
     examples: [
       {
@@ -535,6 +545,7 @@ export const commands: AutoCommand[] = [
           "Force a canary release, even if the PR is marked to skip the release",
         config: true,
       },
+      quiet,
     ],
   },
   {
@@ -557,6 +568,7 @@ export const commands: AutoCommand[] = [
           "The message used when attaching the prerelease version to a PR",
         config: true,
       },
+      quiet,
     ],
   },
 ];
diff --git a/packages/core/src/auto-args.ts b/packages/core/src/auto-args.ts
index 72235ec7..105b2775 100644
--- a/packages/core/src/auto-args.ts
+++ b/packages/core/src/auto-args.ts
@@ -49,6 +49,11 @@ export type IVersionOptions = ReleaseCalculationOptions & {
   from?: string;
 };
 
+interface Quiet {
+  /** Print **only** the result of the command */
+  quiet?: boolean;
+}
+
 interface NoVersionPrefix {
   /** Whether to prefix the version with a "v" */
   noVersionPrefix?: boolean;
@@ -82,6 +87,7 @@ interface BaseBranch {
 export type IChangelogOptions = BaseBranch &
   ChangelogTitle &
   ChangelogMessage &
+  Quiet &
   DryRun &
   NoVersionPrefix &
   Partial<AuthorInformation> & {
@@ -124,6 +130,7 @@ export type IShipItOptions = BaseBranch &
   ChangelogMessage &
   ChangelogTitle &
   DryRun &
+  Quiet &
   Partial<AuthorInformation> &
   Partial<RepoInformation> &
   ReleaseCalculationOptions & {
@@ -136,7 +143,7 @@ export type IShipItOptions = BaseBranch &
     onlyGraduateWithReleaseLabel?: boolean;
   };
 
-export interface ICanaryOptions {
+export type ICanaryOptions = Quiet & {
   /** Do not actually do anything */
   dryRun?: boolean;
   /** THe PR to attach the canary to */
@@ -147,14 +154,14 @@ export interface ICanaryOptions {
   message?: string | "false";
   /** Always deploy a canary, even if the PR is marked as skip release */
   force?: boolean;
-}
+};
 
-export interface INextOptions {
+export type INextOptions = Quiet & {
   /** Do not actually do anything */
   dryRun?: boolean;
   /** The message used when attaching the prerelease version to a PR */
   message?: string;
-}
+};
 
 export interface IInfoOptions {
   /** List some of the available plugins */
diff --git a/packages/core/src/auto.ts b/packages/core/src/auto.ts
index 7ee59c66..67f9aa75 100644
--- a/packages/core/src/auto.ts
+++ b/packages/core/src/auto.ts
@@ -65,6 +65,7 @@ import {
 import { omit } from "./utils/omit";
 import { execSync } from "child_process";
 import isBinary from "./utils/is-binary";
+import { gitReset } from "./utils/git-reset";
 
 const proxyUrl = process.env.https_proxy || process.env.http_proxy;
 const env = envCi();
@@ -324,7 +325,9 @@ export default class Auto {
     this.options = options;
     this.baseBranch = options.baseBranch || "master";
     setLogLevel(
-      Array.isArray(options.verbose) && options.verbose.length > 1
+      "quiet" in options && options.quiet
+        ? "quiet"
+        : Array.isArray(options.verbose) && options.verbose.length > 1
         ? "veryVerbose"
         : options.verbose
         ? "verbose"
@@ -1110,10 +1113,32 @@ export default class Auto {
       canaryVersion = `${canaryVersion}.${await this.git.getSha(true)}`;
     }
 
+    canaryVersion = `canary${canaryVersion}`;
+
     if (options.dryRun) {
-      this.logger.log.warn(
-        `Published canary identifier would be: "-canary${canaryVersion}"`
-      );
+      const lastRelease = await this.git.getLatestRelease();
+      const current = await this.getCurrentVersion(lastRelease);
+
+      if (parse(current)) {
+        const next = determineNextVersion(
+          lastRelease,
+          current,
+          version,
+          canaryVersion
+        );
+
+        if (options.quiet) {
+          console.log(next);
+        } else {
+          this.logger.log.warn(`Published canary identifier would be: ${next}`);
+        }
+      } else if (options.quiet) {
+        console.log(`-${canaryVersion}`);
+      } else {
+        this.logger.log.warn(
+          `Published canary identifier would be: "-${canaryVersion}"`
+        );
+      }
     } else {
       this.logger.verbose.info("Calling canary hook");
       const result = await this.hooks.canary.promise(version, canaryVersion);
@@ -1154,7 +1179,11 @@ export default class Auto {
         `Published canary version${newVersion ? `: ${newVersion}` : ""}`
       );
 
-      await execPromise("git", ["reset", "--hard", "HEAD"]);
+      if (args.quiet) {
+        console.log(newVersion);
+      }
+
+      await gitReset();
     }
 
     let latestTag: string;
@@ -1236,18 +1265,48 @@ export default class Auto {
       calculateSemVerBump(labels, this.semVerLabels!, this.config) ||
       SEMVER.patch;
 
-    this.logger.log.info("Full Release notes for next release:");
-    console.log(fullReleaseNotes);
+    if (!args.quiet) {
+      this.logger.log.info("Full Release notes for next release:");
+      console.log(fullReleaseNotes);
 
-    if (releaseNotes) {
-      this.logger.log.info("Release notes for last change in next release");
-      console.log(releaseNotes);
+      if (releaseNotes) {
+        this.logger.log.info("Release notes for last change in next release");
+        console.log(releaseNotes);
+      }
     }
 
     if (options.dryRun) {
-      this.logger.log.success(
-        `Would have created prerelease version with: ${bump} from ${lastTag}`
-      );
+      const lastRelease = await this.git.getLatestRelease();
+      const current = await this.getCurrentVersion(lastRelease);
+
+      if (parse(current)) {
+        const prereleaseBranches = this.config?.prereleaseBranches!;
+        const branch = getCurrentBranch() || "";
+        const prereleaseBranch = prereleaseBranches.includes(branch)
+          ? branch
+          : prereleaseBranches[0];
+        const prerelease = determineNextVersion(
+          lastRelease,
+          current,
+          bump,
+          prereleaseBranch
+        );
+
+        if (options.quiet) {
+          console.log(prerelease);
+        } else {
+          this.logger.log.success(
+            `Would have created prerelease version: ${prerelease}`
+          );
+        }
+      } else if (options.quiet) {
+        // The following cases could use some work. They are really just there for lerna independent
+        console.log(`${bump} on ${lastTag}`);
+      } else {
+        this.logger.log.success(
+          `Would have created prerelease version with: ${bump} on ${lastTag}`
+        );
+      }
 
       return { newVersion: "", commitsInRelease: commits, context: "next" };
     }
@@ -1300,13 +1359,22 @@ export default class Auto {
       }
     }
 
-    await execPromise("git", ["reset", "--hard", "HEAD"]);
+    if (options.quiet) {
+      console.log(newVersion);
+    }
+
+    await gitReset();
     return { newVersion, commitsInRelease: commits, context: "next" };
   }
 
   /** Force a release to latest and bypass `shipit` safeguards. */
-  async latest(options: IShipItOptions = {}) {
-    await this.publishFullRelease(options);
+  async latest(args: IShipItOptions = {}) {
+    const options = {
+      ...(this.getCommandDefault("latest") as IShipItOptions),
+      ...args,
+    };
+
+    return this.publishFullRelease(options);
   }
 
   /**
@@ -1373,7 +1441,7 @@ export default class Auto {
     await this.hooks.beforeShipIt.promise({ releaseType });
 
     if (releaseType === "latest") {
-      publishInfo = await this.publishFullRelease(options);
+      publishInfo = await this.latest(options);
     } else if (releaseType === "old") {
       publishInfo = await this.oldRelease(options);
     } else if (releaseType === "next") {
@@ -1381,7 +1449,7 @@ export default class Auto {
     } else {
       publishInfo = await this.canary(options);
 
-      if (options.dryRun) {
+      if (options.dryRun && !options.quiet) {
         this.logger.log.success(
           "Below is what would happen upon merge of the current branch into master"
         );
@@ -1473,7 +1541,7 @@ export default class Auto {
       lastRelease
     );
 
-    await this.makeChangelog(options);
+    await this.makeChangelog({ ...options, quiet: undefined });
 
     if (!options.dryRun) {
       await this.checkClean();
@@ -1493,10 +1561,16 @@ export default class Auto {
       const current = await this.getCurrentVersion(lastRelease);
 
       if (parse(current)) {
-        this.logger.log.warn(
-          `Published version would be: ${inc(current, version as ReleaseType)}`
-        );
+        const next = inc(current, version as ReleaseType);
+
+        if (options.quiet) {
+          console.log(next);
+        } else {
+          this.logger.log.warn(`Published version would be: ${next}`);
+        }
       }
+    } else if (options.quiet) {
+      console.log(newVersion);
     }
 
     return { newVersion, commitsInRelease, context: "latest" };
@@ -1608,7 +1682,11 @@ export default class Auto {
       return;
     }
 
-    this.logger.log.info("New Release Notes\n", releaseNotes);
+    if (args.quiet) {
+      console.log(releaseNotes);
+    } else {
+      this.logger.log.info("New Release Notes\n", releaseNotes);
+    }
 
     const currentVersion = await this.getCurrentVersion(lastRelease);
 
diff --git a/packages/core/src/utils/git-reset.ts b/packages/core/src/utils/git-reset.ts
new file mode 100644
index 00000000..8d7afe6e
--- /dev/null
+++ b/packages/core/src/utils/git-reset.ts
@@ -0,0 +1,5 @@
+import execPromise from "./exec-promise";
+
+/** Reset all git changes */
+export const gitReset = async () =>
+  execPromise("git", ["reset", "--hard", "HEAD"]);
diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts
index 8ae9baf8..0c6a8008 100644
--- a/packages/core/src/utils/logger.ts
+++ b/packages/core/src/utils/logger.ts
@@ -2,7 +2,7 @@
 
 import signale from "signale";
 
-export type LogLevel = undefined | "verbose" | "veryVerbose";
+export type LogLevel = undefined | "verbose" | "veryVerbose" | "quiet";
 
 export interface ILogger {
   /** The level at which to log messages */
@@ -53,6 +53,8 @@ export function setLogLevel(newLogLevel: LogLevel) {
     toggleLogs({ log: true, verbose: true, veryVerbose: false });
   } else if (logger.logLevel === "veryVerbose") {
     toggleLogs({ log: true, verbose: true, veryVerbose: true });
+  } else if (logger.logLevel === "quiet") {
+    toggleLogs({ log: false, verbose: false, veryVerbose: false });
   } else {
     toggleLogs({ log: true, verbose: false, veryVerbose: false });
   }
diff --git a/plugins/npm/src/index.ts b/plugins/npm/src/index.ts
index 737bebb8..dc98494e 100644
--- a/plugins/npm/src/index.ts
+++ b/plugins/npm/src/index.ts
@@ -184,8 +184,6 @@ async function bumpLatest(
   return latestVersion ? inc(latestVersion, version as ReleaseType) : version;
 }
 
-const verbose = ["--loglevel", "silly"];
-
 const pluginOptions = t.partial({
   /** Whether to create sub-package changelogs */
   subPackageChangelogs: t.boolean,
@@ -433,10 +431,15 @@ export default class NPMPlugin implements IPlugin {
 
   /** Tap into auto plugin points. */
   apply(auto: Auto) {
+    const isQuiet = auto.logger.logLevel === "verbose";
     const isVerbose =
       auto.logger.logLevel === "verbose" ||
       auto.logger.logLevel === "veryVerbose";
-    const verboseArgs = isVerbose ? verbose : [];
+    const verboseArgs = isQuiet
+      ? ["-silent"]
+      : isVerbose
+      ? ["--loglevel", "silly"]
+      : [];
     const prereleaseBranches = auto.config?.prereleaseBranches!;
     const branch = getCurrentBranch();
     // if ran from master we publish the prerelease to the first
@@ -640,7 +643,7 @@ export default class NPMPlugin implements IPlugin {
       auto.logger.verbose.info("Successfully versioned repo");
     });
 
-    auto.hooks.canary.tapPromise(this.name, async (bump, postFix) => {
+    auto.hooks.canary.tapPromise(this.name, async (bump, preid) => {
       if (this.setRcToken) {
         await setTokenOnCI(auto.logger);
         auto.logger.verbose.info("Set CI NPM_TOKEN");
@@ -657,7 +660,6 @@ export default class NPMPlugin implements IPlugin {
         auto.logger.verbose.info("Detected monorepo, using lerna");
 
         const packagesBefore = await getLernaPackages();
-        const preid = `canary${postFix}`;
         const next =
           (isIndependent && `pre${bump}`) ||
           determineNextVersion(
@@ -732,7 +734,7 @@ export default class NPMPlugin implements IPlugin {
         lastRelease,
         current,
         bump,
-        `canary${postFix}`
+        preid
       );
 
       if (this.canaryScope) {


Test patch:
diff --git a/packages/core/src/__tests__/auto-canary-local.test.ts b/packages/core/src/__tests__/auto-canary-local.test.ts
index 0323c88d..1adf40e1 100644
--- a/packages/core/src/__tests__/auto-canary-local.test.ts
+++ b/packages/core/src/__tests__/auto-canary-local.test.ts
@@ -47,5 +47,5 @@ test("shipit should publish canary in locally when not on master", async () => {
   auto.hooks.canary.tap("test", canary);
 
   await auto.shipit();
-  expect(canary).toHaveBeenCalledWith(SEMVER.patch, ".abc");
+  expect(canary).toHaveBeenCalledWith(SEMVER.patch, "canary.abc");
 });
diff --git a/packages/core/src/__tests__/auto-in-pr-ci.test.ts b/packages/core/src/__tests__/auto-in-pr-ci.test.ts
index e118e2c7..5541cef3 100644
--- a/packages/core/src/__tests__/auto-in-pr-ci.test.ts
+++ b/packages/core/src/__tests__/auto-in-pr-ci.test.ts
@@ -61,7 +61,7 @@ describe("canary in ci", () => {
     jest.spyOn(auto.release!, "getCommits").mockImplementation();
 
     await auto.canary();
-    expect(canary).toHaveBeenCalledWith(SEMVER.patch, ".123.1");
+    expect(canary).toHaveBeenCalledWith(SEMVER.patch, "canary.123.1");
   });
 
   test("comments on PR in CI", async () => {
@@ -117,7 +117,7 @@ describe("canary in ci", () => {
     const addToPrBody = jest.fn();
     auto.git!.addToPrBody = addToPrBody;
     jest.spyOn(auto.release!, "getCommits").mockImplementation();
-    auto.hooks.canary.tap("test", (bump, post) => `1.2.4-canary${post}`);
+    auto.hooks.canary.tap("test", (bump, post) => `1.2.4-${post}`);
 
     await auto.canary({ pr: 123, build: 1, message: "false" });
     expect(addToPrBody).not.toHaveBeenCalled();
@@ -135,7 +135,7 @@ describe("canary in ci", () => {
     const addToPrBody = jest.fn();
     auto.git!.addToPrBody = addToPrBody;
     jest.spyOn(auto.release!, "getCommits").mockImplementation();
-    auto.hooks.canary.tap("test", (bump, post) => `1.2.4-canary${post}`);
+    auto.hooks.canary.tap("test", (bump, post) => `1.2.4-${post}`);
 
     const version = await auto.canary({ pr: 456, build: 5 });
     expect(version!.newVersion).toBe("1.2.4-canary.456.5");
@@ -159,7 +159,7 @@ describe("shipit in ci", () => {
     auto.hooks.canary.tap("test", canary);
 
     await auto.shipit();
-    expect(canary).toHaveBeenCalledWith(SEMVER.patch, ".123.1");
+    expect(canary).toHaveBeenCalledWith(SEMVER.patch, "canary.123.1");
   });
 });
 
@@ -168,7 +168,7 @@ describe("next in ci", () => {
     const auto = new Auto({ ...defaults, plugins: [] });
 
     // @ts-ignore
-    jest.spyOn(console, 'log').mockImplementation();
+    jest.spyOn(console, "log").mockImplementation();
     exec.mockResolvedValue("v1.0.0");
     // @ts-ignore
     auto.checkClean = () => Promise.resolve(true);
diff --git a/packages/core/src/__tests__/auto.test.ts b/packages/core/src/__tests__/auto.test.ts
index 44f70937..c7c039e8 100644
--- a/packages/core/src/__tests__/auto.test.ts
+++ b/packages/core/src/__tests__/auto.test.ts
@@ -8,6 +8,7 @@ import child from "child_process";
 
 const importMock = jest.fn();
 
+jest.mock("../utils/git-reset.ts");
 jest.mock("../utils/load-plugins.ts");
 jest.mock("../utils/verify-auth.ts", () => () => true);
 jest.mock("import-cwd", () => (path: string) => importMock(path));
@@ -1066,7 +1067,7 @@ describe("Auto", () => {
       jest.spyOn(auto.release!, "getCommits").mockImplementation();
 
       await auto.canary({ pr: 123, build: 1 });
-      expect(canary).toHaveBeenCalledWith(SEMVER.patch, ".123.1");
+      expect(canary).toHaveBeenCalledWith(SEMVER.patch, "canary.123.1");
       expect(auto.git!.addToPrBody).toHaveBeenCalled();
     });
 
@@ -1114,7 +1115,7 @@ describe("Auto", () => {
       jest.spyOn(auto.release!, "getCommits").mockImplementation();
 
       await auto.canary();
-      expect(canary).toHaveBeenCalledWith(SEMVER.patch, ".abc");
+      expect(canary).toHaveBeenCalledWith(SEMVER.patch, "canary.abc");
     });
 
     test("doesn't comment if there is an error", async () => {
@@ -1153,7 +1154,7 @@ describe("Auto", () => {
       auto.hooks.canary.tap("test", canary);
 
       await auto.canary();
-      expect(canary).toHaveBeenCalledWith(SEMVER.patch, ".abcd");
+      expect(canary).toHaveBeenCalledWith(SEMVER.patch, "canary.abcd");
     });
 
     test('should not publish when is present "skip-release" label', async () => {
diff --git a/plugins/npm/__tests__/npm.test.ts b/plugins/npm/__tests__/npm.test.ts
index 45dfa157..dcd749b1 100644
--- a/plugins/npm/__tests__/npm.test.ts
+++ b/plugins/npm/__tests__/npm.test.ts
@@ -696,7 +696,7 @@ describe("canary", () => {
       }
     `;
 
-    await hooks.canary.promise(Auto.SEMVER.patch, ".123.1");
+    await hooks.canary.promise(Auto.SEMVER.patch, "canary.123.1");
     expect(execPromise.mock.calls[0]).toContain("npm");
     expect(execPromise.mock.calls[0][1]).toContain("1.2.4-canary.123.1.0");
   });