Skip to content

Commit b1e27da

Browse files
authored
Skip writing changelogs for packages not being bumped (#1124)
1 parent b07e4b5 commit b1e27da

5 files changed

Lines changed: 131 additions & 140 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Skip adding changelog entries for packages without a calculated change type",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

src/__e2e__/bump.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ describe('version bumping', () => {
6464

6565
// Only pkg-1 actually gets bumped
6666
expect(bumpInfo?.calculatedChangeTypes).toEqual({ 'pkg-1': 'minor' });
67-
// But currently, pkg-2 ends up in the modified list and dependentChangedBy via setDependentVersions.
68-
// It's debatable whether this is correct: https://github.com/microsoft/beachball/issues/620#issuecomment-3609264966
67+
// Currently, pkg-2 ends up included due to https://github.com/microsoft/beachball/issues/1123
6968
expect(bumpInfo?.modifiedPackages).toEqual(new Set(['pkg-1', 'pkg-2']));
7069
expect(bumpInfo?.dependentChangedBy).toEqual({ 'pkg-2': new Set(['pkg-1']) });
7170

@@ -89,6 +88,10 @@ describe('version bumping', () => {
8988

9089
const pkg1Changelog = readChangelogJson(repo.pathTo('packages/pkg-1'));
9190
expect(pkg1Changelog!.entries[0].comments.minor![0].comment).toBe(comment);
91+
// There's a check in writeChangeFiles to prevent writing changelogs for packages in
92+
// dependentChangedBy with no changeType
93+
const pkg2Changelog = readChangelogJson(repo.pathTo('packages/pkg-2'));
94+
expect(pkg2Changelog).toBeNull();
9295
});
9396

9497
it('for multi-root monorepo, only bumps packages in the current root', async () => {
Lines changed: 105 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { afterEach, describe, expect, it, jest } from '@jest/globals';
22
import { getPackageChangelogs } from '../../changelog/getPackageChangelogs';
3-
import type { BumpInfo } from '../../types/BumpInfo';
43
import type { ChangeFileInfo, ChangeSet } from '../../types/ChangeInfo';
5-
import type { PackageInfos } from '../../types/PackageInfo';
6-
import { makePackageInfos } from '../../__fixtures__/packageInfos';
4+
import { makePackageInfos, type PartialPackageInfos } from '../../__fixtures__/packageInfos';
75
import { getFileAddedHash } from 'workspace-tools';
86

7+
type PartialBumpInfo = Parameters<typeof getPackageChangelogs>[0];
8+
99
const commit = 'deadbeef';
1010
const author = 'something@something.com';
1111

@@ -15,48 +15,58 @@ jest.mock('workspace-tools', () => ({
1515
getFileAddedHash: jest.fn(() => commit),
1616
}));
1717

18-
function makeChangeInfo(pkg: string, overrides?: Partial<ChangeFileInfo>, filename?: string): ChangeSet[number] {
19-
return {
20-
changeFile: filename || `${pkg}.json`,
21-
change: {
22-
comment: `comment for ${pkg}`,
23-
dependentChangeType: 'patch',
24-
email: author,
25-
packageName: pkg,
26-
type: 'patch',
27-
...overrides,
28-
},
29-
};
30-
}
31-
32-
const options = {
33-
path: '.',
34-
changeDir: 'change',
35-
};
36-
3718
describe('getPackageChangelogs', () => {
3819
// eslint-disable-next-line etc/no-deprecated
3920
const getFileAddedHashMock = getFileAddedHash as jest.MockedFunction<typeof getFileAddedHash>;
4021

22+
/**
23+
* Call `getPackageChangelogs` with filled in params. Defaults:
24+
* - package version `1.0.0`
25+
* - `author` and `commit` constants
26+
* - `changeType: 'patch'`
27+
*/
28+
function getPackageChangelogsWrapper(
29+
bumpInfo: Pick<PartialBumpInfo, 'calculatedChangeTypes' | 'dependentChangedBy'> & {
30+
packageInfos: PartialPackageInfos;
31+
/** Changed package names or change file info (must include `packageName`) */
32+
changes: (string | Partial<ChangeFileInfo>)[];
33+
},
34+
options?: Partial<Parameters<typeof getPackageChangelogs>[1]>
35+
) {
36+
return getPackageChangelogs(
37+
{
38+
packageInfos: makePackageInfos(bumpInfo.packageInfos),
39+
calculatedChangeTypes: bumpInfo.calculatedChangeTypes,
40+
dependentChangedBy: bumpInfo.dependentChangedBy,
41+
changeFileChangeInfos: bumpInfo.changes.map((change): ChangeSet[number] => {
42+
const pkg = typeof change === 'string' ? change : change.packageName!;
43+
return {
44+
changeFile: `${pkg}.json`,
45+
change: {
46+
comment: `comment for ${pkg}`,
47+
dependentChangeType: 'patch',
48+
email: author,
49+
packageName: pkg,
50+
type: 'patch',
51+
...(typeof change === 'string' ? {} : change),
52+
},
53+
};
54+
}),
55+
},
56+
{ path: '.', changeDir: 'change', ...options }
57+
);
58+
}
59+
4160
afterEach(() => {
4261
getFileAddedHashMock.mockClear();
4362
});
4463

4564
it('generates correct changelog entries for a single package', () => {
46-
const changeFileChangeInfos: ChangeSet = [
47-
makeChangeInfo('foo'),
48-
makeChangeInfo('foo', { type: 'minor', comment: 'other comment' }),
49-
];
50-
const packageInfos = makePackageInfos({ foo: { version: '1.0.0' } });
51-
52-
const changelogs = getPackageChangelogs(
53-
{
54-
changeFileChangeInfos,
55-
calculatedChangeTypes: { foo: 'patch' },
56-
packageInfos,
57-
},
58-
options
59-
);
65+
const changelogs = getPackageChangelogsWrapper({
66+
packageInfos: { foo: { version: '1.0.0' } },
67+
calculatedChangeTypes: { foo: 'patch' },
68+
changes: ['foo', { packageName: 'foo', type: 'minor', comment: 'other comment' }],
69+
});
6070

6171
expect(changelogs.foo).toEqual({
6272
comments: {
@@ -74,20 +84,14 @@ describe('getPackageChangelogs', () => {
7484
});
7585

7686
it('generates correct changelog entries for multiple packages', () => {
77-
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo'), makeChangeInfo('bar')];
78-
const packageInfos = makePackageInfos({
79-
foo: { version: '1.0.0' },
80-
bar: { version: '2.0.0' },
81-
});
82-
83-
const changelogs = getPackageChangelogs(
84-
{
85-
changeFileChangeInfos,
86-
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
87-
packageInfos,
87+
const changelogs = getPackageChangelogsWrapper({
88+
packageInfos: {
89+
foo: { version: '1.0.0' },
90+
bar: { version: '2.0.0' },
8891
},
89-
options
90-
);
92+
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
93+
changes: ['foo', 'bar'],
94+
});
9195

9296
expect(changelogs.foo).toEqual({
9397
comments: {
@@ -112,54 +116,31 @@ describe('getPackageChangelogs', () => {
112116
});
113117

114118
it('preserves custom properties from change files', () => {
115-
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo', { extra: 'prop' })];
116-
const packageInfos: PackageInfos = makePackageInfos({ foo: { version: '1.0.0' } });
117-
118-
const changelogs = getPackageChangelogs(
119-
{
120-
changeFileChangeInfos,
121-
calculatedChangeTypes: { foo: 'patch' },
122-
packageInfos,
123-
},
124-
options
125-
);
126-
119+
const changelogs = getPackageChangelogsWrapper({
120+
packageInfos: { foo: { version: '1.0.0' } },
121+
calculatedChangeTypes: { foo: 'patch' },
122+
changes: [{ packageName: 'foo', extra: 'prop' }],
123+
});
127124
expect(changelogs.foo.comments.patch![0]).toMatchObject({ extra: 'prop' });
128125
});
129126

130127
it('records dependent bumps', () => {
131-
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo')];
132-
133-
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {
134-
bar: new Set(['foo']),
135-
};
136-
137-
const packageInfos = makePackageInfos({
138-
foo: { version: '1.0.0' },
139-
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
140-
});
141-
142-
const changelogs = getPackageChangelogs(
143-
{
144-
changeFileChangeInfos,
145-
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
146-
dependentChangedBy,
147-
packageInfos,
128+
const changelogs = getPackageChangelogsWrapper({
129+
packageInfos: {
130+
foo: { version: '1.0.0' },
131+
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
148132
},
149-
options
150-
);
133+
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
134+
changes: ['foo'],
135+
dependentChangedBy: { bar: new Set(['foo']) },
136+
});
151137

152138
expect(Object.keys(changelogs.foo.comments.patch!)).toHaveLength(1);
153139
expect(changelogs.bar).toEqual({
154140
comments: {
155141
patch: [
156-
{
157-
author: 'beachball',
158-
package: 'bar',
159-
comment: 'Bump foo to v1.0.0',
160-
// IMPORTANT: this should not record an actual commit hash, because it will be incorrect
161-
commit: 'not available',
162-
},
142+
// IMPORTANT: this should not record an actual commit hash, because it will be incorrect
143+
{ author: 'beachball', package: 'bar', comment: 'Bump foo to v1.0.0', commit: 'not available' },
163144
],
164145
},
165146
date: expect.any(Date),
@@ -171,26 +152,15 @@ describe('getPackageChangelogs', () => {
171152
});
172153

173154
it('records multiple comment entries when a package has a change file AND was part of a dependent bump', () => {
174-
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo'), makeChangeInfo('bar')];
175-
176-
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {
177-
bar: new Set(['foo']),
178-
};
179-
180-
const packageInfos = makePackageInfos({
181-
foo: { version: '1.0.0' },
182-
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
183-
});
184-
185-
const changelogs = getPackageChangelogs(
186-
{
187-
changeFileChangeInfos,
188-
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
189-
dependentChangedBy,
190-
packageInfos,
155+
const changelogs = getPackageChangelogsWrapper({
156+
packageInfos: {
157+
foo: { version: '1.0.0' },
158+
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
191159
},
192-
options
193-
);
160+
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
161+
changes: ['foo', 'bar'],
162+
dependentChangedBy: { bar: new Set(['foo']) },
163+
});
194164

195165
expect(changelogs.bar.comments).toEqual({
196166
patch: [
@@ -204,50 +174,50 @@ describe('getPackageChangelogs', () => {
204174
});
205175

206176
it('does not generate changelogs for dependent bumps of private packages', () => {
207-
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('bar')];
208-
209-
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {
210-
'private-pkg': new Set(['bar']),
211-
};
212-
213-
const packageInfos = makePackageInfos({
214-
'private-pkg': {
215-
version: '1.0.0',
216-
private: true,
217-
dependencies: { bar: '^1.0.0' },
177+
const changelogs = getPackageChangelogsWrapper({
178+
packageInfos: {
179+
'private-pkg': { private: true, dependencies: { bar: '^1.0.0' } },
180+
bar: {},
218181
},
219-
bar: { version: '1.0.0' },
182+
calculatedChangeTypes: { bar: 'patch', 'private-pkg': 'patch' },
183+
changes: ['bar'],
184+
dependentChangedBy: { 'private-pkg': new Set(['bar']) },
220185
});
221186

222-
const changelogs = getPackageChangelogs(
223-
{
224-
changeFileChangeInfos,
225-
calculatedChangeTypes: { bar: 'patch', 'private-pkg': 'patch' },
226-
dependentChangedBy,
227-
packageInfos,
228-
},
229-
options
230-
);
231-
232187
expect(changelogs.bar).toBeTruthy();
233188
expect(changelogs['private-pkg']).toBeUndefined();
234189
});
235190

236191
it('omits commit hashes if requested', () => {
237-
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo')];
238-
const packageInfos = makePackageInfos({ foo: { version: '1.0.0' } });
239-
240-
const changelogs = getPackageChangelogs(
192+
const changelogs = getPackageChangelogsWrapper(
241193
{
242-
changeFileChangeInfos,
194+
packageInfos: { foo: { version: '1.0.0' } },
243195
calculatedChangeTypes: { foo: 'patch' },
244-
packageInfos,
196+
changes: ['foo'],
245197
},
246-
{ ...options, changelog: { includeCommitHashes: false } }
198+
{ changelog: { includeCommitHashes: false } }
247199
);
248200

249201
expect(changelogs.foo.comments.patch).toHaveLength(1);
250202
expect(changelogs.foo.comments.patch![0].commit).toBeUndefined();
251203
expect(getFileAddedHashMock).not.toHaveBeenCalled();
252204
});
205+
206+
it('ignores dependent bumps for packages with no calculatedChangeType', () => {
207+
// This happens when bumpDeps is false or the dependent is out of scope
208+
// (Related issue for why the package ends up in dependentChangedBy at all: https://github.com/microsoft/beachball/issues/1123
209+
// and this test could potentially be removed once the issue is fixed)
210+
const changelogs = getPackageChangelogsWrapper({
211+
packageInfos: {
212+
foo: { version: '1.0.0' },
213+
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
214+
},
215+
calculatedChangeTypes: { foo: 'patch' },
216+
changes: ['foo'],
217+
dependentChangedBy: { bar: new Set(['foo']) },
218+
});
219+
220+
expect(changelogs.foo).toBeDefined();
221+
expect(changelogs.bar).toBeUndefined();
222+
});
253223
});

src/changelog/getPackageChangelogs.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,14 @@ export function getPackageChangelogs(
5757
continue;
5858
}
5959

60-
changelogs[dependent] ??= createPackageChangelog(packageInfos[dependent]);
61-
6260
const changeType = calculatedChangeTypes[dependent];
61+
if (!changeType) {
62+
// Either bumpDeps is false, or the dependent is out of scope, so skip writing changelogs.
63+
// (Related issue for why the package ends up in dependentChangedBy at all: https://github.com/microsoft/beachball/issues/1123)
64+
continue;
65+
}
66+
67+
changelogs[dependent] ??= createPackageChangelog(packageInfos[dependent]);
6368

6469
changelogs[dependent].comments ??= {};
6570
changelogs[dependent].comments[changeType] ??= [];
@@ -73,6 +78,7 @@ export function getPackageChangelogs(
7378
// This change will be made in the commit that is currently being created, so unless we
7479
// split publishing into two commits (one for bumps and one for changelog updates),
7580
// there's no way to know the hash yet. It's better to record nothing than incorrect info.
81+
// TODO: switch to actually writing nothing at all
7682
// https://github.com/microsoft/beachball/issues/901
7783
...(includeCommitHashes && { commit: commitNotAvailable }),
7884
});

src/types/BumpInfo.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export type BumpInfo = {
2424
calculatedChangeTypes: { [pkgName: string]: ChangeType };
2525

2626
/**
27-
* Package version groups (not changelog groups) derived from `BeachballOptions.groups` (`VersionGroupOptions`).
27+
* Package version groups (not changelog groups) derived from `BeachballOptions.groups`
28+
* (`VersionGroupOptions`).
2829
*/
2930
packageGroups: DeepReadonly<PackageGroups>;
3031

@@ -39,6 +40,10 @@ export type BumpInfo = {
3940
/**
4041
* Map from package name to its internal dependency names that were bumped.
4142
* This is just used for changelog generation.
43+
*
44+
* Note: due to [this issue](https://github.com/microsoft/beachball/issues/1123), there may be
45+
* packages here that don't have a `calculatedChangeTypes` entry, and those should be ignored
46+
* when generating changelogs.
4247
*/
4348
dependentChangedBy: { [pkgName: string]: Set<string> };
4449

0 commit comments

Comments
 (0)