diff --git a/docs/support/FAQ.md b/docs/support/FAQ.md index b035b2cb..a2f0bb1a 100644 --- a/docs/support/FAQ.md +++ b/docs/support/FAQ.md @@ -123,6 +123,10 @@ See [Artifactory - npm Registry](https://www.jfrog.com/confluence/display/RTF/Np You can trigger a release by pushing to your Git repository. You deliberately cannot trigger a *specific* version release, because this is the whole point of semantic-release. +### Can I exclude commits from the analysis? + +Yes, every commits that contains `[skip release]` or `[release skip]` in their message will be excluded from the commit analysis and won't participate in the release type determination. + ## Is it *really* a good idea to release on every push? It is indeed a great idea because it *forces* you to follow best practices. If you don’t feel comfortable releasing every feature or fix on your `master` you might not treat your `master` branch as intended. diff --git a/index.js b/index.js index 04bfbb4f..fa829d1f 100644 --- a/index.js +++ b/index.js @@ -50,7 +50,12 @@ module.exports = async opts => { ); logger.log('Call plugin %s', 'analyze-commits'); - const type = await plugins.analyzeCommits({options, logger, lastRelease, commits}); + const type = await plugins.analyzeCommits({ + options, + logger, + lastRelease, + commits: commits.filter(commit => !/\[skip\s+release\]|\[release\s+skip\]/i.test(commit.message)), + }); if (!type) { logger.log('There are no relevant changes, so no new version is released.'); return; diff --git a/test/index.test.js b/test/index.test.js index 65416225..43afe5bf 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -460,6 +460,52 @@ test.serial('Returns falsy value if there is no relevant changes', async t => { t.is(t.context.log.args[6][0], 'There are no relevant changes, so no new version is released.'); }); +test.serial('Exclude commits with [skip release] or [release skip] from analysis', async t => { + // Create a git repository, set the current working directory at the root of the repo + await gitRepo(); + // Add commits to the master branch + const commits = await gitCommits([ + 'Test commit', + 'Test commit [skip release]', + 'Test commit [release skip]', + 'Test commit [Release Skip]', + 'Test commit [Skip Release]', + 'Test commit [skip release]', + 'Test commit\n\n commit body\n[skip release]', + 'Test commit\n\n commit body\n[release skip]', + ]); + + const verifyConditions1 = stub().resolves(); + const verifyConditions2 = stub().resolves(); + const getLastRelease = stub().resolves({}); + const analyzeCommits = stub().resolves(); + const verifyRelease = stub().resolves(); + const generateNotes = stub().resolves(); + const publish = stub().resolves(); + + const config = {branch: 'master', repositoryUrl: 'git@hostname.com:owner/module.git', globalOpt: 'global'}; + const options = { + ...config, + verifyConditions: [verifyConditions1, verifyConditions2], + getLastRelease, + analyzeCommits, + verifyRelease, + generateNotes, + publish, + }; + + const semanticRelease = proxyquire('..', { + './lib/logger': t.context.logger, + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + await semanticRelease(options); + + t.is(analyzeCommits.callCount, 1); + t.is(analyzeCommits.args[0][1].commits.length, 1); + t.deepEqual(analyzeCommits.args[0][1].commits[0].hash.substring(0, 7), commits[commits.length - 1].hash); + t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[commits.length - 1].message); +}); + test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => { // Create a git repository, set the current working directory at the root of the repo await gitRepo();