Skip to content

Commit be6dca6

Browse files
authored
Add more tests for bump and publish (#1127)
1 parent a460008 commit be6dca6

24 files changed

Lines changed: 1294 additions & 482 deletions

.vscode/launch.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@
2626
"processId": "${command:PickProcess}",
2727
"sourceMaps": true,
2828
"skipFiles": ["<node_internals>/**"]
29+
},
30+
{
31+
// Debug configuration for Jest extension
32+
"name": "vscode-jest-tests.v2",
33+
"type": "node",
34+
"request": "launch",
35+
"runtimeExecutable": "npm",
36+
"cwd": "${workspaceFolder}",
37+
"runtimeArgs": ["run-script", "test"],
38+
"args": [
39+
"--",
40+
"--runInBand",
41+
"--watch",
42+
"--testTimeout=1000000",
43+
"--no-coverage",
44+
"--testNamePattern",
45+
"${jest.testNamePattern}",
46+
"--runTestsByPath",
47+
"${jest.testFile}"
48+
],
49+
"console": "integratedTerminal",
50+
"runtimeVersion": "14"
2951
}
3052
]
3153
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Add validation of package names in callHook. Update internal handling of dependent modified packages (behavior is the same).",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ const commonOptions = {
1818
const config = {
1919
reporters: ['default', 'github-actions'],
2020
testTimeout: 60000,
21+
// Enable to locally test with coverage info (will only work properly with `yarn test`, not
22+
// `test:all` or individual projects). This would be tricky to enable in CI due to the multiple
23+
// test projects that run in sequence. Coverage also doesn't alone capture tricky scenarios.
24+
// collectCoverage: true,
25+
coveragePathIgnorePatterns: ['/node_modules/', '__fixtures__'],
26+
coverageThreshold: {
27+
global: { branches: 80, functions: 100, lines: 90, statements: 90 },
28+
},
2129
projects: [
2230
{
2331
displayName: 'unit',

src/__e2e__/bump.test.ts

Lines changed: 123 additions & 372 deletions
Large diffs are not rendered by default.

src/__e2e__/getChangedPackages.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ describe('getChangedPackages', () => {
215215

216216
// foo is not included in changed packages
217217
let changedPackages = getChangedPackages(options, packageInfos, scopedPackages);
218-
const logLines = logs.getMockLines('all', { guids: true });
218+
const logLines = logs.getMockLines('all', { sanitize: true });
219219
expect(logLines).toMatch(/Your local repository already has change files for these packages:\s+ foo/);
220220
expect(logLines).toMatchInlineSnapshot(`
221221
"[log] Checking for changes against "origin/master"
@@ -232,7 +232,7 @@ describe('getChangedPackages', () => {
232232
// change bar => bar is the only changed package returned
233233
repo.stageChange('packages/bar/test.js');
234234
changedPackages = getChangedPackages(options, packageInfos, scopedPackages);
235-
expect(logs.getMockLines('all', { guids: true })).toMatchInlineSnapshot(`
235+
expect(logs.getMockLines('all', { sanitize: true })).toMatchInlineSnapshot(`
236236
"[log] Checking for changes against "origin/master"
237237
[log] Found 3 changed files in current branch (before filtering)
238238
[log] - ~~change/foo-<guid>.json~~ (ignored by pattern "change/*.json")
@@ -266,7 +266,7 @@ describe('getChangedPackages', () => {
266266

267267
const changedPackages = getChangedPackages(options, packageInfos, scopedPackages);
268268
expect(changedPackages).toStrictEqual(['foo']);
269-
expect(logs.getMockLines('all', { guids: true })).toMatchInlineSnapshot(`
269+
expect(logs.getMockLines('all', { sanitize: true })).toMatchInlineSnapshot(`
270270
"[log] Checking for changes against "origin/master"
271271
[log] Found 2 changed files in current branch (before filtering)
272272
[log] - test.js

src/__e2e__/publishRegistry.test.ts

Lines changed: 131 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { initMockLogs } from '../__fixtures__/mockLogs';
66
import type { Repository } from '../__fixtures__/repository';
77
import { RepositoryFactory } from '../__fixtures__/repositoryFactory';
88
import { publish } from '../commands/publish';
9-
import type { RepoOptions } from '../types/BeachballOptions';
9+
import type { ParsedOptions, RepoOptions } from '../types/BeachballOptions';
1010
import { initNpmMock } from '../__fixtures__/mockNpm';
1111
import { removeTempDir, tmpdir } from '../__fixtures__/tmpdir';
1212
import { getParsedOptions } from '../options/getOptions';
1313
import { createCommandContext } from '../monorepo/createCommandContext';
14+
import { validate } from '../validation/validate';
15+
import { deepFreezeProperties } from '../__fixtures__/object';
1416

1517
// Spawning actual npm to run commands against a fake registry is extremely slow, so mock it for
1618
// this test (packagePublish covers the more complete npm registry scenario).
@@ -27,9 +29,11 @@ describe('publish command (registry)', () => {
2729
let repo: Repository | undefined;
2830
let packToPath: string | undefined;
2931

30-
// show error logs for these tests
31-
const logs = initMockLogs({ alsoLog: ['error'] });
32+
const logs = initMockLogs();
3233

34+
/**
35+
* Get options with defaults including skipping git stuff
36+
*/
3337
function getOptions(repoOptions?: Partial<RepoOptions>) {
3438
const parsedOptions = getParsedOptions({
3539
cwd: repo!.rootPath,
@@ -38,6 +42,7 @@ describe('publish command (registry)', () => {
3842
branch: defaultRemoteBranchName,
3943
registry: 'fake',
4044
message: 'apply package updates',
45+
fetch: false,
4146
bumpDeps: false,
4247
push: false,
4348
gitTags: false,
@@ -49,6 +54,20 @@ describe('publish command (registry)', () => {
4954
return { options: parsedOptions.options, parsedOptions };
5055
}
5156

57+
/**
58+
* For more realistic testing, call `validate()` like the CLI command does, then call `publish()`.
59+
* This helps catch any new issues with double bumps or context mutation.
60+
*/
61+
async function publishWrapper(parsedOptions: ParsedOptions) {
62+
logs.clear();
63+
// This does an initial bump
64+
const { context } = validate(parsedOptions, { checkDependencies: true });
65+
// Ensure the later bump process does not modify the context
66+
deepFreezeProperties(context.bumpInfo);
67+
deepFreezeProperties(context.originalPackageInfos);
68+
await publish(parsedOptions.options, context);
69+
}
70+
5271
afterEach(() => {
5372
repositoryFactory?.cleanUp();
5473
repositoryFactory = undefined;
@@ -64,15 +83,14 @@ describe('publish command (registry)', () => {
6483

6584
const { options, parsedOptions } = getOptions({ packToPath });
6685
generateChangeFiles(['foo'], options);
67-
repo.push();
68-
69-
await publish(options, createCommandContext(parsedOptions));
86+
await publishWrapper(parsedOptions);
7087

7188
expect(fs.readdirSync(packToPath)).toEqual(['1-foo-1.1.0.tgz']);
7289
expect(npmMock.getPublishedVersions('foo')).toBeUndefined();
90+
expect(logs.mocks.error).not.toHaveBeenCalled();
7391
});
7492

75-
it('publishes in monorepo with mixed public and private packages', async () => {
93+
it('skips publishing private package with change file', async () => {
7694
repositoryFactory = new RepositoryFactory({
7795
folders: {
7896
packages: {
@@ -86,36 +104,92 @@ describe('publish command (registry)', () => {
86104
const { options, parsedOptions } = getOptions();
87105
generateChangeFiles(['foopkg'], options);
88106

89-
repo.push();
90-
91-
await publish(options, createCommandContext(parsedOptions));
92-
107+
// If there's only the private package with a change file, nothing happens
108+
await publishWrapper(parsedOptions);
93109
expect(logs.mocks.log).toHaveBeenCalledWith('Nothing to bump, skipping publish!');
94110
expect(logs.mocks.warn).toHaveBeenCalledWith(expect.stringContaining('Change detected for private package foopkg'));
95-
111+
expect(logs.mocks.error).not.toHaveBeenCalled();
96112
expect(npmMock.getPublishedVersions('foopkg')).toBeUndefined();
113+
114+
// Now try with a public package change file
115+
logs.clear();
116+
generateChangeFiles(['publicpkg'], options);
117+
await publishWrapper(parsedOptions);
118+
// Should be published despite private package change also existing
119+
expect(npmMock.getPublishedPackage('publicpkg')!.version).toEqual('1.1.0');
120+
// This is also a good case to get a "visual regression" test of the logs
121+
expect(logs.getMockLines('all', { root: repo.rootPath, sanitize: true })).toMatchInlineSnapshot(`
122+
"[log]
123+
Validating options and change files...
124+
[warn] Change detected for private package foopkg; delete this file: <root>/change/foopkg-<guid>.json
125+
[log]
126+
Validating package dependencies...
127+
[log] Validating no private package among package dependencies
128+
[log] OK!
129+
130+
[log]
131+
[log]
132+
Preparing to publish
133+
[log]
134+
Publishing with the following configuration:
135+
136+
registry: fake
137+
138+
current branch: master
139+
current hash: <commit>
140+
target branch: origin/master
141+
npm dist-tag: latest
142+
143+
bumps versions before publishing: yes
144+
publishes to npm registry: yes
145+
pushes bumps and changelogs to remote git repo: no
146+
147+
148+
[log] Creating temporary publish branch publish_<timestamp>
149+
[log]
150+
Bumping versions and publishing packages to npm registry
151+
152+
[log] Removing change files:
153+
[log] - publicpkg-<guid>.json
154+
[log]
155+
Validating new package versions...
156+
[log]
157+
Package versions are OK to publish:
158+
• publicpkg@1.1.0
159+
[log] Validating no private package among package dependencies
160+
[log] OK!
161+
162+
[log]
163+
Publishing - publicpkg@1.1.0 with tag latest
164+
[log] publish command: publish --registry fake --tag latest --loglevel warn
165+
[log] (cwd: <root>/packages/publicpkg)
166+
[log] Published! - publicpkg@1.1.0
167+
[log]
168+
[log] Skipping git push and tagging
169+
[log]
170+
Cleaning up
171+
[log] git checkout master
172+
[log] deleting temporary publish branch publish_<timestamp>"
173+
`);
97174
});
98175

99176
it('publishes multiple changed packages', async () => {
100-
repositoryFactory = new RepositoryFactory({
101-
folders: {
102-
packages: {
103-
foopkg: { version: '1.0.0', dependencies: { barpkg: '^1.0.0' } },
104-
barpkg: { version: '1.0.0' },
105-
},
106-
},
107-
});
177+
repositoryFactory = new RepositoryFactory('monorepo');
108178
repo = repositoryFactory.cloneRepository();
179+
// Simulate the current package versions already existing to test validatePackageVersions
180+
npmMock.publishPackage(repositoryFactory.fixture.folders.packages.foo);
181+
npmMock.publishPackage(repositoryFactory.fixture.folders.packages.bar);
182+
npmMock.publishPackage(repositoryFactory.fixture.folders.packages.baz);
109183

110184
const { options, parsedOptions } = getOptions();
111-
generateChangeFiles(['foopkg', 'barpkg'], options);
185+
generateChangeFiles(['foo', 'bar'], options);
112186

113-
repo.push();
187+
await publishWrapper(parsedOptions);
114188

115-
await publish(options, createCommandContext(parsedOptions));
116-
117-
expect(npmMock.getPublishedPackage('foopkg')!.version).toEqual('1.1.0');
118-
expect(npmMock.getPublishedPackage('barpkg')!.version).toEqual('1.1.0');
189+
expect(npmMock.getPublishedPackage('foo')!.version).toEqual('1.1.0');
190+
expect(npmMock.getPublishedPackage('bar')!.version).toEqual('1.4.0');
191+
expect(npmMock.mock).toHaveBeenCalledTimes(2);
192+
expect(logs.mocks.error).not.toHaveBeenCalled();
119193
});
120194

121195
it('packs many packages', async () => {
@@ -136,14 +210,14 @@ describe('publish command (registry)', () => {
136210

137211
const { options, parsedOptions } = getOptions({ packToPath, groupChanges: true });
138212
generateChangeFiles(packageNames, options);
139-
repo.push();
140-
213+
// initial validate() isn't relevant here
141214
await publish(options, createCommandContext(parsedOptions));
142215

143216
expect(fs.readdirSync(packToPath).sort()).toEqual(
144217
[...packageNames].reverse().map((name, i) => `${String(i + 1).padStart(2, '0')}-${name}-1.1.0.tgz`)
145218
);
146219
expect(npmMock.getPublishedVersions('pkg-1')).toBeUndefined();
220+
expect(logs.mocks.error).not.toHaveBeenCalled();
147221
});
148222

149223
it('exits publishing early if only invalid change files exist', async () => {
@@ -155,16 +229,43 @@ describe('publish command (registry)', () => {
155229
const { options, parsedOptions } = getOptions();
156230
generateChangeFiles(['bar', 'fake'], options);
157231

158-
repo.push();
159-
232+
// initial validate() isn't relevant here
160233
await publish(options, createCommandContext(parsedOptions));
161234

162235
expect(logs.mocks.log).toHaveBeenCalledWith('Nothing to bump, skipping publish!');
163236
expect(logs.mocks.warn).toHaveBeenCalledWith(expect.stringContaining('Change detected for private package bar'));
164237
expect(logs.mocks.warn).toHaveBeenCalledWith(
165238
expect.stringContaining('Change detected for nonexistent package fake')
166239
);
240+
expect(logs.mocks.error).not.toHaveBeenCalled();
167241

168242
expect(npmMock.getPublishedVersions('foo')).toBeUndefined();
169243
});
244+
245+
it('errors if version already exists in registry', async () => {
246+
repositoryFactory = new RepositoryFactory('monorepo');
247+
repo = repositoryFactory.cloneRepository();
248+
// Simulate the current package versions already existing to test validatePackageVersions
249+
npmMock.publishPackage(repositoryFactory.fixture.folders.packages.foo);
250+
npmMock.publishPackage(repositoryFactory.fixture.folders.packages.bar);
251+
npmMock.publishPackage(repositoryFactory.fixture.folders.packages.baz);
252+
// also say the bumped version of foo and bar already exist (baz is fine)
253+
npmMock.publishPackage({ ...repositoryFactory.fixture.folders.packages.foo, version: '1.1.0' });
254+
npmMock.publishPackage({ ...repositoryFactory.fixture.folders.packages.bar, version: '1.4.0' });
255+
256+
const { options, parsedOptions } = getOptions();
257+
generateChangeFiles(['foo', 'bar', 'baz'], options);
258+
await expect(() => publishWrapper(parsedOptions)).rejects.toThrow('process.exit');
259+
260+
expect(logs.getMockLines('error')).toMatchInlineSnapshot(`
261+
"ERROR: Attempting to publish package versions that already exist in the registry:
262+
• bar@1.4.0
263+
• foo@1.1.0
264+
Something went wrong with publishing! Manually update these package and versions:
265+
• bar@1.4.0
266+
• baz@1.4.0
267+
• foo@1.1.0
268+
No packages were published due to validation errors (see above for details)."
269+
`);
270+
});
170271
});

src/__fixtures__/changeFiles.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,36 @@ export function getChange(
3737
* - `dependentChangeType: 'patch'`
3838
* - `comment: '<packageName> comment'`
3939
* - `email: 'test@test.com'`
40+
*
41+
* @param changes Array of package names or partial change files (which must include `packageName`).
42+
*/
43+
export function generateChanges(changes: (string | PartialChangeFile)[]): ChangeFileInfo[] {
44+
return changes.map(change => {
45+
change = typeof change === 'string' ? { packageName: change } : change;
46+
return {
47+
...getChange(change.packageName, undefined, 'minor'),
48+
...change,
49+
};
50+
});
51+
}
52+
53+
/**
54+
* Generates and writes change files for the given packages.
55+
* Also commits if `options.commit` is true.
56+
*
57+
* Default change info values:
58+
* - `type: 'minor'`
59+
* - `dependentChangeType: 'patch'`
60+
* - `comment: '<packageName> comment'`
61+
* - `email: 'test@test.com'`
62+
*
63+
* @param changes Array of package names or partial change files (which must include `packageName`).
4064
*/
4165
export function generateChangeFiles(
4266
changes: (string | PartialChangeFile)[],
4367
options: Parameters<typeof writeChangeFiles>[1]
4468
): void {
45-
writeChangeFiles(
46-
changes.map(change => {
47-
change = typeof change === 'string' ? { packageName: change } : change;
48-
return {
49-
...getChange(change.packageName, undefined, 'minor'),
50-
...change,
51-
};
52-
}),
53-
options
54-
);
69+
writeChangeFiles(generateChanges(changes), options);
5570
}
5671

5772
/** Get full paths to existing change files under `cwd` */

0 commit comments

Comments
 (0)