Skip to content

Commit d0bc43c

Browse files
authored
For version groups, use the max change type from the group, not the first encountered (#1083)
1 parent 164b69e commit d0bc43c

File tree

12 files changed

+150
-66
lines changed

12 files changed

+150
-66
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "For version groups, use the max change type from the group, not the first one encountered",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

src/__e2e__/bump.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,50 @@ describe('version bumping', () => {
236236
expect(changeFiles).toHaveLength(0);
237237
});
238238

239+
it('bumps all grouped packages to the greatest change type in the group, regardless of change file order', async () => {
240+
repositoryFactory = new RepositoryFactory('monorepo');
241+
repo = repositoryFactory.cloneRepository();
242+
243+
repo.commitChange('packages/commonlib/package.json', {
244+
// The prefix z- here ensures commonlib's change file is loaded AFTER its dependents.
245+
// This makes sure we set the group's version bumps based on ChangeType order and not in
246+
// the sort order the filesystem gives us.
247+
name: 'z-commonlib',
248+
version: '1.0.0',
249+
});
250+
repo.commitChange('packages/pkg-1/package.json', {
251+
name: 'pkg-1',
252+
version: '1.0.0',
253+
dependencies: {
254+
'z-commonlib': '1.0.0',
255+
},
256+
});
257+
258+
const options = getOptions({
259+
groups: [{ include: 'packages/*', disallowedChangeTypes: null, name: 'grp' }],
260+
bumpDeps: true,
261+
commit: true,
262+
});
263+
generateChangeFiles(
264+
[
265+
{ packageName: 'z-commonlib', type: 'none', dependentChangeType: 'none' },
266+
{ packageName: 'pkg-1', type: 'minor', dependentChangeType: 'minor' },
267+
],
268+
options
269+
);
270+
repo.push();
271+
272+
await bump(options);
273+
274+
const packageInfos = getPackageInfos(repo.rootPath);
275+
276+
expect(packageInfos['pkg-1'].version).toBe('1.1.0');
277+
expect(packageInfos['z-commonlib'].version).toBe('1.1.0');
278+
279+
const changeFiles = getChangeFiles(options);
280+
expect(changeFiles).toHaveLength(0);
281+
});
282+
239283
it('bumps all grouped AND dependent packages', async () => {
240284
const monorepo: RepoFixture['folders'] = {
241285
'packages/grp': {

src/__functional__/changelog/writeChangelog.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ describe('writeChangelog', () => {
5454
const calculatedChangeTypes: BumpInfo['calculatedChangeTypes'] = {};
5555
for (const { change } of changeFileChangeInfos) {
5656
const { packageName, type } = change;
57-
calculatedChangeTypes[packageName] = getMaxChangeType(type, calculatedChangeTypes[packageName]);
57+
calculatedChangeTypes[packageName] = getMaxChangeType([type, calculatedChangeTypes[packageName]]);
5858
}
5959
for (const pkgName of Object.keys(dependentChangedBy)) {
60-
calculatedChangeTypes[pkgName] = getMaxChangeType('patch', calculatedChangeTypes[pkgName]);
60+
calculatedChangeTypes[pkgName] = getMaxChangeType(['patch', calculatedChangeTypes[pkgName]]);
6161
}
6262

6363
// Bump versions in package info and package.json for more realistic changelogs.

src/__tests__/changefile/changeTypes.test.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,52 @@ import { getMaxChangeType, initializePackageChangeTypes } from '../../changefile
33
import type { ChangeSet } from '../../types/ChangeInfo';
44

55
describe('getMaxChangeType', () => {
6+
it('handles empty change type array', () => {
7+
expect(getMaxChangeType([], null)).toBe('none');
8+
});
9+
10+
it('handles single change type', () => {
11+
expect(getMaxChangeType(['patch'], null)).toBe('patch');
12+
});
13+
614
it('handles equal change types', () => {
7-
const changeType = getMaxChangeType('patch', 'patch', null);
15+
const changeType = getMaxChangeType(['patch', 'patch'], null);
816
expect(changeType).toBe('patch');
917
});
1018

1119
it('returns greater change type without disallowedChangeTypes', () => {
12-
expect(getMaxChangeType('patch', 'minor', null)).toBe('minor');
13-
expect(getMaxChangeType('minor', 'patch', null)).toBe('minor');
14-
expect(getMaxChangeType('minor', 'major', null)).toBe('major');
15-
expect(getMaxChangeType('patch', 'major', null)).toBe('major');
16-
expect(getMaxChangeType('patch', 'prerelease', null)).toBe('patch');
17-
expect(getMaxChangeType('patch', 'none', null)).toBe('patch');
18-
expect(getMaxChangeType('prerelease', 'none', null)).toBe('prerelease');
20+
expect(getMaxChangeType(['patch', 'minor'], null)).toBe('minor');
21+
expect(getMaxChangeType(['minor', 'patch'], null)).toBe('minor');
22+
expect(getMaxChangeType(['minor', 'major'], null)).toBe('major');
23+
expect(getMaxChangeType(['patch', 'major'], null)).toBe('major');
24+
expect(getMaxChangeType(['patch', 'prerelease'], null)).toBe('patch');
25+
expect(getMaxChangeType(['patch', 'none'], null)).toBe('patch');
26+
expect(getMaxChangeType(['prerelease', 'none'], null)).toBe('prerelease');
27+
});
28+
29+
it('handles longer array of changeTypes with max in middle', () => {
30+
const changeType = getMaxChangeType(['patch', 'minor', 'major', 'patch'], null);
31+
expect(changeType).toBe('major');
1932
});
2033

2134
it('returns none if all given change types are disallowed', () => {
22-
const changeType = getMaxChangeType('patch', 'major', [
23-
'major',
24-
'minor',
25-
'patch',
26-
'prerelease',
27-
'premajor',
28-
'preminor',
29-
'prepatch',
30-
]);
35+
const changeType = getMaxChangeType(
36+
['patch', 'major'],
37+
['major', 'minor', 'patch', 'prerelease', 'premajor', 'preminor', 'prepatch']
38+
);
3139
expect(changeType).toBe('none');
3240
});
3341

3442
it('returns next greatest change type if max is disallowed', () => {
35-
const changeType = getMaxChangeType('patch', 'major', ['major', 'premajor', 'preminor', 'prepatch']);
43+
const changeType = getMaxChangeType(['patch', 'major'], ['major', 'premajor', 'preminor', 'prepatch']);
3644
expect(changeType).toBe('minor');
3745
});
3846

3947
it('handles prerelease only case', () => {
40-
const changeType = getMaxChangeType('patch', 'major', [
41-
'major',
42-
'minor',
43-
'patch',
44-
'premajor',
45-
'preminor',
46-
'prepatch',
47-
]);
48+
const changeType = getMaxChangeType(
49+
['patch', 'major'],
50+
['major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch']
51+
);
4852
expect(changeType).toBe('prerelease');
4953
});
5054
});

src/bump/bumpInPlace.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { updateRelatedChangeType } from './updateRelatedChangeType';
44
import { bumpPackageInfoVersion } from './bumpPackageInfoVersion';
55
import type { BeachballOptions } from '../types/BeachballOptions';
66
import { setDependentVersions } from './setDependentVersions';
7+
import { getMaxChangeType } from '../changefile/changeTypes';
8+
import { ChangeType } from '../types/ChangeInfo';
79

810
/**
911
* Updates BumpInfo according to change types, bump deps, and version groups
@@ -22,12 +24,21 @@ export function bumpInPlace(bumpInfo: BumpInfo, options: BeachballOptions): void
2224
// - the main concern is how to capture the bump reason in grouped changelog
2325

2426
// pass 2: initialize grouped calculatedChangeTypes together
25-
for (const { change: changeInfo } of changeFileChangeInfos) {
26-
const group = Object.values(bumpInfo.packageGroups).find(g => g.packageNames.includes(changeInfo.packageName));
27+
for (const group of Object.values(bumpInfo.packageGroups)) {
28+
// If any of the group's packages have a change, find the max change type out of any package in the group.
29+
const seenTypes = new Set<ChangeType>();
30+
for (const packageNameInGroup of group.packageNames) {
31+
const changeType = calculatedChangeTypes[packageNameInGroup];
32+
if (changeType) {
33+
seenTypes.add(changeType);
34+
}
35+
}
2736

28-
if (group) {
37+
if (seenTypes.size) {
38+
// Set all packages in the group to the max change type.
39+
const maxChangeInGroup = getMaxChangeType([...seenTypes], group.disallowedChangeTypes);
2940
for (const packageNameInGroup of group.packageNames) {
30-
calculatedChangeTypes[packageNameInGroup] = changeInfo.type;
41+
calculatedChangeTypes[packageNameInGroup] = maxChangeInGroup;
3142
}
3243
}
3344
}

src/bump/updateRelatedChangeType.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function updateRelatedChangeType(params: {
3535
const { calculatedChangeTypes, packageGroups, packageInfos } = bumpInfo;
3636

3737
// If dependentChangeType is none (or somehow unset), there's nothing to do.
38-
const updatedChangeType = getMaxChangeType(change.dependentChangeType);
38+
const updatedChangeType = getMaxChangeType([change.dependentChangeType]);
3939
if (updatedChangeType === 'none') {
4040
return;
4141
}
@@ -56,8 +56,7 @@ export function updateRelatedChangeType(params: {
5656
if (subjectPackage !== entryPointPackageName) {
5757
const oldType = calculatedChangeTypes[subjectPackage];
5858
calculatedChangeTypes[subjectPackage] = getMaxChangeType(
59-
oldType,
60-
changeType,
59+
[oldType, changeType],
6160
packageInfos[subjectPackage]?.combinedOptions?.disallowedChangeTypes
6261
);
6362

src/changefile/changeTypes.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function initializePackageChangeTypes(changeSet: ChangeSet): BumpInfo['ca
3434

3535
for (const { change } of changeSet) {
3636
const { packageName: pkg } = change;
37-
const changeType = getMaxChangeType(change.type, pkgChangeTypes[pkg]);
37+
const changeType = getMaxChangeType([change.type, pkgChangeTypes[pkg]]);
3838
// It's best to totally ignore "none" changes to do a bit less processing.
3939
if (changeType !== 'none') {
4040
pkgChangeTypes[pkg] = changeType;
@@ -58,19 +58,25 @@ function getAllowedChangeType(changeType: ChangeType, disallowedChangeTypes: Rea
5858
}
5959

6060
/**
61-
* Get the max allowed change type based on `a` and `b`, accounting for disallowed change types:
62-
* e.g. if `a` is "major" and `b` is "patch", and "major" is disallowed, the result will be "minor"
61+
* Get the max allowed change type out of `changeTypes`, accounting for disallowed change types:
62+
* e.g. if `changeTypes` is `["major", "patch"]`, and `"major"` is disallowed, the result will be `"minor"`
6363
* (the greatest allowed change type).
6464
*/
6565
export function getMaxChangeType(
66-
a: ChangeType | undefined,
67-
b?: ChangeType,
66+
changeTypes: (ChangeType | undefined)[],
6867
disallowedChangeTypes?: ReadonlyArray<ChangeType> | null
6968
): ChangeType {
70-
if (disallowedChangeTypes?.length) {
71-
a = a && getAllowedChangeType(a, disallowedChangeTypes);
72-
b = b && getAllowedChangeType(b, disallowedChangeTypes);
69+
let max: ChangeType = 'none';
70+
71+
for (let changeType of changeTypes) {
72+
if (!changeType) {
73+
continue;
74+
}
75+
if (disallowedChangeTypes?.length) {
76+
changeType = getAllowedChangeType(changeType, disallowedChangeTypes);
77+
}
78+
max = ChangeTypeWeights[changeType] > ChangeTypeWeights[max] ? changeType : max;
7379
}
7480

75-
return a && b ? (ChangeTypeWeights[a] > ChangeTypeWeights[b] ? a : b) : a || b || 'none';
81+
return max;
7682
}

src/changelog/writeChangelog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async function writeGroupedChangelog(
6868
// Validate groups and initialize groupedChangelogs
6969
for (const group of changelogGroups) {
7070
const { changelogAbsDir } = group;
71-
const mainPackageName = 'mainPackageName' in group ? group.mainPackageName : group.masterPackageName;
71+
const mainPackageName = group.mainPackageName ?? group.masterPackageName!;
7272
const mainPackage = packageInfos[mainPackageName];
7373
if (!mainPackage) {
7474
console.warn(`main package ${mainPackageName} does not exist.`);

src/types/BeachballOptions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ export interface RepoOptions {
150150
* - `'json'` to generate only CHANGELOG.json
151151
*/
152152
generateChangelog: boolean | 'md' | 'json';
153-
/** Options for bumping package versions together */
153+
/**
154+
* Options for bumping package versions together.
155+
* (For changelog groups, use `BeachballOptions.changelog.groups`.)
156+
*/
154157
groups?: VersionGroupOptions[];
155158
/**
156159
* Whether to create git tags for published packages
@@ -255,6 +258,8 @@ export interface PackageOptions {
255258

256259
/**
257260
* Options for bumping package versions together.
261+
*
262+
* For changelog groups, use `BeachballOptions.changelog.groups` (`ChangelogGroupOptions`).
258263
*/
259264
export interface VersionGroupOptions {
260265
/** name of the version group */

src/types/BumpInfo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export type BumpInfo = {
2323
*/
2424
calculatedChangeTypes: { [pkgName: string]: ChangeType };
2525

26-
/** Package grouping */
26+
/**
27+
* Package version groups (not changelog groups) derived from `BeachballOptions.groups` (`VersionGroupOptions`).
28+
*/
2729
packageGroups: DeepReadonly<PackageGroups>;
2830

2931
/** Set of packages that had been modified */

0 commit comments

Comments
 (0)