Repository: intuit/auto
Pull request #820 (2019-12-19 17:57:37)

determine next version using by omitting tags from master
# What Changed

Previously to determine the last prerelease tag we were just looking for the last tag in a branch. If you merged master to your prerelease branch there was a chance that the base branch could have newer tags than in the prerelease branch.

This change makes it so we get the latest tag not in the base branch

# Why

@Aghassi's botched release

Todo:

- [x] Add tests

<!-- GITHUB_RELEASE PR BODY: canary-version -->
Published PR with canary version: <details><summary>Published under canary scope @auto-canary</summary>
- `@auto-canary/auto@8.6.3-canary.820.10894.0`
- `@auto-canary/core@8.6.3-canary.820.10894.0`
- `@auto-canary/all-contributors@8.6.3-canary.820.10894.0`
- `@auto-canary/chrome@8.6.3-canary.820.10894.0`
- `@auto-canary/conventional-commits@8.6.3-canary.820.10894.0`
- `@auto-canary/crates@8.6.3-canary.820.10894.0`
- `@auto-canary/first-time-contributor@8.6.3-canary.820.10894.0`
- `@auto-canary/git-tag@8.6.3-canary.820.10894.0`
- `@auto-canary/jira@8.6.3-canary.820.10894.0`
- `@auto-canary/maven@8.6.3-canary.820.10894.0`
- `@auto-canary/npm@8.6.3-canary.820.10894.0`
- `@auto-canary/omit-commits@8.6.3-canary.820.10894.0`
- `@auto-canary/omit-release-notes@8.6.3-canary.820.10894.0`
- `@auto-canary/released@8.6.3-canary.820.10894.0`
- `@auto-canary/s3@8.6.3-canary.820.10894.0`
- `@auto-canary/slack@8.6.3-canary.820.10894.0`
- `@auto-canary/twitter@8.6.3-canary.820.10894.0`
- `@auto-canary/upload-assets@8.6.3-canary.820.10894.0`</details>
<!-- GITHUB_RELEASE PR BODY: canary-version -->



Patch:
diff --git a/packages/core/src/auto.ts b/packages/core/src/auto.ts
index 6c7bbc44..8ddd9cd9 100644
--- a/packages/core/src/auto.ts
+++ b/packages/core/src/auto.ts
@@ -44,7 +44,7 @@ import Release, {
 import SEMVER, { calculateSemVerBump, IVersionLabels } from './semver';
 import execPromise from './utils/exec-promise';
 import loadPlugin, { IPlugin } from './utils/load-plugins';
-import createLog, { ILogger } from './utils/logger';
+import createLog, { ILogger, setLogLevel } from './utils/logger';
 import { makeHooks } from './utils/make-hooks';
 import { IAuthorOptions, IRepoOptions } from './auto-args';
 import { execSync } from 'child_process';
@@ -245,13 +245,14 @@ export default class Auto {
   constructor(options: ApiOptions = {}) {
     this.options = options;
     this.baseBranch = options.baseBranch || 'master';
-    this.logger = createLog(
+    setLogLevel(
       Array.isArray(options.verbose) && options.verbose.length > 1
         ? 'veryVerbose'
         : options.verbose
         ? 'verbose'
         : undefined
     );
+    this.logger = createLog();
     this.hooks = makeHooks();
 
     this.hooks.onCreateRelease.tap('Link onCreateChangelog', release => {
@@ -1073,6 +1074,7 @@ export default class Auto {
         repo: gitOptions.repo,
         token: gitOptions.token,
         baseUrl: gitOptions.baseUrl,
+        baseBranch: this.baseBranch,
         graphqlBaseUrl: gitOptions.graphqlBaseUrl,
         agent: gitOptions.agent
       },
diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts
index 2db44305..8d53805f 100644
--- a/packages/core/src/git.ts
+++ b/packages/core/src/git.ts
@@ -30,6 +30,8 @@ export interface IGitOptions {
   repo: string;
   /** The URL to the GitHub (public or enterprise) the project is using */
   baseUrl?: string;
+  /** The main branch of the repo. Usually master */
+  baseBranch: string;
   /** The URL to the GitHub graphql API (public or enterprise) the project is using */
   graphqlBaseUrl?: string;
   /** A token to auth to GitHub with */
@@ -730,17 +732,31 @@ export default class Git {
   }
 
   /** Get the latest tag in the git tree */
-  async getLatestTagInBranch() {
-    return execPromise('git', ['describe', '--tags', '--abbrev=0']);
+  async getLatestTagInBranch(since?: string) {
+    return execPromise('git', ['describe', '--tags', '--abbrev=0', since]);
   }
 
   /** Get the tag before latest in the git tree */
   async getPreviousTagInBranch() {
-    return execPromise('git', [
-      'describe',
-      '--tags',
-      '--abbrev=0',
-      '$(git describe --tags --abbrev=0)^1'
-    ]);
+    const latest = await this.getLatestTagInBranch();
+    return this.getLatestTagInBranch(`${latest}^1`);
+  }
+
+  /** Get all the tags for a given branch. */
+  async getTags(branch: string) {
+    const tags = await execPromise('git', ['tag', '--merged', branch]);
+
+    return tags
+      .split('\n')
+      .map(tag => tag.trim())
+      .filter(Boolean);
+  }
+
+  /** Get the last tag that isn't in the base branch */
+  async getLastTagNotInBaseBranch(branch: string) {
+    const baseTags = await this.getTags(this.options.baseBranch);
+    const branchTags = await this.getTags(branch);
+
+    return branchTags.reverse().find(tag => !baseTags.includes(tag));
   }
 }
diff --git a/packages/core/src/utils/exec-promise.ts b/packages/core/src/utils/exec-promise.ts
index 5cc66de4..764467bb 100644
--- a/packages/core/src/utils/exec-promise.ts
+++ b/packages/core/src/utils/exec-promise.ts
@@ -1,7 +1,7 @@
 import { spawn } from 'child_process';
 import createLog from './logger';
 
-const log = createLog('verbose');
+const log = createLog();
 
 /**
  * Wraps up running a command into a single promise,
diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts
index b2635c59..b4435588 100644
--- a/packages/core/src/utils/logger.ts
+++ b/packages/core/src/utils/logger.ts
@@ -1,8 +1,12 @@
-import signale, { Signale } from 'signale';
+/* eslint-disable @typescript-eslint/ban-ts-ignore */
+
+import signale from 'signale';
+
+export type LogLevel = undefined | 'verbose' | 'veryVerbose';
 
 export interface ILogger {
   /** The level at which to log messages */
-  logLevel: LogLevel;
+  logLevel?: LogLevel;
   /** The default logger. Always on */
   log: signale.Signale<signale.DefaultMethods>;
   /** The verbose log. Has more debug logs */
@@ -14,25 +18,47 @@ export interface ILogger {
 /** Create a dummy logger for testing. */
 export function dummyLog(): ILogger {
   return {
-    logLevel: undefined,
-    log: new Signale({ disabled: true }),
-    verbose: new Signale({ disabled: true }),
-    veryVerbose: new Signale({ disabled: true })
+    log: new signale.Signale({ disabled: true }),
+    verbose: new signale.Signale({ disabled: true }),
+    veryVerbose: new signale.Signale({ disabled: true })
   };
 }
 
-export type LogLevel = 'verbose' | 'veryVerbose' | undefined;
+const logger: ILogger = {
+  log: new signale.Signale(),
+  verbose: new signale.Signale(),
+  veryVerbose: new signale.Signale()
+};
+
+/** Turn the logs on an off */
+function toggleLogs(
+  options: Record<Exclude<keyof ILogger, 'logLevel'>, boolean>
+) {
+  Object.entries(options).forEach(([level, enabled]) => {
+    if (enabled) {
+      // @ts-ignore
+      logger[level].enable();
+    } else {
+      // @ts-ignore
+      logger[level].disable();
+    }
+  });
+}
+
+/** Set the log level */
+export function setLogLevel(newLogLevel: LogLevel) {
+  logger.logLevel = newLogLevel;
+
+  if (logger.logLevel === 'verbose') {
+    toggleLogs({ log: true, verbose: true, veryVerbose: false });
+  } else if (logger.logLevel === 'veryVerbose') {
+    toggleLogs({ log: true, verbose: true, veryVerbose: true });
+  } else {
+    toggleLogs({ log: true, verbose: false, veryVerbose: false });
+  }
+}
 
 /** Create a logger the the given log level. */
-export default function createLog(mode: LogLevel): ILogger {
-  return {
-    logLevel: mode,
-    log: new Signale(),
-    verbose: new Signale({
-      disabled: mode !== 'verbose' && mode !== 'veryVerbose'
-    }),
-    veryVerbose: new Signale({
-      disabled: mode !== 'veryVerbose'
-    })
-  };
+export default function createLog(): ILogger {
+  return logger;
 }
diff --git a/plugins/git-tag/src/index.ts b/plugins/git-tag/src/index.ts
index 8e039325..b534f45d 100644
--- a/plugins/git-tag/src/index.ts
+++ b/plugins/git-tag/src/index.ts
@@ -58,7 +58,9 @@ export default class GitTagPlugin implements IPlugin {
         ? branch
         : prereleaseBranches[0];
       const lastRelease = await auto.git.getLatestRelease();
-      const current = await auto.getCurrentVersion(lastRelease);
+      const current =
+        (await auto.git.getLastTagNotInBaseBranch(prereleaseBranch)) ||
+        (await auto.getCurrentVersion(lastRelease));
       const prerelease = determineNextVersion(
         lastRelease,
         current,
diff --git a/plugins/npm/src/index.ts b/plugins/npm/src/index.ts
index 750e236d..85790eba 100644
--- a/plugins/npm/src/index.ts
+++ b/plugins/npm/src/index.ts
@@ -667,8 +667,9 @@ export default class NPMPlugin implements IPlugin {
       }
 
       const lastRelease = await auto.git!.getLatestRelease();
-      const latestTag = await (auto.git?.getLatestTagInBranch() ||
-        getPreviousVersion(auto, prereleaseBranch));
+      const latestTag =
+        (await auto.git?.getLastTagNotInBaseBranch(prereleaseBranch)) ||
+        (await getPreviousVersion(auto, prereleaseBranch));
 
       if (isMonorepo()) {
         auto.logger.verbose.info('Detected monorepo, using lerna');


Test patch:
diff --git a/packages/core/src/__tests__/git.test.ts b/packages/core/src/__tests__/git.test.ts
index 8c178250..049898a1 100644
--- a/packages/core/src/__tests__/git.test.ts
+++ b/packages/core/src/__tests__/git.test.ts
@@ -89,7 +89,8 @@ jest.mock('@octokit/graphql', () => ({
 const options = {
   owner: 'Adam Dierkens',
   repo: 'test',
-  token: 'MyToken'
+  token: 'MyToken',
+  baseBranch: 'master'
 };
 
 describe('github', () => {
@@ -186,6 +187,29 @@ describe('github', () => {
     expect(await gh.getLatestTagInBranch()).toBeDefined();
   });
 
+  test('getTags', async () => {
+    const gh = new Git(options);
+
+    expect(Array.isArray(await gh.getTags('master'))).toBe(true);
+  });
+
+  test('getLastTagNotInBaseBranch', async () => {
+    const getTags = jest.fn();
+    const gh = new Git(options);
+    gh.getTags = getTags;
+
+    getTags.mockReturnValueOnce(['0.1.0', '0.2.0', '0.3.0']);
+    getTags.mockReturnValueOnce([
+      '0.1.0',
+      '0.2.0',
+      '0.4.0-alpha.0',
+      '0.4.0-alpha.1',
+      '0.3.0'
+    ]);
+
+    expect(await gh.getLastTagNotInBaseBranch('alpha')).toBe('0.4.0-alpha.1');
+  });
+
   test('getGitLog ', async () => {
     const gh = new Git(options);
 
diff --git a/packages/core/src/__tests__/release.test.ts b/packages/core/src/__tests__/release.test.ts
index b76172a5..cff7b1da 100644
--- a/packages/core/src/__tests__/release.test.ts
+++ b/packages/core/src/__tests__/release.test.ts
@@ -121,7 +121,8 @@ const logParse = new LogParse();
 const git = new Git({
   owner: 'Andrew',
   repo: 'test',
-  token: 'MY_TOKEN'
+  token: 'MY_TOKEN',
+  baseBranch: 'master'
 });
 
 describe('getVersionMap', () => {
diff --git a/plugins/git-tag/__tests__/git-tag.test.ts b/plugins/git-tag/__tests__/git-tag.test.ts
index d1d4eee4..1c2fbc3e 100644
--- a/plugins/git-tag/__tests__/git-tag.test.ts
+++ b/plugins/git-tag/__tests__/git-tag.test.ts
@@ -69,7 +69,10 @@ describe('Git Tag Plugin', () => {
     });
 
     test('should tag next version', async () => {
-      const hooks = setup({ getLatestRelease: () => 'v1.0.0' });
+      const hooks = setup({
+        getLatestRelease: () => 'v0.1.0',
+        getLastTagNotInBaseBranch: () => 'v1.0.0'
+      });
 
       await hooks.next.promise([], Auto.SEMVER.patch);
 
diff --git a/plugins/npm/__tests__/npm.test.ts b/plugins/npm/__tests__/npm.test.ts
index ec99418d..a23ba96e 100644
--- a/plugins/npm/__tests__/npm.test.ts
+++ b/plugins/npm/__tests__/npm.test.ts
@@ -839,8 +839,8 @@ describe('next', () => {
       getCurrentVersion: () => '1.2.3',
       prefixRelease: (v: string) => v,
       git: {
-        getLatestRelease: () => '1.2.3',
-        getLatestTagInBranch: () => '1.2.3'
+        getLatestRelease: () => '1.0.0',
+        getLastTagNotInBaseBranch: () => '1.2.3'
       }
     } as unknown) as Auto.Auto);
 
@@ -883,8 +883,8 @@ describe('next', () => {
       getCurrentVersion: () => '1.2.3',
       prefixRelease: (v: string) => v,
       git: {
-        getLatestRelease: () => '1.2.3',
-        getLatestTagInBranch: () => '1.2.3'
+        getLatestRelease: () => '1.0.0',
+        getLastTagNotInBaseBranch: () => '1.2.3'
       }
     } as unknown) as Auto.Auto);
 
@@ -916,8 +916,8 @@ describe('next', () => {
       logger: dummyLog(),
       prefixRelease: (v: string) => v,
       git: {
-        getLatestRelease: () => '@foo/1@1.0.0-next.0',
-        getLatestTagInBranch: () => '@foo/1@1.0.0-next.0'
+        getLatestRelease: () => '@foo/1@0.1.0',
+        getLastTagNotInBaseBranch: () => '@foo/1@1.0.0-next.0'
       }
     } as unknown) as Auto.Auto);
 
diff --git a/plugins/released/__tests__/released-label.test.ts b/plugins/released/__tests__/released-label.test.ts
index 91dedbb2..10886468 100644
--- a/plugins/released/__tests__/released-label.test.ts
+++ b/plugins/released/__tests__/released-label.test.ts
@@ -11,7 +11,7 @@ import {
 
 import ReleasedLabelPlugin from '../src';
 
-const git = new Git({ owner: '1', repo: '2' });
+const git = new Git({ owner: '1', repo: '2', baseBranch: 'master' });
 const log = new LogParse();
 
 const comment = jest.fn();