Skip to content

Commit

Permalink
feat: use cwd and env options passed by core
Browse files Browse the repository at this point in the history
BREAKING CHANGE: require `semantic-release` >= `15.8.0`
  • Loading branch information
pvdlg committed Jul 17, 2018
1 parent e8c5604 commit 1d74e46
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 58 deletions.
20 changes: 11 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ const DEFAULT_RELEASE_RULES = require('./lib/default-release-rules');
/**
* Determine the type of release to create based on a list of commits.
*
* @param {Object} [pluginConfig={}] semantic-release configuration
* @param {Object} pluginConfig The plugin configuration.
* @param {String} pluginConfig.preset conventional-changelog preset ('angular', 'atom', 'codemirror', 'ember', 'eslint', 'express', 'jquery', 'jscs', 'jshint')
* @param {String} pluginConfig.config requierable npm package with a custom conventional-changelog preset
* @param {String|Array} pluginConfig.releaseRules a string to load an external module or an `Array` of rules.
* @param {Object} pluginConfig.parserOpts additional `conventional-changelog-parser` options that will overwrite ones loaded by `preset` or `config`.
* @param {Object} options semantic-release options
* @param {Array} options.commits array of commits
* @param {String} pluginConfig.config Requierable npm package with a custom conventional-changelog preset
* @param {String|Array} pluginConfig.releaseRules A `String` to load an external module or an `Array` of rules.
* @param {Object} pluginConfig.parserOpts Additional `conventional-changelog-parser` options that will overwrite ones loaded by `preset` or `config`.
* @param {Object} context The semantic-release context.
* @param {Array<Object>} context.commits The commits to analyze.
* @param {String} context.cwd The current working directory.
*
* @returns {String|null} the type of release to create based on the list of commits or `null` if no release has to be done.
*/
async function commitAnalyzer(pluginConfig, {commits, logger}) {
const releaseRules = loadReleaseRules(pluginConfig);
const config = await loadParserConfig(pluginConfig);
async function commitAnalyzer(pluginConfig, context) {
const {commits, logger} = context;
const releaseRules = loadReleaseRules(pluginConfig, context);
const config = await loadParserConfig(pluginConfig, context);
let releaseType = null;

filter(
Expand Down
15 changes: 9 additions & 6 deletions lib/load-parser-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ const conventionalChangelogAngular = require('conventional-changelog-angular');
/**
* Load `conventional-changelog-parser` options. Handle presets that return either a `Promise<Array>` or a `Promise<Function>`.
*
* @param {Object} preset conventional-changelog preset ('angular', 'atom', 'codemirror', 'ember', 'eslint', 'express', 'jquery', 'jscs', 'jshint')
* @param {string} config requierable npm package with a custom conventional-changelog preset
* @param {Object} parserOpts additionnal `conventional-changelog-parser` options that will overwrite ones loaded by `preset` or `config`.
* @param {Object} pluginConfig The plugin configuration.
* @param {Object} pluginConfig.preset conventional-changelog preset ('angular', 'atom', 'codemirror', 'ember', 'eslint', 'express', 'jquery', 'jscs', 'jshint')
* @param {String} pluginConfig.config Requierable npm package with a custom conventional-changelog preset
* @param {Object} pluginConfig.parserOpts Additionnal `conventional-changelog-parser` options that will overwrite ones loaded by `preset` or `config`.
* @param {Object} context The semantic-release context.
* @param {String} context.cwd The current working directory.
* @return {Promise<Object>} a `Promise` that resolve to the `conventional-changelog-parser` options.
*/
module.exports = async ({preset, config, parserOpts}) => {
module.exports = async ({preset, config, parserOpts}, {cwd}) => {
let loadedConfig;

if (preset) {
const presetPackage = `conventional-changelog-${preset.toLowerCase()}`;
loadedConfig = importFrom.silent(__dirname, presetPackage) || importFrom(process.cwd(), presetPackage);
loadedConfig = importFrom.silent(__dirname, presetPackage) || importFrom(cwd, presetPackage);
} else if (config) {
loadedConfig = importFrom.silent(__dirname, config) || importFrom(process.cwd(), config);
loadedConfig = importFrom.silent(__dirname, config) || importFrom(cwd, config);
} else {
loadedConfig = conventionalChangelogAngular;
}
Expand Down
10 changes: 7 additions & 3 deletions lib/load-release-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ const RELEASE_TYPES = require('./default-release-types');
* If `releaseRules` parameter is a `string` then load it as an external module with `require`.
* Verifies that the loaded/parameter `releaseRules` is an `Array` and each element has a valid `release` attribute.
*
* @param {string|Array} releaseRules a string to load an external module or an `Array` of rules.
* @param {Object} pluginConfig The plugin configuration.
* @param {String|Array} pluginConfig.releaseRules A `String` to load an external module or an `Array` of rules.
* @param {Object} context The semantic-release context.
* @param {String} context.cwd The current working directory.
*
* @return {Array} the loaded and validated `releaseRules`.
*/
module.exports = ({releaseRules}) => {
module.exports = ({releaseRules}, {cwd}) => {
let loadedReleaseRules;

if (releaseRules) {
loadedReleaseRules =
typeof releaseRules === 'string'
? importFrom.silent(__dirname, releaseRules) || importFrom(process.cwd(), releaseRules)
? importFrom.silent(__dirname, releaseRules) || importFrom(cwd, releaseRules)
: releaseRules;

if (!Array.isArray(loadedReleaseRules)) {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
],
"all": true
},
"peerDependencies": {
"semantic-release": ">=15.8.0 <16.0.0"
},
"prettier": {
"printWidth": 120,
"trailingComma": "es5"
Expand Down
47 changes: 23 additions & 24 deletions test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import test from 'ava';
import {stub} from 'sinon';
import commitAnalyzer from '..';

const cwd = process.cwd();

test.beforeEach(t => {
const log = stub();
t.context.log = log;
Expand All @@ -10,7 +12,7 @@ test.beforeEach(t => {

test('Parse with "conventional-changelog-angular" by default', async t => {
const commits = [{message: 'fix(scope1): First fix'}, {message: 'feat(scope2): Second feature'}];
const releaseType = await commitAnalyzer({}, {commits, logger: t.context.logger});
const releaseType = await commitAnalyzer({}, {cwd, commits, logger: t.context.logger});

t.is(releaseType, 'minor');
t.true(t.context.log.calledWith('Analyzing commit: %s', commits[0].message));
Expand All @@ -22,7 +24,7 @@ test('Parse with "conventional-changelog-angular" by default', async t => {

test('Accept "preset" option', async t => {
const commits = [{message: 'Fix: First fix (fixes #123)'}, {message: 'Update: Second feature (fixes #456)'}];
const releaseType = await commitAnalyzer({preset: 'eslint'}, {commits, logger: t.context.logger});
const releaseType = await commitAnalyzer({preset: 'eslint'}, {cwd, commits, logger: t.context.logger});

t.is(releaseType, 'minor');
t.true(t.context.log.calledWith('Analyzing commit: %s', commits[0].message));
Expand All @@ -36,7 +38,7 @@ test('Accept "config" option', async t => {
const commits = [{message: 'Fix: First fix (fixes #123)'}, {message: 'Update: Second feature (fixes #456)'}];
const releaseType = await commitAnalyzer(
{config: 'conventional-changelog-eslint'},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -54,7 +56,7 @@ test('Accept a "parseOpts" object as option', async t => {
];
const releaseType = await commitAnalyzer(
{parserOpts: {headerPattern: /^%%(.*?)%% (.*)$/, headerCorrespondence: ['tag', 'shortDesc']}},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -72,7 +74,7 @@ test('Accept a partial "parseOpts" object as option', async t => {
config: 'conventional-changelog-eslint',
parserOpts: {headerPattern: /^%%(.*?)%% (.*)$/, headerCorrespondence: ['type', 'shortDesc']},
},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'patch');
Expand All @@ -89,7 +91,7 @@ test('Exclude commits if they have a matching revert commits', async t => {
{hash: '456', message: 'revert: feat(scope): First feature\n\nThis reverts commit 123.\n'},
{message: 'fix(scope): First fix'},
];
const releaseType = await commitAnalyzer({}, {commits, logger: t.context.logger});
const releaseType = await commitAnalyzer({}, {cwd, commits, logger: t.context.logger});

t.is(releaseType, 'patch');
t.true(t.context.log.calledWith('Analyzing commit: %s', commits[2].message));
Expand All @@ -101,7 +103,7 @@ test('Accept a "releaseRules" option that reference a requierable module', async
const commits = [{message: 'fix(scope1): First fix'}, {message: 'feat(scope2): Second feature'}];
const releaseType = await commitAnalyzer(
{releaseRules: './test/fixtures/release-rules'},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -117,7 +119,7 @@ test('Return "major" if there is a breaking change, using default releaseRules',
{message: 'Fix: First fix (fixes #123)'},
{message: 'Update: Second feature (fixes #456) \n\n BREAKING CHANGE: break something'},
];
const releaseType = await commitAnalyzer({preset: 'eslint'}, {commits, logger: t.context.logger});
const releaseType = await commitAnalyzer({preset: 'eslint'}, {cwd, commits, logger: t.context.logger});

t.is(releaseType, 'major');
t.true(t.context.log.calledWith('Analyzing commit: %s', commits[0].message));
Expand All @@ -129,7 +131,7 @@ test('Return "major" if there is a breaking change, using default releaseRules',

test('Return "patch" if there is only types set to "patch", using default releaseRules', async t => {
const commits = [{message: 'fix: First fix (fixes #123)'}, {message: 'perf: perf improvement'}];
const releaseType = await commitAnalyzer({}, {commits, logger: t.context.logger});
const releaseType = await commitAnalyzer({}, {cwd, commits, logger: t.context.logger});

t.is(releaseType, 'patch');
t.true(t.context.log.calledWith('Analyzing commit: %s', commits[0].message));
Expand All @@ -142,11 +144,8 @@ test('Return "patch" if there is only types set to "patch", using default releas
test('Allow to use regex in "releaseRules" configuration', async t => {
const commits = [{message: 'Chore: First chore (fixes #123)'}, {message: 'Docs: update README (fixes #456)'}];
const releaseType = await commitAnalyzer(
{
preset: 'eslint',
releaseRules: [{tag: 'Chore', release: 'patch'}, {message: '/README/', release: 'minor'}],
},
{commits, logger: t.context.logger}
{preset: 'eslint', releaseRules: [{tag: 'Chore', release: 'patch'}, {message: '/README/', release: 'minor'}]},
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -159,7 +158,7 @@ test('Allow to use regex in "releaseRules" configuration', async t => {

test('Return "null" if no rule match', async t => {
const commits = [{message: 'doc: doc update'}, {message: 'chore: Chore'}];
const releaseType = await commitAnalyzer({}, {commits, logger: t.context.logger});
const releaseType = await commitAnalyzer({}, {cwd, commits, logger: t.context.logger});

t.is(releaseType, null);
t.true(t.context.log.calledWith('Analyzing commit: %s', commits[0].message));
Expand All @@ -173,7 +172,7 @@ test('Process rules in order and apply highest match', async t => {
const commits = [{message: 'Chore: First chore (fixes #123)'}, {message: 'Docs: update README (fixes #456)'}];
const releaseType = await commitAnalyzer(
{preset: 'eslint', releaseRules: [{tag: 'Chore', release: 'minor'}, {tag: 'Chore', release: 'patch'}]},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -191,7 +190,7 @@ test('Process rules in order and apply highest match from config even if default
];
const releaseType = await commitAnalyzer(
{preset: 'eslint', releaseRules: [{tag: 'Chore', release: 'patch'}, {breaking: true, release: 'minor'}]},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -206,7 +205,7 @@ test('Use default "releaseRules" if none of provided match', async t => {
const commits = [{message: 'Chore: First chore'}, {message: 'Update: new feature'}];
const releaseType = await commitAnalyzer(
{preset: 'eslint', releaseRules: [{tag: 'Chore', release: 'patch'}]},
{commits, logger: t.context.logger}
{cwd, commits, logger: t.context.logger}
);

t.is(releaseType, 'minor');
Expand All @@ -218,40 +217,40 @@ test('Use default "releaseRules" if none of provided match', async t => {
});

test('Throw error if "preset" doesn`t exist', async t => {
const error = await t.throws(commitAnalyzer({preset: 'unknown-preset'}, {}));
const error = await t.throws(commitAnalyzer({preset: 'unknown-preset'}, {cwd}));

t.is(error.code, 'MODULE_NOT_FOUND');
});

test('Throw error if "releaseRules" is not an Array or a String', async t => {
await t.throws(
commitAnalyzer({releaseRules: {}}, {}),
commitAnalyzer({releaseRules: {}}, {cwd}),
/Error in commit-analyzer configuration: "releaseRules" must be an array of rules/
);
});

test('Throw error if "releaseRules" option reference a requierable module that is not an Array or a String', async t => {
await t.throws(
commitAnalyzer({releaseRules: './test/fixtures/release-rules-invalid'}, {}),
commitAnalyzer({releaseRules: './test/fixtures/release-rules-invalid'}, {cwd}),
/Error in commit-analyzer configuration: "releaseRules" must be an array of rules/
);
});

test('Throw error if "config" doesn`t exist', async t => {
const commits = [{message: 'Fix: First fix (fixes #123)'}, {message: 'Update: Second feature (fixes #456)'}];
const error = await t.throws(commitAnalyzer({config: 'unknown-config'}, {commits, logger: t.context.logger}));
const error = await t.throws(commitAnalyzer({config: 'unknown-config'}, {cwd, commits, logger: t.context.logger}));

t.is(error.code, 'MODULE_NOT_FOUND');
});

test('Throw error if "releaseRules" reference invalid commit type', async t => {
await t.throws(
commitAnalyzer({preset: 'eslint', releaseRules: [{tag: 'Update', release: 'invalid'}]}, {}),
commitAnalyzer({preset: 'eslint', releaseRules: [{tag: 'Update', release: 'invalid'}]}, {cwd}),
/Error in commit-analyzer configuration: "invalid" is not a valid release type\. Valid values are:\[?.*\]/
);
});

test('Re-Throw error from "conventional-changelog-parser"', async t => {
const commits = [{message: 'Fix: First fix (fixes #123)'}, {message: 'Update: Second feature (fixes #456)'}];
await t.throws(commitAnalyzer({parserOpts: {headerPattern: '\\'}}, {commits, logger: t.context.logger}));
await t.throws(commitAnalyzer({parserOpts: {headerPattern: '\\'}}, {cwd, commits, logger: t.context.logger}));
});
21 changes: 13 additions & 8 deletions test/load-parser-config.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import test from 'ava';
import loadParserConfig from '../lib/load-parser-config';

const cwd = process.cwd();

/**
* AVA macro to verify that `loadParserConfig` return a parserOpts object.
*
Expand All @@ -9,7 +11,7 @@ import loadParserConfig from '../lib/load-parser-config';
* @param {[type]} preset the `conventional-changelog` preset to test.
*/
async function loadPreset(t, preset) {
t.truthy((await loadParserConfig({preset})).headerPattern);
t.truthy((await loadParserConfig({preset}, {cwd})).headerPattern);
}
loadPreset.title = (providedTitle, preset) => `${providedTitle} Load "${preset}" preset`.trim();

Expand All @@ -21,17 +23,17 @@ loadPreset.title = (providedTitle, preset) => `${providedTitle} Load "${preset}"
* @param {[type]} config the `conventional-changelog` config to test.
*/
async function loadConfig(t, config) {
t.truthy((await loadParserConfig({config: `conventional-changelog-${config}`})).headerPattern);
t.truthy((await loadParserConfig({config: `conventional-changelog-${config}`}, {cwd})).headerPattern);
}
loadConfig.title = (providedTitle, config) => `${providedTitle} Load "${config}" config`.trim();

test('Load "conventional-changelog-angular" by default', async t => {
t.deepEqual(await loadParserConfig({}), (await require('conventional-changelog-angular')).parserOpts);
t.deepEqual(await loadParserConfig({}, {cwd}), (await require('conventional-changelog-angular')).parserOpts);
});

test('Accept a "parserOpts" object as option', async t => {
const customParserOpts = {headerPattern: /^##(.*?)## (.*)$/, headerCorrespondence: ['tag', 'shortDesc']};
const parserOpts = await loadParserConfig({parserOpts: customParserOpts});
const parserOpts = await loadParserConfig({parserOpts: customParserOpts}, {cwd});

t.is(customParserOpts.headerPattern, parserOpts.headerPattern);
t.deepEqual(customParserOpts.headerCorrespondence, parserOpts.headerCorrespondence);
Expand All @@ -40,7 +42,7 @@ test('Accept a "parserOpts" object as option', async t => {

test('Accept a partial "parserOpts" object as option that overlaod a preset', async t => {
const customParserOpts = {headerPattern: /^##(.*?)## (.*)$/, headerCorrespondence: ['tag', 'shortDesc']};
const parserOpts = await loadParserConfig({parserOpts: customParserOpts, preset: 'angular'});
const parserOpts = await loadParserConfig({parserOpts: customParserOpts, preset: 'angular'}, {cwd});

t.is(customParserOpts.headerPattern, parserOpts.headerPattern);
t.deepEqual(customParserOpts.headerCorrespondence, parserOpts.headerCorrespondence);
Expand All @@ -49,7 +51,10 @@ test('Accept a partial "parserOpts" object as option that overlaod a preset', as

test('Accept a partial "parserOpts" object as option that overlaod a config', async t => {
const customParserOpts = {headerPattern: /^##(.*?)## (.*)$/, headerCorrespondence: ['tag', 'shortDesc']};
const parserOpts = await loadParserConfig({parserOpts: customParserOpts, config: 'conventional-changelog-angular'});
const parserOpts = await loadParserConfig(
{parserOpts: customParserOpts, config: 'conventional-changelog-angular'},
{cwd}
);

t.is(customParserOpts.headerPattern, parserOpts.headerPattern);
t.deepEqual(customParserOpts.headerCorrespondence, parserOpts.headerCorrespondence);
Expand All @@ -70,13 +75,13 @@ test(loadPreset, 'jshint');
test(loadConfig, 'jshint');

test('Throw error if "config" doesn`t exist', async t => {
const error = await t.throws(loadParserConfig({config: 'unknown-config'}));
const error = await t.throws(loadParserConfig({config: 'unknown-config'}, {cwd}));

t.is(error.code, 'MODULE_NOT_FOUND');
});

test('Throw error if "preset" doesn`t exist', async t => {
const error = await t.throws(loadParserConfig({preset: 'unknown-preset'}));
const error = await t.throws(loadParserConfig({preset: 'unknown-preset'}, {cwd}));

t.is(error.code, 'MODULE_NOT_FOUND');
});

0 comments on commit 1d74e46

Please sign in to comment.