From f882009dfc93a18f29ca23d95767c9ba250c31b0 Mon Sep 17 00:00:00 2001 From: Edouard Bozon Date: Tue, 23 Mar 2021 17:04:39 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9E=20release=20since=20first?= =?UTF-8?q?=20commit=20if=20no=20version=20found=20(resolve=20#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Younes Jaaidi --- .../src/builders/version/builder.e2e.spec.ts | 6 +- .../src/builders/version/builder.spec.ts | 5 +- .../semver/src/builders/version/testing.ts | 2 +- .../version/utils/get-current-version.spec.ts | 58 ------------------- .../version/utils/get-current-version.ts | 58 ------------------- .../version/utils/get-last-version.spec.ts | 34 +++++++++++ .../version/utils/get-last-version.ts | 25 ++++++++ .../src/builders/version/utils/git.spec.ts | 32 +--------- .../semver/src/builders/version/utils/git.ts | 8 --- .../builders/version/utils/try-bump.spec.ts | 20 +++---- .../src/builders/version/utils/try-bump.ts | 45 ++++++++------ 11 files changed, 105 insertions(+), 188 deletions(-) delete mode 100644 packages/semver/src/builders/version/utils/get-current-version.spec.ts delete mode 100644 packages/semver/src/builders/version/utils/get-current-version.ts create mode 100644 packages/semver/src/builders/version/utils/get-last-version.spec.ts create mode 100644 packages/semver/src/builders/version/utils/get-last-version.ts diff --git a/packages/semver/src/builders/version/builder.e2e.spec.ts b/packages/semver/src/builders/version/builder.e2e.spec.ts index 95b19f6cf..e1e9365a6 100644 --- a/packages/semver/src/builders/version/builder.e2e.spec.ts +++ b/packages/semver/src/builders/version/builder.e2e.spec.ts @@ -636,15 +636,15 @@ $`) function commitChanges() { execSync( ` - git init; + git init # These are needed by CI. git config user.email "bot@jest.io" git config user.name "Test Bot" git config commit.gpgsign false - git add .; - git commit -m "🐣"; + git add . + git commit -m "🐣" echo a > packages/a/a.txt git add . git commit -m "feat(a): 🚀 new feature" diff --git a/packages/semver/src/builders/version/builder.spec.ts b/packages/semver/src/builders/version/builder.spec.ts index fab7e0647..e124f1e46 100644 --- a/packages/semver/src/builders/version/builder.spec.ts +++ b/packages/semver/src/builders/version/builder.spec.ts @@ -360,7 +360,8 @@ describe('@jscutlery/semver:version', () => { }); it('should abort when validation hook failed', async () => { - mockValidateA.mockRejectedValue(new Error('Validation failure')); + context.logger.error = jest.fn(); + mockValidateA.mockRejectedValue('Validation failure'); const { success } = await runBuilder( { ...options, plugins: ['@mock-plugin/A', '@mock-plugin/B'] }, @@ -372,7 +373,7 @@ describe('@jscutlery/semver:version', () => { expect(mockPublishA).not.toBeCalled(); expect(mockPublishB).not.toBeCalled(); expect(context.logger.error).toBeCalledWith( - expect.stringContaining('Error: Validation failure') + expect.stringContaining('Validation failure') ); }); diff --git a/packages/semver/src/builders/version/testing.ts b/packages/semver/src/builders/version/testing.ts index ad4385a22..f62bf8864 100644 --- a/packages/semver/src/builders/version/testing.ts +++ b/packages/semver/src/builders/version/testing.ts @@ -48,7 +48,7 @@ export function setupTestingWorkspace( export function createFakeLogger(): logging.LoggerApi { return { - error: jest.fn(), + error: jest.fn((e) => console.error(e)), info: jest.fn(), warn: jest.fn(), createChild: jest.fn(), diff --git a/packages/semver/src/builders/version/utils/get-current-version.spec.ts b/packages/semver/src/builders/version/utils/get-current-version.spec.ts deleted file mode 100644 index 9255a37a6..000000000 --- a/packages/semver/src/builders/version/utils/get-current-version.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { logging } from '@angular-devkit/core'; -import * as gitSemverTags from 'git-semver-tags'; -import { of, throwError } from 'rxjs'; -import { callbackify } from 'util'; - -import { createFakeLogger } from '../testing'; -import { getCurrentVersion } from './get-current-version'; -import * as gitUtils from './git'; - -jest.mock('git-semver-tags', () => jest.fn()); -jest.mock('./project'); -jest.mock('./git'); - -const tagPrefix = 'v'; - -describe('getCurrentVersion', () => { - let mockGitSemverTags: jest.Mock; - let logger: logging.LoggerApi; - - beforeEach(() => { - mockGitSemverTags = jest.fn(); - (gitSemverTags as jest.Mock).mockImplementation( - callbackify(mockGitSemverTags) - ); - logger = createFakeLogger(); - }); - - it('should compute current version from previous semver tag', async () => { - mockGitSemverTags.mockResolvedValue(['v2.1.0', 'v2.0.0', 'v1.0.0']); - - const version = await getCurrentVersion({ tagPrefix, logger }).toPromise(); - - expect(logger.warn).not.toBeCalled(); - expect(version).toEqual('v2.1.0'); - }); - - it('should compute current version from last tag if no semver tag was found', async () => { - mockGitSemverTags.mockResolvedValue([]); - jest.spyOn(gitUtils, 'getLastTag').mockReturnValue(of('v2.1.0')); - - const version = await getCurrentVersion({ tagPrefix, logger }).toPromise(); - - expect(logger.warn).toBeCalled(); - expect(version).toEqual('v2.1.0'); - }); - - it('should default to 0.0.0', async () => { - mockGitSemverTags.mockResolvedValue([]); - jest - .spyOn(gitUtils, 'getLastTag') - .mockReturnValue(throwError('No tag found')); - - const version = await getCurrentVersion({ tagPrefix, logger }).toPromise(); - - expect(logger.warn).toBeCalled(); - expect(version).toEqual('0.0.0'); - }); -}); diff --git a/packages/semver/src/builders/version/utils/get-current-version.ts b/packages/semver/src/builders/version/utils/get-current-version.ts deleted file mode 100644 index 22a536d3e..000000000 --- a/packages/semver/src/builders/version/utils/get-current-version.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { logging } from '@angular-devkit/core'; -import * as gitSemverTags from 'git-semver-tags'; -import { from, Observable, of, throwError } from 'rxjs'; -import { catchError, switchMap, tap } from 'rxjs/operators'; -import * as semver from 'semver'; -import { promisify } from 'util'; - -import { getLastTag } from './git'; - -export function getLastSemverTag({ - tagPrefix, -}: { - tagPrefix: string; -}): Observable { - return from(promisify(gitSemverTags)({ tagPrefix })).pipe( - switchMap((tags: string[]) => { - const [version] = tags.sort(semver.rcompare); - - if (version == null) { - return throwError(new Error('No semver tag found')); - } - - return of(version); - }) - ); -} - -/** - * Returns a valid git tag that we can use as a ref for version comparison. - * Otherwise it returns '0.0.0'. - */ -export function getCurrentVersion({ - logger, - tagPrefix, -}: { - logger: logging.LoggerApi; - tagPrefix?: string; -}): Observable { - /* Get last semver tags. */ - return getLastSemverTag({ tagPrefix }).pipe( - /* Fallback to last Git tag. */ - catchError(() => - getLastTag().pipe( - tap((tag) => { - logger.warn(`🟠 No previous semver tag found, fallback from: ${tag}`); - }) - ) - ), - - /* Fallback to 0.0.0 */ - catchError(() => { - logger.warn( - '🟠 No previous tag found, fallback to version 0.0.0' - ); - return of('0.0.0'); - }) - ); -} diff --git a/packages/semver/src/builders/version/utils/get-last-version.spec.ts b/packages/semver/src/builders/version/utils/get-last-version.spec.ts new file mode 100644 index 000000000..2c2c81c90 --- /dev/null +++ b/packages/semver/src/builders/version/utils/get-last-version.spec.ts @@ -0,0 +1,34 @@ +import * as gitSemverTags from 'git-semver-tags'; +import { callbackify } from 'util'; + +import { getLastVersion } from './get-last-version'; + +jest.mock('git-semver-tags', () => jest.fn()); +jest.mock('./project'); + +const tagPrefix = 'my-lib-'; + +describe(getLastVersion.name, () => { + let mockGitSemverTags: jest.Mock; + + beforeEach(() => { + mockGitSemverTags = jest.fn(); + (gitSemverTags as jest.Mock).mockImplementation( + callbackify(mockGitSemverTags) + ); + }); + + it('should compute current version from previous semver tag', async () => { + mockGitSemverTags.mockResolvedValue(['my-lib-2.1.0', 'my-lib-2.0.0', 'my-lib-1.0.0']); + + const tag = await getLastVersion({ tagPrefix }).toPromise(); + + expect(tag).toEqual('2.1.0'); + }); + + it('should throw error if no tag available', async () => { + mockGitSemverTags.mockResolvedValue([]); + + expect(getLastVersion({ tagPrefix }).toPromise()).rejects.toThrow('No semver tag found'); + }); +}); diff --git a/packages/semver/src/builders/version/utils/get-last-version.ts b/packages/semver/src/builders/version/utils/get-last-version.ts new file mode 100644 index 000000000..a68132cc8 --- /dev/null +++ b/packages/semver/src/builders/version/utils/get-last-version.ts @@ -0,0 +1,25 @@ +import * as gitSemverTags from 'git-semver-tags'; +import { from, Observable, of, throwError } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import * as semver from 'semver'; +import { promisify } from 'util'; + +export function getLastVersion({ + tagPrefix, +}: { + tagPrefix: string; +}): Observable { + return from(promisify(gitSemverTags)({ tagPrefix })).pipe( + switchMap((tags: string[]) => { + const versions = tags.map(tag => tag.substring(tagPrefix.length)); + const [version] = versions.sort(semver.rcompare); + + if (version == null) { + return throwError(new Error('No semver tag found')); + } + + const tag =`${tagPrefix}${version}`; + return of(tag.substring(tagPrefix.length)); + }) + ); +} diff --git a/packages/semver/src/builders/version/utils/git.spec.ts b/packages/semver/src/builders/version/utils/git.spec.ts index 40e747930..282a8ffa5 100644 --- a/packages/semver/src/builders/version/utils/git.spec.ts +++ b/packages/semver/src/builders/version/utils/git.spec.ts @@ -7,7 +7,6 @@ import { addToStage, getCommits, getFirstCommitRef, - getLastTag, tryPushToGitRemote, } from './git'; @@ -175,36 +174,7 @@ describe('git', () => { }); }); - describe('getLastTag', () => { - it('should get last git commit', async () => { - jest - .spyOn(cp, 'execAsync') - .mockReturnValue(of({ stderr: '', stdout: '0.0.0' })); - - const tag = await getLastTag().toPromise(); - - expect(tag).toBe('0.0.0'); - expect(cp.execAsync).toBeCalledWith( - 'git', - expect.arrayContaining(['describe', '--tags', '--abbrev=0']) - ); - }); - - it('should throw on failure', async () => { - jest - .spyOn(cp, 'execAsync') - .mockReturnValue(of({ stderr: 'error', stdout: '' })); - - try { - await getLastTag().toPromise(); - fail(); - } catch (error) { - expect(error.message).toBe('No tag found'); - } - }); - }); - - describe('getFirstCommitRef', () => { + describe('getFirstCommitRef', () => { it('should get last git commit', async () => { jest .spyOn(cp, 'execAsync') diff --git a/packages/semver/src/builders/version/utils/git.ts b/packages/semver/src/builders/version/utils/git.ts index 94c1fc0a0..568c799fc 100644 --- a/packages/semver/src/builders/version/utils/git.ts +++ b/packages/semver/src/builders/version/utils/git.ts @@ -93,14 +93,6 @@ export function addToStage({ }); } -export function getLastTag(): Observable { - return execAsync('git', ['describe', '--tags', '--abbrev=0']).pipe( - switchMap(({ stdout }) => - stdout ? of(stdout) : throwError(new Error('No tag found')) - ) - ); -} - export function getFirstCommitRef(): Observable { return execAsync('git', ['rev-list', '--max-parents=0', 'HEAD']).pipe( /** Remove line breaks. */ diff --git a/packages/semver/src/builders/version/utils/try-bump.spec.ts b/packages/semver/src/builders/version/utils/try-bump.spec.ts index 9354492ce..09a9909d4 100644 --- a/packages/semver/src/builders/version/utils/try-bump.spec.ts +++ b/packages/semver/src/builders/version/utils/try-bump.spec.ts @@ -1,23 +1,23 @@ import { logging } from '@angular-devkit/core'; import * as conventionalRecommendedBump from 'conventional-recommended-bump'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { callbackify } from 'util'; import { createFakeLogger } from '../testing'; -import { getCurrentVersion } from './get-current-version'; +import { getLastVersion } from './get-last-version'; import { getCommits, getFirstCommitRef } from './git'; import { tryBump } from './try-bump'; jest.mock('conventional-recommended-bump'); -jest.mock('./get-current-version'); +jest.mock('./get-last-version'); jest.mock('./git'); describe('tryBump', () => { const mockConventionalRecommendedBump = conventionalRecommendedBump as jest.MockedFunction< typeof conventionalRecommendedBump >; - const mockCurrentVersion = getCurrentVersion as jest.MockedFunction< - typeof getCurrentVersion + const mockGetLastVersion = getLastVersion as jest.MockedFunction< + typeof getLastVersion >; const mockGetCommits = getCommits as jest.MockedFunction; const mockGetFirstCommitRef = getFirstCommitRef as jest.MockedFunction; @@ -26,11 +26,11 @@ describe('tryBump', () => { beforeEach(() => { logger = createFakeLogger(); - mockCurrentVersion.mockReturnValue(of('v2.1.0')) + mockGetLastVersion.mockReturnValue(of('2.1.0')) }); afterEach(() => { - mockCurrentVersion.mockRestore(); + mockGetLastVersion.mockRestore(); mockConventionalRecommendedBump.mockRestore(); mockGetCommits.mockRestore(); mockGetFirstCommitRef.mockRestore(); @@ -38,7 +38,7 @@ describe('tryBump', () => { afterEach(() => (getCommits as jest.Mock).mockRestore()); - it('should compute next version based on current version and changes', async () => { + it('should compute next version based on last version and changes', async () => { mockGetCommits.mockReturnValue(of(['feat: A', 'feat: B'])); /* Mock bump to return "minor". */ mockConventionalRecommendedBump.mockImplementation( @@ -112,7 +112,7 @@ describe('tryBump', () => { }); it('should call getFirstCommitRef if version is 0.0.0', async () => { - mockCurrentVersion.mockReturnValue(of('0.0.0')); + mockGetLastVersion.mockReturnValue(throwError('No version found')); mockGetCommits.mockReturnValue(of([])); mockGetFirstCommitRef.mockReturnValue(of('sha1')); @@ -125,7 +125,7 @@ describe('tryBump', () => { logger, }).toPromise(); - + expect(logger.warn).toBeCalledWith(expect.stringContaining('No previous version tag found')) expect(mockGetCommits).toBeCalledTimes(1); expect(mockGetCommits).toBeCalledWith({ projectRoot: '/libs/demo', diff --git a/packages/semver/src/builders/version/utils/try-bump.ts b/packages/semver/src/builders/version/utils/try-bump.ts index a0d68129a..d1bc3796e 100644 --- a/packages/semver/src/builders/version/utils/try-bump.ts +++ b/packages/semver/src/builders/version/utils/try-bump.ts @@ -1,11 +1,11 @@ import { logging } from '@angular-devkit/core'; import * as conventionalRecommendedBump from 'conventional-recommended-bump'; import { defer, forkJoin, iif, Observable, of } from 'rxjs'; -import { shareReplay, switchMap } from 'rxjs/operators'; +import { catchError, shareReplay, switchMap } from 'rxjs/operators'; import * as semver from 'semver'; import { promisify } from 'util'; -import { getCurrentVersion } from './get-current-version'; +import { getLastVersion } from './get-last-version'; import { getCommits, getFirstCommitRef } from './git'; /** @@ -26,45 +26,56 @@ export function tryBump({ preid: string | null; logger: logging.LoggerApi; }): Observable { - const since$ = getCurrentVersion({ - logger, - tagPrefix, - }).pipe( + const initialVersion = '0.0.0'; + const lastVersion$ = getLastVersion({ tagPrefix }).pipe( + catchError(() => { + logger.warn( + `🟠 No previous version tag found, fallback to version 0.0.0. +New version will be calculated based on all changes since first commit. +If your project is already versioned, please tag the latest release commit with ${tagPrefix}-x.y.z and run this command again.` + ); + return of(initialVersion); + }), shareReplay({ refCount: true, bufferSize: 1, }) ); - const sinceGitRef$ = since$.pipe( + const lastVersionGitRef$ = lastVersion$.pipe( /** If since equals 0.0.0 it means no tag exist, - * then get the first commit ref to compute the initial version. */ - switchMap((since) => - iif(() => since === `0.0.0`, getFirstCommitRef(), of(since)) + * then get the first commit ref to compute the initial version. */ + switchMap((lastVersion) => + iif(() => lastVersion === initialVersion, getFirstCommitRef(), of(`${tagPrefix}${lastVersion}`)) ) ); - const commits$ = sinceGitRef$.pipe( - switchMap((since) => + const commits$ = lastVersionGitRef$.pipe( + switchMap((lastVersionGitRef) => getCommits({ projectRoot, - since, + since: lastVersionGitRef, }) ) ); - return forkJoin([since$, commits$]).pipe( - switchMap(([since, commits]) => { + return forkJoin([lastVersion$, commits$]).pipe( + switchMap(([lastVersion, commits]) => { /* No commits since last release so don't bump. */ if (commits.length === 0) { return of(null); } if (releaseType !== null) { - return _manualBump({ since, releaseType, preid }); + return _manualBump({ since: lastVersion, releaseType, preid }); } - return _semverBump({ since, preset, projectRoot, tagPrefix }); + return _semverBump({ + since: lastVersion, + preset, + projectRoot, + tagPrefix, + }); }) ); }