Skip to content

Commit a460008

Browse files
authored
Skip publishing packages that aren't bumped (#1126)
1 parent b1e27da commit a460008

10 files changed

Lines changed: 298 additions & 133 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"javascript.preferences.quoteStyle": "single",
99
"typescript.preferences.quoteStyle": "single",
1010
"typescript.tsdk": "node_modules/typescript/lib",
11+
"jest.runMode": "on-demand",
1112
"search.exclude": {
1213
"**/node_modules": true,
1314
"**/lib": true,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Ensure packages that aren't being bumped (and aren't new) aren't published",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

src/__e2e__/publishE2E.test.ts

Lines changed: 33 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, expect, it, afterEach, jest } from '@jest/globals';
22
import fs from 'fs';
3-
import fsPromises from 'fs/promises';
43
import path from 'path';
54
import { addGitObserver, clearGitObservers } from 'workspace-tools';
65
import { generateChangeFiles, getChangeFiles } from '../__fixtures__/changeFiles';
@@ -9,14 +8,15 @@ import { initMockLogs } from '../__fixtures__/mockLogs';
98
import type { Repository } from '../__fixtures__/repository';
109
import { type PackageJsonFixture, RepositoryFactory } from '../__fixtures__/repositoryFactory';
1110
import { publish } from '../commands/publish';
12-
import type { RepoOptions } from '../types/BeachballOptions';
11+
import type { ParsedOptions, RepoOptions } from '../types/BeachballOptions';
1312
import { _mockNpmPublish, initNpmMock } from '../__fixtures__/mockNpm';
1413
import type { PackageJson } from '../types/PackageInfo';
1514
import { getParsedOptions } from '../options/getOptions';
1615
import { getPackageInfos } from '../monorepo/getPackageInfos';
1716
import { validate } from '../validation/validate';
1817
import { readJson } from '../object/readJson';
1918
import { createCommandContext } from '../monorepo/createCommandContext';
19+
import { deepFreezeProperties } from '../__fixtures__/object';
2020

2121
// Spawning actual npm to run commands against a fake registry is extremely slow, so mock it for
2222
// this test (packagePublish covers the more complete npm registry scenario).
@@ -51,6 +51,19 @@ describe('publish command (e2e)', () => {
5151
return { options: parsedOptions.options, parsedOptions };
5252
}
5353

54+
/**
55+
* For more realistic testing, call `validate()` like the CLI command does, then call `publish()`.
56+
* This helps catch any new issues with double bumps or context mutation.
57+
*/
58+
async function publishWrapper(parsedOptions: ParsedOptions) {
59+
// This does an initial bump
60+
const { context } = validate(parsedOptions, { checkDependencies: true });
61+
// Ensure the later bump process does not modify the context
62+
deepFreezeProperties(context.bumpInfo);
63+
deepFreezeProperties(context.originalPackageInfos);
64+
await publish(parsedOptions.options, context);
65+
}
66+
5467
afterEach(() => {
5568
clearGitObservers();
5669

@@ -75,11 +88,7 @@ describe('publish command (e2e)', () => {
7588
args[0] === 'fetch' && fetchCount++;
7689
});
7790

78-
// For this test, run validate first to simulate what the CLI does.
79-
// This would catch double bump issues if the validate step's bump call mutated the original PackageInfos.
80-
const { context } = validate(parsedOptions, { checkDependencies: true });
81-
82-
await publish(options, context);
91+
await publishWrapper(parsedOptions);
8392

8493
const publishedFoo = npmMock.getPublishedVersions('foo');
8594
expect(publishedFoo).toEqual({
@@ -112,7 +121,7 @@ describe('publish command (e2e)', () => {
112121

113122
repo.checkout('--detach');
114123

115-
await publish(options, createCommandContext(parsedOptions));
124+
await publishWrapper(parsedOptions);
116125

117126
expect(npmMock.getPublishedVersions('foo')).toEqual({
118127
versions: ['1.1.0'],
@@ -145,7 +154,7 @@ describe('publish command (e2e)', () => {
145154
}
146155
});
147156

148-
await publish(options, createCommandContext(parsedOptions));
157+
await publishWrapper(parsedOptions);
149158

150159
expect(npmMock.getPublishedVersions('foo')).toEqual({
151160
versions: ['1.1.0'],
@@ -193,7 +202,7 @@ describe('publish command (e2e)', () => {
193202
}
194203
});
195204

196-
await publish(options, createCommandContext(parsedOptions));
205+
await publishWrapper(parsedOptions);
197206

198207
expect(npmMock.getPublishedVersions('foo')).toEqual({
199208
versions: ['1.1.0'],
@@ -221,7 +230,7 @@ describe('publish command (e2e)', () => {
221230
generateChangeFiles(['foo'], options);
222231
repo.push();
223232

224-
await publish(options, createCommandContext(parsedOptions));
233+
await publishWrapper(parsedOptions);
225234

226235
expect(npmMock.getPublishedVersions('foo')).toEqual({
227236
versions: ['1.0.0'],
@@ -236,36 +245,7 @@ describe('publish command (e2e)', () => {
236245
expect(newPackageInfos.foo.version).toBe('1.0.0');
237246
});
238247

239-
it('publishes only changed packages in a monorepo', async () => {
240-
repositoryFactory = new RepositoryFactory('monorepo');
241-
repo = repositoryFactory.cloneRepository();
242-
243-
const { options, parsedOptions } = getOptions({ fetch: false });
244-
245-
generateChangeFiles(['foo'], options);
246-
repo.push();
247-
248-
// For this test, run validate first to simulate what the CLI does
249-
validate(parsedOptions, { checkDependencies: true });
250-
251-
await publish(options, createCommandContext(parsedOptions));
252-
253-
expect(npmMock.getPublishedVersions('bar')).toBeUndefined();
254-
255-
expect(npmMock.getPublishedVersions('foo')).toEqual({
256-
versions: ['1.1.0'],
257-
'dist-tags': { latest: '1.1.0' },
258-
});
259-
260-
repo.checkout(defaultBranchName);
261-
repo.pull();
262-
expect(repo.getCurrentTags()).toEqual(['foo_v1.1.0']);
263-
264-
const newPackageInfos = getPackageInfos(parsedOptions);
265-
expect(newPackageInfos.foo.version).toBe('1.1.0');
266-
});
267-
268-
it('publishes dependent packages in a monorepo', async () => {
248+
it('publishes changed and dependent packages in a monorepo', async () => {
269249
repositoryFactory = new RepositoryFactory('monorepo');
270250
repo = repositoryFactory.cloneRepository();
271251

@@ -280,7 +260,7 @@ describe('publish command (e2e)', () => {
280260
// For this test, run validate first to simulate what the CLI does
281261
validate(parsedOptions, { checkDependencies: true });
282262

283-
await publish(options, createCommandContext(parsedOptions));
263+
await publishWrapper(parsedOptions);
284264

285265
expect(npmMock.getPublishedVersions('baz')).toEqual({ versions: ['1.4.0'], 'dist-tags': { latest: '1.4.0' } });
286266

@@ -314,7 +294,7 @@ describe('publish command (e2e)', () => {
314294
generateChangeFiles(['foo'], options);
315295
repo.push();
316296

317-
await publish(options, createCommandContext(parsedOptions));
297+
await publishWrapper(parsedOptions);
318298

319299
expect(npmMock.getPublishedVersions('foo')).toEqual({ versions: ['1.1.0'], 'dist-tags': { latest: '1.1.0' } });
320300
expect(npmMock.getPublishedVersions('bar')).toEqual({ versions: ['1.3.4'], 'dist-tags': { latest: '1.3.4' } });
@@ -337,7 +317,7 @@ describe('publish command (e2e)', () => {
337317
expect(getChangeFiles(options)).toHaveLength(2);
338318
repo.push();
339319

340-
await publish(options, createCommandContext(parsedOptions));
320+
await publishWrapper(parsedOptions);
341321

342322
expect(npmMock.getPublishedVersions('foo')).toBeUndefined();
343323
expect(npmMock.getPublishedVersions('bar')).toEqual({
@@ -375,9 +355,13 @@ describe('publish command (e2e)', () => {
375355
const { options, parsedOptions } = getOptions({
376356
fetch: false,
377357
hooks: {
378-
prepublish: (packagePath: string) => {
358+
prepublish: (packagePath, name, version) => {
379359
const packageJsonPath = path.join(packagePath, 'package.json');
380360
const packageJson = readJson<ExtraPackageJson>(packageJsonPath);
361+
if (name === 'foo') {
362+
expect(version).toBe('1.1.0');
363+
expect(packageJson.version).toBe('1.1.0'); // bumped version
364+
}
381365
if (packageJson.customOnPublish) {
382366
Object.assign(packageJson, packageJson.customOnPublish);
383367
delete packageJson.customOnPublish;
@@ -397,7 +381,7 @@ describe('publish command (e2e)', () => {
397381
generateChangeFiles(['foo'], options);
398382
repo.push();
399383

400-
await publish(options, createCommandContext(parsedOptions));
384+
await publishWrapper(parsedOptions);
401385

402386
// Query the information from package.json from the registry to see if it was successfully patched
403387
const publishedFooJson = npmMock.getPublishedPackage('foo')!;
@@ -416,69 +400,6 @@ describe('publish command (e2e)', () => {
416400
expect(notified).toBe(fooJsonPost.customAfterPublish?.notify);
417401
});
418402

419-
it('specifies fetch depth when depth param is defined', async () => {
420-
repositoryFactory = new RepositoryFactory('single');
421-
repo = repositoryFactory.cloneRepository();
422-
423-
const { options, parsedOptions } = getOptions({
424-
depth: 10,
425-
});
426-
427-
generateChangeFiles(['foo'], options);
428-
repo.push();
429-
430-
let fetchCommand = '';
431-
432-
addGitObserver(args => {
433-
if (args[0] === 'fetch') {
434-
fetchCommand = args.join(' ');
435-
}
436-
});
437-
438-
await publish(options, createCommandContext(parsedOptions));
439-
440-
expect(npmMock.getPublishedVersions('foo')).toEqual({
441-
versions: ['1.1.0'],
442-
'dist-tags': { latest: '1.1.0' },
443-
});
444-
445-
expect(fetchCommand).toMatch('--depth=10');
446-
});
447-
448-
it('calls precommit hook before committing changes', async () => {
449-
repositoryFactory = new RepositoryFactory('monorepo');
450-
repo = repositoryFactory.cloneRepository();
451-
452-
const { options, parsedOptions } = getOptions({
453-
publish: false, // irrelevant to this test
454-
fetch: false,
455-
hooks: {
456-
precommit: jest.fn(async (cwd: string) => {
457-
expect(readJson<PackageJson>(path.join(cwd, 'packages/foo/package.json')).version).toBe('1.1.0');
458-
const filePath = path.join(cwd, 'foo.txt');
459-
await fsPromises.writeFile(filePath, 'foo');
460-
}),
461-
},
462-
});
463-
464-
generateChangeFiles(['foo', 'bar'], options);
465-
repo.push();
466-
467-
await publish(options, createCommandContext(parsedOptions));
468-
469-
// precommit was called (once for whole repo, not per package)
470-
expect(options.hooks?.precommit).toHaveBeenCalledTimes(1);
471-
// but changes from publish process were reverted locally
472-
const txtPath = repo.pathTo('foo.txt');
473-
expect(fs.existsSync(txtPath)).toBe(false);
474-
475-
repo.checkout(defaultBranchName);
476-
repo.pull();
477-
478-
// changes from publish process were committed
479-
expect(fs.existsSync(txtPath)).toBe(true);
480-
});
481-
482403
it('respects concurrency limit when publishing multiple packages', async () => {
483404
const packagesToPublish = ['pkg1', 'pkg2', 'pkg3', 'pkg4', 'pkg5'];
484405
const packages: { [packageName: string]: PackageJsonFixture } = {};
@@ -511,6 +432,7 @@ describe('publish command (e2e)', () => {
511432
return result;
512433
});
513434

435+
// skip validate for this test since it's not relevant
514436
await publish(options, createCommandContext(parsedOptions));
515437
// Verify that at most `concurrency` number of packages were published concurrently
516438
expect(maxConcurrency).toBe(concurrency);
@@ -556,6 +478,7 @@ describe('publish command (e2e)', () => {
556478
return _mockNpmPublish(registryData, args, opts);
557479
});
558480

481+
// skip validate for this test since it's not relevant
559482
await expect(publish(options, createCommandContext(parsedOptions))).rejects.toThrow(
560483
'Error publishing! Refer to the previous logs for recovery instructions.'
561484
);
@@ -620,6 +543,7 @@ describe('publish command (e2e)', () => {
620543

621544
generateChangeFiles(packagesToPublish, options);
622545

546+
// skip validate for this test since it's not relevant
623547
await publish(options, createCommandContext(parsedOptions));
624548
// Verify that at most `concurrency` number of postpublish hooks were running concurrently
625549
expect(maxConcurrency).toBe(concurrency);

0 commit comments

Comments
 (0)