Skip to content

Commit 5e5aa82

Browse files
committed
Include catalog dependency changes when checking for changed packages
1 parent 2165415 commit 5e5aa82

14 files changed

Lines changed: 1056 additions & 309 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"type": "patch",
5+
"comment": "Include catalog dependency changes when checking for changed packages",
6+
"packageName": "beachball",
7+
"email": "elcraig@microsoft.com",
8+
"dependentChangeType": "patch"
9+
}
10+
]
11+
}

packages/beachball/src/__e2e__/getChangedPackages.test.ts

Lines changed: 157 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { describe, expect, it, afterEach, beforeAll, afterAll, jest } from '@jest/globals';
2+
import {
3+
addGitObserver,
4+
clearGitObservers,
5+
getPackageInfo as getWsPackageInfo,
6+
type Catalogs,
7+
catalogsToYaml,
8+
getCatalogs,
9+
} from 'workspace-tools';
210
import { defaultBranchName, defaultRemoteBranchName } from '../__fixtures__/gitDefaults';
311
import { RepositoryFactory } from '../__fixtures__/repositoryFactory';
412
import { getPackageInfos } from '../monorepo/getPackageInfos';
@@ -9,7 +17,6 @@ import { generateChangeFiles } from '../__fixtures__/changeFiles';
917
import type { Repository } from '../__fixtures__/repository';
1018
import { getParsedOptions } from '../options/getOptions';
1119
import { getScopedPackages } from '../monorepo/getScopedPackages';
12-
import { addGitObserver, clearGitObservers, getPackageInfo as getWsPackageInfo } from 'workspace-tools';
1320

1421
//
1522
// NOTE: These tests should mostly cover logic specific to getChangePackages itself.
@@ -19,12 +26,14 @@ describe('getChangedPackages', () => {
1926
const gitObserver = jest.fn();
2027
const logs = initMockLogs();
2128

22-
/** Factory reused for most tests */
29+
let singleFactory: RepositoryFactory;
2330
let monorepoFactory: RepositoryFactory;
2431
const extraFactories: RepositoryFactory[] = [];
25-
/** Monorepo reused for multiple tests where it's safe */
26-
let _reusedRepo: Repository;
27-
/** Repo for current test */
32+
/** Repos reused for some tests where it's safe (don't use directly) */
33+
let _reusedRepos: {
34+
single: Repository;
35+
monorepo: Repository;
36+
}; /** Repo for current test */
2837
let repo: Repository | undefined;
2938

3039
/**
@@ -33,8 +42,8 @@ describe('getChangedPackages', () => {
3342
* - create uncommitted files or directories (commited files are fine because they'll go away
3443
* on branch change)
3544
*/
36-
function getReusedRepoWithBranch() {
37-
repo = _reusedRepo;
45+
function getReusedRepoWithBranch(type: 'single' | 'monorepo') {
46+
repo = type === 'single' ? _reusedRepos.single : _reusedRepos.monorepo;
3847
repo.checkoutTestBranch();
3948
return repo;
4049
}
@@ -54,7 +63,9 @@ describe('getChangedPackages', () => {
5463
}
5564

5665
/** Get options/context, clear `gitObserver` mock, and call `getChangedPackages` */
57-
function getChangedPackagesWrapper(params: { repoOptions?: Partial<RepoOptions>; extraArgv?: string[] } = {}) {
66+
function getChangedPackagesWrapper(
67+
params: { repoOptions?: Partial<RepoOptions>; extraArgv?: string[]; cwd?: string } = {}
68+
) {
5869
const parsedOptions = getOptions(params);
5970
const packageInfos = getPackageInfos(parsedOptions);
6071
const scopedPackages = getScopedPackages(parsedOptions.options, packageInfos);
@@ -63,8 +74,12 @@ describe('getChangedPackages', () => {
6374
}
6475

6576
beforeAll(() => {
77+
singleFactory = new RepositoryFactory('single');
6678
monorepoFactory = new RepositoryFactory('monorepo');
67-
_reusedRepo = monorepoFactory.cloneRepository();
79+
_reusedRepos = {
80+
single: singleFactory.cloneRepository(),
81+
monorepo: monorepoFactory.cloneRepository(),
82+
};
6883
addGitObserver(gitObserver);
6984
});
7085

@@ -81,7 +96,7 @@ describe('getChangedPackages', () => {
8196
});
8297

8398
it('returns the given package name(s) as-is', () => {
84-
repo = getReusedRepoWithBranch();
99+
repo = getReusedRepoWithBranch('single');
85100
expect(getChangedPackagesWrapper({ extraArgv: ['--package', 'foo'] })).toEqual(['foo']);
86101
expect(gitObserver).not.toHaveBeenCalled();
87102

@@ -93,24 +108,108 @@ describe('getChangedPackages', () => {
93108
expect(gitObserver).not.toHaveBeenCalled();
94109
});
95110

96-
// Basic test as a sanity check, but simple file-based cases are mainly covered by getAllChangedPackages
111+
// do a full test for each major repo structure
112+
it('detects changed files in single-package repo', () => {
113+
repo = getReusedRepoWithBranch('single');
114+
repo.commitChange('myFilename');
115+
expect(getChangedPackagesWrapper()).toEqual(['foo']);
116+
expect(gitObserver).toHaveBeenCalled();
117+
});
118+
97119
it('detects changed files in monorepo', () => {
98-
repo = getReusedRepoWithBranch();
120+
repo = getReusedRepoWithBranch('monorepo');
99121

100122
// empty if no changes yet
101123
expect(getChangedPackagesWrapper()).toEqual([]);
102124

103-
repo.commitChange('packages/foo/test.js');
104-
expect(getChangedPackagesWrapper()).toEqual(['foo']);
125+
repo.writeFile('packages/foo/test.js');
126+
repo.writeFile('packages/bar/test.js');
127+
repo.commitAll();
105128

106-
expect(logs.getMockLines('all', { sanitize: true })).toMatchInlineSnapshot(`
129+
logs.clear();
130+
const result = getChangedPackagesWrapper({ extraArgv: ['--verbose'] });
131+
expect(result.sort()).toEqual(['bar', 'foo']);
132+
expect(gitObserver).toHaveBeenCalled();
133+
134+
expect(logs.getMockLines('all')).toMatchInlineSnapshot(`
135+
"[log] Checking for changes against "origin/master"
136+
[log] Found 2 changed files in current branch (before filtering)
137+
[log] - packages/bar/test.js
138+
[log] - packages/foo/test.js
139+
[log] Found 2 files in 2 packages that should be published"
140+
`);
141+
});
142+
143+
it('detects changed files in multi-project monorepo', () => {
144+
// this is the only multi-project test
145+
const multiFactory = new RepositoryFactory('multi-project');
146+
extraFactories.push(multiFactory);
147+
repo = multiFactory.cloneRepository();
148+
expect(Object.keys(multiFactory.fixtures)).toEqual(['project-a', 'project-b']);
149+
150+
const projectARoot = repo.pathTo('project-a');
151+
const projectBRoot = repo.pathTo('project-b');
152+
153+
repo.stageChange('project-a/packages/foo/test.js');
154+
155+
const changedPackagesA = getChangedPackagesWrapper({ cwd: projectARoot });
156+
const changedPackagesB = getChangedPackagesWrapper({ cwd: projectBRoot });
157+
158+
expect(changedPackagesA).toEqual(['@project-a/foo']);
159+
expect(changedPackagesB).toEqual([]);
160+
});
161+
162+
// Do one real combined test with ignores to ensure all the path handling works
163+
// (the logic is mostly covered by getAllChangedPackages tests)
164+
it('ignores CHANGELOG, change files, and ignorePatterns in single-package repo', () => {
165+
repo = getReusedRepoWithBranch('single');
166+
167+
repo.writeFile('change/change-abc123.json', {});
168+
repo.writeFile('CHANGELOG.md');
169+
repo.writeFile('src/foo.test.js');
170+
repo.writeFile('src/foo.test.js');
171+
repo.writeFile('tests/stuff.js');
172+
repo.writeFile('yarn.lock');
173+
repo.commitAll();
174+
175+
const result = getChangedPackagesWrapper({
176+
repoOptions: { ignorePatterns: ['*.test.js', 'tests/**', 'yarn.lock'] },
177+
extraArgv: ['--verbose'],
178+
});
179+
expect(result).toEqual([]);
180+
181+
const logLines = logs.getMockLines('all');
182+
expect(logLines).toMatch('ignored by pattern');
183+
expect(logLines).toMatchInlineSnapshot(`
107184
"[log] Checking for changes against "origin/master"
108-
[log] Checking for changes against "origin/master""
185+
[log] Found 4 changed files in current branch (before filtering)
186+
[log] - ~~CHANGELOG.md~~ (ignored by pattern "CHANGELOG.{md,json}")
187+
[log] - ~~change/change-abc123.json~~ (ignored by pattern "change/*.json")
188+
[log] - ~~src/foo.test.js~~ (ignored by pattern "*.test.js")
189+
[log] - ~~tests/stuff.js~~ (ignored by pattern "tests/**")
190+
[log] All files were ignored"
109191
`);
110192
});
111193

194+
it('includes staged files', () => {
195+
// need a separate repo since it stages changes
196+
repo = singleFactory.cloneRepository();
197+
repo.checkout('-b', 'test-staged');
198+
// write this file and modify it
199+
repo.commitChange('src/foo.js');
200+
repo.writeFile('src/bar.js');
201+
// add a new file that's only staged
202+
repo.writeFile('src/foo.js');
203+
repo.git(['add', '-A']);
204+
205+
const result = getChangedPackagesWrapper({ extraArgv: ['--verbose'] });
206+
expect(result).toEqual(['foo']);
207+
// src/foo.js is both committed and staged, but should be listed only once
208+
expect(logs.getMockLines('all')).toContain('Found 2 changed files in current branch (before filtering)');
209+
});
210+
112211
it('excludes packages that already have change files', () => {
113-
repo = getReusedRepoWithBranch();
212+
repo = getReusedRepoWithBranch('monorepo');
114213

115214
// setup: change foo, create a change file, commit
116215
repo.checkout('-b', 'test');
@@ -152,7 +251,7 @@ describe('getChangedPackages', () => {
152251
});
153252

154253
it('returns all packages with all: true, removing those with change files', () => {
155-
repo = getReusedRepoWithBranch();
254+
repo = getReusedRepoWithBranch('monorepo');
156255
generateChangeFiles(['foo', 'a'], { ...getOptions().options, commit: true });
157256

158257
const result = getChangedPackagesWrapper({ extraArgv: ['--all'] });
@@ -161,7 +260,7 @@ describe('getChangedPackages', () => {
161260
});
162261

163262
it('throws if the remote is invalid', () => {
164-
repo = getReusedRepoWithBranch();
263+
repo = getReusedRepoWithBranch('monorepo');
165264
const customRemote = 'foo';
166265
repo.git(['remote', 'add', customRemote, 'file:///__nonexistent']);
167266
repo.commitChange('fake.js');
@@ -216,4 +315,43 @@ describe('getChangedPackages', () => {
216315
[log] Found 1 file in 1 package that should be published"
217316
`);
218317
});
318+
319+
it('includes catalog changes in the list of changed packages', () => {
320+
// This needs a separate factory since it pushes changes
321+
const repositoryFactory = new RepositoryFactory('monorepo');
322+
extraFactories.push(repositoryFactory);
323+
repo = repositoryFactory.cloneRepository();
324+
325+
// Add catalog dependencies
326+
const initialCatalogs: Catalogs = { default: { react: '18.0.0', other: '1.0.0' } };
327+
repo.writeFile('.yarnrc.yml', catalogsToYaml(initialCatalogs));
328+
repo.updateJsonFile('packages/foo/package.json', { dependencies: { react: 'catalog:', other: 'catalog:' } });
329+
repo.updateJsonFile('packages/bar/package.json', { dependencies: { react: 'catalog:' } });
330+
repo.commitAll('add catalogs');
331+
repo.push();
332+
// sanity check
333+
expect(getCatalogs(repo.rootPath)).toEqual(initialCatalogs);
334+
335+
// Update a catalog version, and make unrelated changes to verify merging
336+
const newCatalogs: Catalogs = { default: { react: '19.0.0', other: '1.0.0' } };
337+
repo.writeFile('.yarnrc.yml', catalogsToYaml(newCatalogs));
338+
repo.writeFile('packages/foo/test.js', '');
339+
repo.writeFile('packages/grouped/a/foo.ts', '');
340+
repo.commitAll();
341+
342+
const result = getChangedPackagesWrapper({ extraArgv: ['--verbose'] });
343+
expect(result.sort()).toEqual(['a', 'bar', 'foo']);
344+
expect(logs.getMockLines('all', { sanitize: true })).toMatchInlineSnapshot(`
345+
"[log] Checking for changes against "origin/master"
346+
[log] Found 3 changed files in current branch (before filtering)
347+
[log] - ~~.yarnrc.yml~~ (not in a package)
348+
[log] - packages/foo/test.js
349+
[log] - packages/grouped/a/foo.ts
350+
[log] Found 2 files in 2 packages that should be published
351+
[log] Checking for changes to catalog: dependencies...
352+
[log] catalog: dependencies referenced by the following packages have changed:
353+
[log] - bar: react
354+
[log] - foo: react"
355+
`);
356+
});
219357
});

0 commit comments

Comments
 (0)