From e99fba3b66a1ecba55bd3584e141ce5bca5de2ed Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Fri, 27 Oct 2017 01:21:17 -0400 Subject: [PATCH] feat: Use `commits`, `lastRelease` and `nextRelease` passed by `semantic-release` Use the same `commits`, `lastRelease` and `nextRelease` as other `semantic-release` plugin, instead of retrieving them locally with `conventional-commits-core`. That avoid discrepancy in the determination of next version, last release and commits to include in release note. BREAKING CHANGE: Expect to be passed `commits`, `lastRelease` and `nextRelease` in `options`. Require `semantic-remantic` `>=9.0.0`. BREAKING CHANGE: Do not wrap unexpected errors in a `@semantic-release/error`. This way, in case of unexpected error (missing preset for example) `semantic-release` will fail and return with an error code --- lib/index.js | 50 ++++--- lib/load/changelog-config.js | 13 +- package.json | 12 +- test/helpers/commits.js | 22 --- test/integration.test.js | 209 +++++++++++++++++++---------- test/load-changelog-config.test.js | 17 +-- 6 files changed, 180 insertions(+), 143 deletions(-) delete mode 100644 test/helpers/commits.js diff --git a/lib/index.js b/lib/index.js index d449a5ec..93410535 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,14 +1,12 @@ +const {callbackify} = require('util'); +const url = require('url'); const getStream = require('get-stream'); -const SemanticReleaseError = require('@semantic-release/error'); -const conventionalChangelog = require('conventional-changelog-core'); +const intoStream = require('into-stream'); +const hostedGitInfo = require('hosted-git-info'); +const conventionalCommitsParser = require('conventional-commits-parser').sync; +const conventionalChangelogWriter = require('conventional-changelog-writer'); const loadChangelogConfig = require('./load/changelog-config'); -/** - * @callback releaseNotesGeneratorCallback - * @param {Error} error error object. - * @param {string} changelog changelog generated by the plugin. - */ - /** * Generate the changelog for all the commits since the last release. * @@ -18,14 +16,32 @@ const loadChangelogConfig = require('./load/changelog-config'); * @param {Object} pluginConfig.parserOpts additional `conventional-changelog-parser` options that will overwrite ones loaded by `preset` or `config`. * @param {Object} pluginConfig.writerOpts additional `conventional-changelog-writer` options that will overwrite ones loaded by `preset` or `config`. * @param {Object} options semantic-release options - * @param {releaseNotesGeneratorCallback} callback The callback called with the release note. + * @param {Object} options.pkg normalized `package.json` + * @param {Array} options.commits array of commits, each containing `hash` and `message` + * @param {Object>} options.lastRelease last release with `gitHead` corresponding to the commit hash used to make the last release + * @param {Object>} options.nextRelease next release with `gitHead` corresponding to the commit hash used to make the release and the release `version` */ -module.exports = async (pluginConfig = {}, options, callback) => { - const cb = typeof options === 'function' ? options : callback; +async function releaseNotesGenerator( + pluginConfig, + {pkg, commits, lastRelease: {gitHead: previousTag}, nextRelease: {gitHead: currentTag, version}} +) { + const {parserOpts, writerOpts} = await loadChangelogConfig(pluginConfig); + commits = commits.map(rawCommit => + Object.assign(rawCommit, conventionalCommitsParser(rawCommit.message, parserOpts)) + ); + const {default: protocol, domain: host, project: repository, user: owner} = hostedGitInfo.fromUrl(pkg.repository.url); + const context = { + version, + host: url.format({protocol, host}), + owner, + repository, + previousTag, + currentTag, + linkCompare: currentTag && previousTag, + packageData: pkg, + }; + + return getStream(intoStream.obj(commits).pipe(conventionalChangelogWriter(context, writerOpts))); +} - try { - cb(null, await getStream(conventionalChangelog({config: await loadChangelogConfig(pluginConfig)}))); - } catch (err) { - cb(new SemanticReleaseError(`Error in conventional-changelog: ${err.message}`, err.code)); - } -}; +module.exports = callbackify(releaseNotesGenerator); diff --git a/lib/load/changelog-config.js b/lib/load/changelog-config.js index 9a20661e..2a087a9a 100644 --- a/lib/load/changelog-config.js +++ b/lib/load/changelog-config.js @@ -1,7 +1,6 @@ const importFrom = require('import-from'); const pify = require('pify'); const {mergeWith} = require('lodash'); -const SemanticReleaseError = require('@semantic-release/error'); const conventionalChangelogAngular = require('conventional-changelog-angular'); /** @@ -18,17 +17,9 @@ module.exports = async ({preset, config, parserOpts, writerOpts}) => { if (preset) { const presetPackage = `conventional-changelog-${preset.toLowerCase()}`; - try { - loadedConfig = importFrom.silent(__dirname, presetPackage) || importFrom(process.cwd(), presetPackage); - } catch (err) { - throw new SemanticReleaseError(`Preset: "${preset}" does not exist: ${err.message}`, err.code); - } + loadedConfig = importFrom.silent(__dirname, presetPackage) || importFrom(process.cwd(), presetPackage); } else if (config) { - try { - loadedConfig = importFrom.silent(__dirname, config) || importFrom(process.cwd(), config); - } catch (err) { - throw new SemanticReleaseError(`Config: "${config}" does not exist: ${err.message}`, err.code); - } + loadedConfig = importFrom.silent(__dirname, config) || importFrom(process.cwd(), config); } else if (!parserOpts || !writerOpts) { loadedConfig = conventionalChangelogAngular; } diff --git a/package.json b/package.json index 8779028a..c5a3f190 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,13 @@ } }, "dependencies": { - "@semantic-release/error": "^2.0.0", "conventional-changelog-angular": "^1.4.0", - "conventional-changelog-core": "^1.9.0", + "conventional-changelog-writer": "^2.0.1", + "conventional-commits-parser": "^2.0.0", "get-stream": "^3.0.0", + "hosted-git-info": "^2.5.0", "import-from": "^2.1.0", + "into-stream": "^3.1.0", "lodash": "^4.17.4", "pify": "^3.0.0" }, @@ -43,14 +45,10 @@ "eslint-plugin-prettier": "^2.3.0", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^3.0.1", - "execa": "^0.8.0", - "fs-extra": "^4.0.1", "nyc": "^11.1.0", - "p-each-series": "^1.0.0", "prettier": "^1.7.2", "rimraf": "^2.6.1", - "semantic-release": "^8.0.0", - "tempy": "^0.2.0" + "semantic-release": "^8.0.0" }, "engines": { "node": ">=4" diff --git a/test/helpers/commits.js b/test/helpers/commits.js deleted file mode 100644 index 19326281..00000000 --- a/test/helpers/commits.js +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import tempy from 'tempy'; -import execa from 'execa'; -import pEachSeries from 'p-each-series'; - -/** - * Create a temporary git repository with commits. - * - * @method commits - * @param {Array} messages the commit message (1 commit per message). - */ -export default async function commits(messages) { - const dir = tempy.directory(); - - fs.symlink(path.resolve('./node_modules'), path.join(dir, 'node_modules')); - process.chdir(dir); - await fs.mkdir('git-templates'); - await execa('git', ['init', '--template=./git-templates']); - - await pEachSeries(messages, message => execa('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'])); -} diff --git a/test/integration.test.js b/test/integration.test.js index 9a57eeac..d0285093 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,91 +1,151 @@ import test from 'ava'; import pify from 'pify'; -import SemanticReleaseError from '@semantic-release/error'; import releaseNotesGenerator from '../lib/index'; -import commits from './helpers/commits'; -const cwd = process.cwd(); +const url = 'https://github.com/owner/repo'; +const lastRelease = {gitHead: '123'}; +const nextRelease = {gitHead: '456', version: '1.0.0'}; -test.afterEach.always(() => { - process.chdir(cwd); -}); - -test.serial('Use "conventional-changelog-angular" by default', async t => { - await commits(['fix(scope1): First fix', 'feat(scope2): Second feature']); - const changelog = await pify(releaseNotesGenerator)({}); +test('Use "conventional-changelog-angular" by default', async t => { + const commits = [ + {hash: '111', message: 'fix(scope1): First fix'}, + {hash: '222', message: 'feat(scope2): Second feature'}, + ]; + const changelog = await pify(releaseNotesGenerator)( + {}, + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ); + t.regex(changelog, new RegExp(``)); + t.regex(changelog, new RegExp(`\\(https://github.com/owner/repo/compare/123\\.\\.\\.456\\)`)); t.regex(changelog, /### Bug Fixes/); - t.regex(changelog, /\* \*\*scope1:\*\* First fix/); + t.regex( + changelog, + new RegExp(`scope1:.*First fix \\(\\[111\\]\\(https://github.com/owner/repo\\/commits\\/111\\)\\)`) + ); t.regex(changelog, /### Features/); - t.regex(changelog, /\* \*\*scope2:\*\* Second feature/); + t.regex( + changelog, + new RegExp(`scope2:.*Second feature \\(\\[222\\]\\(https://github.com/owner/repo\\/commits\\/222\\)\\)`) + ); }); -test.serial('Accept a "preset" option', async t => { - await commits(['Fix: First fix (fixes #123)', 'Update: Second feature (fixes #456)']); - const changelog = await pify(releaseNotesGenerator)({preset: 'eslint'}); +test('Accept a "preset" option', async t => { + const commits = [ + {hash: '111', message: 'Fix: First fix (fixes #123)'}, + {hash: '222', message: 'Update: Second feature (fixes #456)'}, + ]; + const changelog = await pify(releaseNotesGenerator)( + {preset: 'eslint'}, + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ); + t.regex(changelog, new RegExp(``)); + t.regex(changelog, new RegExp(`\\(https://github.com/owner/repo/compare/123\\.\\.\\.456\\)`)); t.regex(changelog, /### Fix/); - t.regex(changelog, /\* First fix .*, closes #123/); + t.regex( + changelog, + new RegExp( + `First fix .* \\(\\[111\\]\\(https://github.com/owner/repo\\/commits\\/111\\)\\), closes \\[#123\\]\\(https://github.com/owner/repo\\/issues\\/123\\)` + ) + ); t.regex(changelog, /### Update/); - t.regex(changelog, /\* Second feature .*, closes #456/); + t.regex( + changelog, + new RegExp( + `Second feature .* \\(\\[222\\]\\(https://github.com/owner/repo\\/commits\\/222\\)\\), closes \\[#456\\]\\(https://github.com/owner/repo\\/issues\\/456\\)` + ) + ); }); test.serial('Accept a "config" option', async t => { - await commits(['Fix: First fix (fixes #123)', 'Update: Second feature (fixes #456)']); - const changelog = await pify(releaseNotesGenerator)({config: 'conventional-changelog-eslint'}); - - t.regex(changelog, /### Fix/); - t.regex(changelog, /\* First fix .*, closes #123/); - t.regex(changelog, /### Update/); - t.regex(changelog, /\* Second feature .*, closes #456/); -}); - -test.serial('Accept an additionnal argument', async t => { - await commits(['Fix: First fix (fixes #123)', 'Update: Second feature (fixes #456)']); - const changelog = await pify(releaseNotesGenerator)({preset: 'eslint'}, {}); + const commits = [ + {hash: '111', message: 'Fix: First fix (fixes #123)'}, + {hash: '222', message: 'Update: Second feature (fixes #456)'}, + ]; + const changelog = await pify(releaseNotesGenerator)( + {config: 'conventional-changelog-eslint'}, + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ); + t.regex(changelog, new RegExp(``)); + t.regex(changelog, new RegExp(`\\(https://github.com/owner/repo/compare/123\\.\\.\\.456\\)`)); t.regex(changelog, /### Fix/); - t.regex(changelog, /\* First fix .*, closes #123/); + t.regex( + changelog, + new RegExp( + `First fix .* \\(\\[111\\]\\(https://github.com/owner/repo\\/commits\\/111\\)\\), closes \\[#123\\]\\(https://github.com/owner/repo\\/issues\\/123\\)` + ) + ); t.regex(changelog, /### Update/); - t.regex(changelog, /\* Second feature .*, closes #456/); + t.regex( + changelog, + new RegExp( + `Second feature .* \\(\\[222\\]\\(https://github.com/owner/repo\\/commits\\/222\\)\\), closes \\[#456\\]\\(https://github.com/owner/repo\\/issues\\/456\\)` + ) + ); }); -test.serial('Accept a "parseOpts" and "writerOpts" objects as option', async t => { - const eslintChangelogConfig = await pify(require('conventional-changelog-eslint'))(); - - await commits(['##Fix## First fix (fixes #123)', '##Update## Second feature (fixes #456)']); +test('Accept a "parseOpts" and "writerOpts" objects as option', async t => { + const commits = [ + {hash: '111', message: '##Fix## First fix (fixes #123)'}, + {hash: '222', message: '##Update## Second feature (fixes #456)'}, + ]; const changelog = await pify(releaseNotesGenerator)( { parserOpts: {headerPattern: /^##(.*?)## (.*)$/, headerCorrespondence: ['tag', 'message']}, - writerOpts: eslintChangelogConfig.writerOpts, + writerOpts: (await pify(require('conventional-changelog-eslint'))()).writerOpts, }, - {} + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} ); + t.regex(changelog, new RegExp(``)); + t.regex(changelog, new RegExp(`\\(https://github.com/owner/repo/compare/123\\.\\.\\.456\\)`)); t.regex(changelog, /### Fix/); - t.regex(changelog, /\* First fix .*, closes #123/); + t.regex( + changelog, + new RegExp( + `First fix .* \\(\\[111\\]\\(https://github.com/owner/repo\\/commits\\/111\\)\\), closes \\[#123\\]\\(https://github.com/owner/repo\\/issues\\/123\\)` + ) + ); t.regex(changelog, /### Update/); - t.regex(changelog, /\* Second feature .*, closes #456/); + t.regex( + changelog, + new RegExp( + `Second feature .* \\(\\[222\\]\\(https://github.com/owner/repo\\/commits\\/222\\)\\), closes \\[#456\\]\\(https://github.com/owner/repo\\/issues\\/456\\)` + ) + ); }); test.serial('Accept a partial "parseOpts" and "writerOpts" objects as option', async t => { - await commits(['fix(scope1): 2 First fix (fixes #123)', 'fix(scope2): 1 Second fix (fixes #456)']); + const commits = [ + {hash: '111', message: 'fix(scope1): 2 First fix (fixes #123)'}, + {hash: '222', message: 'fix(scope2): 1 Second fix (fixes #456)'}, + ]; const changelog = await pify(releaseNotesGenerator)( { preset: 'angular', parserOpts: {headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/}, writerOpts: {commitsSort: ['subject', 'scope']}, }, - {} + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} ); + t.regex(changelog, new RegExp(``)); + t.regex(changelog, new RegExp(`\\(https://github.com/owner/repo/compare/123\\.\\.\\.456\\)`)); t.regex(changelog, /### Bug Fixes/); t.regex(changelog, /\* \*\*scope2:\*\* 1 Second fix[\S\s]*\* \*\*scope1:\*\* 2 First fix/); }); -test.serial('Ignore malformatted commits and include valid ones', async t => { - await commits(['fix(scope1): First fix', 'Feature => Invalid message']); - const changelog = await pify(releaseNotesGenerator)({}); +test('Ignore malformatted commits and include valid ones', async t => { + const commits = [ + {hash: '111', message: 'fix(scope1): First fix'}, + {hash: '222', message: 'Feature => Invalid message'}, + ]; + const changelog = await pify(releaseNotesGenerator)( + {}, + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ); t.regex(changelog, /### Bug Fixes/); t.regex(changelog, /\* \*\*scope1:\*\* First fix/); @@ -93,50 +153,53 @@ test.serial('Ignore malformatted commits and include valid ones', async t => { t.notRegex(changelog, /Feature => Invalid message/); }); -test.serial('Throw "SemanticReleaseError" if "preset" doesn`t exist', async t => { - await commits(['Fix: First fix (fixes #123)', 'Update: Second feature (fixes #456)']); +test('Throw error if "preset" doesn`t exist', async t => { + const commits = [ + {hash: '111', message: 'Fix: First fix (fixes #123)'}, + {hash: '222', message: 'Update: Second feature (fixes #456)'}, + ]; const error = await t.throws( - pify(releaseNotesGenerator)({preset: 'unknown-preset'}), - /Preset: "unknown-preset" does not exist:/ + pify(releaseNotesGenerator)( + {preset: 'unknown-preset'}, + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ) ); - t.true(error instanceof SemanticReleaseError); t.is(error.code, 'MODULE_NOT_FOUND'); }); -test.serial('Throw "SemanticReleaseError" if "config" doesn`t exist', async t => { - await commits(['Fix: First fix (fixes #123)', 'Update: Second feature (fixes #456)']); +test('Throw error if "config" doesn`t exist', async t => { + const commits = [ + {hash: '111', message: 'Fix: First fix (fixes #123)'}, + {hash: '222', message: 'Update: Second feature (fixes #456)'}, + ]; const error = await t.throws( - pify(releaseNotesGenerator)({config: 'unknown-config'}), - /Config: "unknown-config" does not exist:/ + pify(releaseNotesGenerator)( + {config: 'unknown-config'}, + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ) ); - t.true(error instanceof SemanticReleaseError); t.is(error.code, 'MODULE_NOT_FOUND'); }); -test.serial('Handle error in "conventional-changelog" and wrap in "SemanticReleaseError"', async t => { - await commits(['Fix: First fix (fixes #123)', 'Update: Second feature (fixes #456)']); +test('ReThrow error from "conventional-changelog"', async t => { + const commits = [ + {hash: '111', message: 'Fix: First fix (fixes #123)'}, + {hash: '222', message: 'Update: Second feature (fixes #456)'}, + ]; const error = await t.throws( - pify(releaseNotesGenerator)({ - writerOpts: { - transform() { - throw new Error(); + pify(releaseNotesGenerator)( + { + writerOpts: { + transform() { + throw new Error('Test error'); + }, }, }, - }), - /Error in conventional-changelog/ + {pkg: {repository: {url}}, lastRelease, nextRelease, commits} + ) ); - t.true(error instanceof SemanticReleaseError); -}); - -test.serial('Accept an undefined "pluginConfig"', async t => { - await commits(['fix(scope1): First fix', 'feat(scope2): Second feature']); - const changelog = await pify(releaseNotesGenerator)(undefined); - - t.regex(changelog, /### Bug Fixes/); - t.regex(changelog, /\* \*\*scope1:\*\* First fix/); - t.regex(changelog, /### Features/); - t.regex(changelog, /\* \*\*scope2:\*\* Second feature/); + t.is(error.message, 'Test error'); }); diff --git a/test/load-changelog-config.test.js b/test/load-changelog-config.test.js index f7154b7b..48ad0c81 100644 --- a/test/load-changelog-config.test.js +++ b/test/load-changelog-config.test.js @@ -1,5 +1,4 @@ import test from 'ava'; -import SemanticReleaseError from '@semantic-release/error'; import loadChangelogConfig from './../lib/load/changelog-config'; /** @@ -125,22 +124,14 @@ test(loadConfig, 'express'); test(loadPreset, 'jshint'); test(loadConfig, 'jshint'); -test('Throw "SemanticReleaseError" if "config" doesn`t exist', async t => { - const error = await t.throws( - loadChangelogConfig({config: 'unknown-config'}), - /Config: "unknown-config" does not exist:/ - ); +test('Throw error if "config" doesn`t exist', async t => { + const error = await t.throws(loadChangelogConfig({config: 'unknown-config'})); - t.true(error instanceof SemanticReleaseError); t.is(error.code, 'MODULE_NOT_FOUND'); }); -test('Throw "SemanticReleaseError" if "preset" doesn`t exist', async t => { - const error = await t.throws( - loadChangelogConfig({preset: 'unknown-preset'}), - /Preset: "unknown-preset" does not exist:/ - ); +test('Throw error if "preset" doesn`t exist', async t => { + const error = await t.throws(loadChangelogConfig({preset: 'unknown-preset'})); - t.true(error instanceof SemanticReleaseError); t.is(error.code, 'MODULE_NOT_FOUND'); });