Skip to content

Commit b60fb23

Browse files
RankarusuGuido CerasohazzoTimothyJones
authored
feat: add --noBumpWhenEmptyChanges flag (#274)
* feat: skip version bumping if no commits * feat: simplified not bumping version by copying whatBump function and adding logic inside * fix: _whatBump also check for fix types to avoid upgrading if there are not patches * chore: added config flag and linted files. Missing tests * chore: rename _whatBump, remove unnecessary async, replace chaining with terneray * test: add mock for git-raw-commits * test: add tests for --noBumpWhenEmptyChanges flag * chore: Run formatter --------- Co-authored-by: Guido Ceraso <gceraso@mrmilu.com> Co-authored-by: Guido Ceraso <guidoceraso@gmail.com> Co-authored-by: Timothy Jones <timothy.l.jones@gmail.com>
1 parent 5016642 commit b60fb23

5 files changed

Lines changed: 133 additions & 0 deletions

File tree

command.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ const yargs = require('yargs')
138138
alias: 'c',
139139
describe: 'Path to a custom configuration file',
140140
})
141+
.option('noBumpWhenEmptyChanges', {
142+
type: 'boolean',
143+
default: false,
144+
describe:
145+
'Avoid bumping files and generating changelog if there are no changes.',
146+
})
141147
.check((argv) => {
142148
if (typeof argv.scripts !== 'object' || Array.isArray(argv.scripts)) {
143149
throw Error('scripts must be an object');

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ module.exports = async function standardVersion(argv) {
8585
}
8686

8787
const newVersion = await bump(args, version);
88+
if (!newVersion) return;
8889
await changelog(args, newVersion);
8990
await commit(args, newVersion);
9091
await tag(newVersion, pkg ? pkg.private : false, args);

lib/lifecycles/bump.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,47 @@ const semver = require('semver');
1313
const writeFile = require('../write-file');
1414
const { resolveUpdaterObjectFromArgument } = require('../updaters');
1515
const gitSemverTags = require('git-semver-tags');
16+
const addBangNotes = require('conventional-changelog-conventionalcommits/add-bang-notes');
1617
let configsToUpdate = {};
1718
const sanitizeQuotesRegex = /['"]+/g;
1819

20+
function noBumpWhenEmptyChanges(config, commits) {
21+
let level = 2;
22+
let breakings = 0;
23+
let features = 0;
24+
let fix = 0;
25+
26+
commits.forEach((commit) => {
27+
addBangNotes(commit);
28+
if (commit.notes.length > 0) {
29+
breakings += commit.notes.length;
30+
level = 0;
31+
} else if (commit.type === 'feat' || commit.type === 'feature') {
32+
features += 1;
33+
if (level === 2) {
34+
level = 1;
35+
}
36+
}
37+
if (commit.type === 'fix') {
38+
fix += 1;
39+
}
40+
});
41+
42+
if (config.preMajor && level < 2) {
43+
level++;
44+
}
45+
46+
if (!breakings && !features && !fix) return {};
47+
48+
return {
49+
level,
50+
reason:
51+
breakings === 1
52+
? `There is ${breakings} BREAKING CHANGE and ${features} features`
53+
: `There are ${breakings} BREAKING CHANGES and ${features} features`,
54+
};
55+
}
56+
1957
async function Bump(args, version) {
2058
// reset the cache of updated config files each
2159
// time we perform the version bump step.
@@ -42,6 +80,7 @@ async function Bump(args, version) {
4280
const prebumpString = stdout.trim().replace(sanitizeQuotesRegex, '');
4381
if (semver.valid(prebumpString)) args.releaseAs = prebumpString;
4482
}
83+
4584
if (!args.firstRelease) {
4685
if (semver.valid(args.releaseAs)) {
4786
const releaseAs = new semver.SemVer(args.releaseAs);
@@ -76,6 +115,11 @@ async function Bump(args, version) {
76115
newVersion = semvarToVersionStr(newVersion, releaseAs.build);
77116
} else {
78117
const release = await bumpVersion(args.releaseAs, version, args);
118+
119+
if (!Object.keys(release).length) {
120+
checkpoint(args, 'no commits found, so not bumping version', []);
121+
return null;
122+
}
79123
const releaseType = getReleaseType(
80124
args.prerelease,
81125
release.releaseType,
@@ -212,6 +256,13 @@ function bumpVersion(releaseAs, currentVersion, args) {
212256
),
213257
}
214258
: {}),
259+
...(args.noBumpWhenEmptyChanges
260+
? {
261+
whatBump(commits) {
262+
return noBumpWhenEmptyChanges(presetOptions, commits);
263+
},
264+
}
265+
: {}),
215266
},
216267
args.parserOpts,
217268
function (err, release) {

test/core.spec.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ function mock({
158158
bump,
159159
changelog,
160160
tags,
161+
commits,
161162
existingChangelog,
162163
testFiles,
163164
realTestFiles,
@@ -174,6 +175,10 @@ function mock({
174175
tags,
175176
});
176177

178+
mockers.mockGitRawCommits({
179+
commits,
180+
});
181+
177182
// needs to be set after mockery, but before mock-fs
178183
standardVersion = require('../index');
179184

@@ -913,6 +918,58 @@ describe('cli', function () {
913918
expect(consoleErrorSpy).not.toHaveBeenCalled();
914919
expect(consoleInfoSpy).not.toHaveBeenCalled();
915920
});
921+
922+
it('bumps using actual recommendedBump when there are no semver relevant changes and --noBumpWhenEmptyChanges flag is not set', async function () {
923+
mock({
924+
bump: jest.requireActual('conventional-recommended-bump'),
925+
changelog: ['chore release\n'],
926+
tags: ['v1.0.0'],
927+
commits: ['chore: update deps\n\n-hash-\nabc123\n'],
928+
});
929+
930+
await exec();
931+
932+
verifyPackageVersion({ writeFileSyncSpy, expectedVersion: '1.0.1' });
933+
});
934+
935+
it('bumps using actual recommendedBump when there are semver relevant changes and --noBumpWhenEmptyChanges flag is not set', async function () {
936+
mock({
937+
bump: jest.requireActual('conventional-recommended-bump'),
938+
changelog: ['feat release\n'],
939+
tags: ['v1.0.0'],
940+
commits: ['feat: shiny new stuff\n\n-hash-\nabc123\n'],
941+
});
942+
943+
await exec();
944+
945+
verifyPackageVersion({ writeFileSyncSpy, expectedVersion: '1.1.0' });
946+
});
947+
948+
it('does not bump when there are no semver relevant changes and --noBumpWhenEmptyChanges flag is set', async function () {
949+
mock({
950+
bump: jest.requireActual('conventional-recommended-bump'),
951+
changelog: ['chore release\n'],
952+
tags: ['v1.0.0'],
953+
commits: ['chore: update deps\n\n-hash-\nabc123\n'],
954+
});
955+
956+
await exec('--noBumpWhenEmptyChanges');
957+
958+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
959+
});
960+
961+
it('bumps when there are semver relevant changes and --noBumpWhenEmptyChanges flag is set', async function () {
962+
mock({
963+
bump: jest.requireActual('conventional-recommended-bump'),
964+
changelog: ['feat release\n'],
965+
tags: ['v1.0.0'],
966+
commits: ['feat: shiny new stuff\n\n-hash-\nabc123\n'],
967+
});
968+
969+
await exec('--noBumpWhenEmptyChanges');
970+
971+
verifyPackageVersion({ writeFileSyncSpy, expectedVersion: '1.1.0' });
972+
});
916973
});
917974

918975
describe('commit-and-tag-version', function () {

test/mocks/jest-mocks.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
const gitSemverTags = require('git-semver-tags');
22
const conventionalChangelog = require('conventional-changelog');
33
const conventionalRecommendedBump = require('conventional-recommended-bump');
4+
const gitRawCommits = require('git-raw-commits');
45

56
const { Readable } = require('stream');
67

78
jest.mock('conventional-changelog');
89
jest.mock('conventional-recommended-bump');
910
jest.mock('git-semver-tags');
11+
jest.mock('git-raw-commits');
1012

1113
const mockGitSemverTags = ({ tags = [] }) => {
1214
gitSemverTags.mockImplementation((opts, cb) => {
@@ -15,6 +17,21 @@ const mockGitSemverTags = ({ tags = [] }) => {
1517
});
1618
};
1719

20+
/**
21+
* @param { { commits: string[] } }
22+
* a commit should look like `<rawBody>\n\n-hash-\n<hash>\n`
23+
*/
24+
const mockGitRawCommits = ({ commits = [] }) => {
25+
gitRawCommits.mockImplementation(() => {
26+
return new Readable({
27+
read() {
28+
commits.forEach((c) => this.push(c));
29+
this.push(null);
30+
},
31+
});
32+
});
33+
};
34+
1835
const mockConventionalChangelog = ({ changelog }) => {
1936
conventionalChangelog.mockImplementation(
2037
(opt) =>
@@ -43,6 +60,7 @@ const mockRecommendedBump = ({ bump }) => {
4360

4461
module.exports = {
4562
mockGitSemverTags,
63+
mockGitRawCommits,
4664
mockConventionalChangelog,
4765
mockRecommendedBump,
4866
};

0 commit comments

Comments
 (0)