Skip to content

Commit b184da0

Browse files
authored
fix(build): ensure to check spawn result and assume files unchanged MONGOSH-521 (#599)
* ensure to check spawn result and assume files unchanged - uses a helper function to ensure spawn.sync calls throw if not successful - before lerna publish marks known changed files as assume-unchanged and reverts after publish Signed-off-by: Michael Rose <[email protected]> * increase test coverage * fixup: increase test coverage
1 parent 84fa715 commit b184da0

File tree

2 files changed

+280
-30
lines changed

2 files changed

+280
-30
lines changed
Lines changed: 200 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,203 @@
11
import { expect } from 'chai';
2-
import { listNpmPackages } from './npm-packages';
3-
4-
describe('listNpmPackages', () => {
5-
it('lists packages', () => {
6-
const packages = listNpmPackages();
7-
expect(packages.length).to.be.greaterThan(1);
8-
for (const { name, version } of packages) {
9-
expect(name).to.be.a('string');
10-
expect(version).to.be.a('string');
11-
}
2+
import path from 'path';
3+
import sinon, { SinonStub } from 'sinon';
4+
import {
5+
bumpNpmPackages,
6+
listNpmPackages,
7+
markBumpedFilesAsAssumeUnchanged,
8+
publishNpmPackages,
9+
spawnSync
10+
} from './npm-packages';
11+
12+
13+
describe('npm-packages', () => {
14+
describe('spawnSync', () => {
15+
it('works for a valid command', () => {
16+
const result = spawnSync('bash', ['-c', 'echo -n works'], { encoding: 'utf8' });
17+
expect(result.status).to.equal(0);
18+
expect(result.stdout).to.equal('works');
19+
});
20+
21+
it('throws on ENOENT error', () => {
22+
try {
23+
spawnSync('notaprogram', [], { encoding: 'utf8' });
24+
} catch (e) {
25+
return expect(e).to.not.be.undefined;
26+
}
27+
expect.fail('Expected error');
28+
});
29+
30+
it('throws on non-zero exit code', () => {
31+
try {
32+
spawnSync('bash', ['-c', 'exit 1'], { encoding: 'utf8' });
33+
} catch (e) {
34+
return expect(e).to.not.be.undefined;
35+
}
36+
expect.fail('Expected error');
37+
});
38+
});
39+
40+
describe('bumpNpmPackages', () => {
41+
let spawnSync: SinonStub;
42+
43+
beforeEach(() => {
44+
spawnSync = sinon.stub();
45+
});
46+
47+
it('does not do anything if no version or placeholder version is specified', () => {
48+
bumpNpmPackages('', spawnSync);
49+
bumpNpmPackages('0.0.0-dev.0', spawnSync);
50+
expect(spawnSync).to.not.have.been.called;
51+
});
52+
53+
it('calls lerna to bump package version', () => {
54+
const lernaBin = path.resolve(__dirname, '..', '..', '..', 'node_modules', '.bin', 'lerna');
55+
bumpNpmPackages('0.7.0', spawnSync);
56+
expect(spawnSync).to.have.been.calledWith(
57+
lernaBin,
58+
['version', '0.7.0', '--no-changelog', '--no-push', '--exact', '--no-git-tag-version', '--force-publish', '--yes'],
59+
sinon.match.any
60+
);
61+
});
62+
});
63+
64+
describe('publishNpmPackages', () => {
65+
let listNpmPackages: SinonStub;
66+
let markBumpedFilesAsAssumeUnchanged: SinonStub;
67+
let spawnSync: SinonStub;
68+
69+
beforeEach(() => {
70+
listNpmPackages = sinon.stub();
71+
markBumpedFilesAsAssumeUnchanged = sinon.stub();
72+
spawnSync = sinon.stub();
73+
});
74+
75+
it('fails if packages have different versions', () => {
76+
listNpmPackages.returns([
77+
{ name: 'packageA', version: '0.0.1' },
78+
{ name: 'packageB', version: '0.0.2' }
79+
]);
80+
try {
81+
publishNpmPackages(
82+
listNpmPackages,
83+
markBumpedFilesAsAssumeUnchanged,
84+
spawnSync
85+
);
86+
} catch (e) {
87+
expect(markBumpedFilesAsAssumeUnchanged).to.not.have.been.called;
88+
expect(spawnSync).to.not.have.been.called;
89+
return;
90+
}
91+
expect.fail('Expected error');
92+
});
93+
94+
it('fails if packages have placeholder versions', () => {
95+
listNpmPackages.returns([
96+
{ name: 'packageA', version: '0.0.0-dev.0' },
97+
{ name: 'packageB', version: '0.0.0-dev.0' }
98+
]);
99+
try {
100+
publishNpmPackages(
101+
listNpmPackages,
102+
markBumpedFilesAsAssumeUnchanged,
103+
spawnSync
104+
);
105+
} catch (e) {
106+
expect(markBumpedFilesAsAssumeUnchanged).to.not.have.been.called;
107+
expect(spawnSync).to.not.have.been.called;
108+
return;
109+
}
110+
expect.fail('Expected error');
111+
});
112+
113+
it('calls lerna to publish packages for a real version', () => {
114+
const lernaBin = path.resolve(__dirname, '..', '..', '..', 'node_modules', '.bin', 'lerna');
115+
const packages = [
116+
{ name: 'packageA', version: '0.7.0' }
117+
];
118+
listNpmPackages.returns(packages);
119+
120+
publishNpmPackages(
121+
listNpmPackages,
122+
markBumpedFilesAsAssumeUnchanged,
123+
spawnSync
124+
);
125+
126+
expect(markBumpedFilesAsAssumeUnchanged).to.have.been.calledWith(packages, true);
127+
expect(spawnSync).to.have.been.calledWith(
128+
lernaBin,
129+
['publish', 'from-package', '--no-changelog', '--no-push', '--exact', '--no-git-tag-version', '--force-publish', '--yes'],
130+
sinon.match.any
131+
);
132+
expect(markBumpedFilesAsAssumeUnchanged).to.have.been.calledWith(packages, false);
133+
});
134+
135+
it('reverts the assume unchanged even on spawn failure', () => {
136+
const packages = [
137+
{ name: 'packageA', version: '0.7.0' }
138+
];
139+
listNpmPackages.returns(packages);
140+
spawnSync.throws(new Error('meeep'));
141+
142+
try {
143+
publishNpmPackages(
144+
listNpmPackages,
145+
markBumpedFilesAsAssumeUnchanged,
146+
spawnSync
147+
);
148+
} catch (e) {
149+
expect(markBumpedFilesAsAssumeUnchanged).to.have.been.calledWith(packages, true);
150+
expect(spawnSync).to.have.been.called;
151+
expect(markBumpedFilesAsAssumeUnchanged).to.have.been.calledWith(packages, false);
152+
return;
153+
}
154+
expect.fail('Expected error');
155+
});
156+
});
157+
158+
describe('listNpmPackages', () => {
159+
it('lists packages', () => {
160+
const packages = listNpmPackages();
161+
expect(packages.length).to.be.greaterThan(1);
162+
for (const { name, version } of packages) {
163+
expect(name).to.be.a('string');
164+
expect(version).to.be.a('string');
165+
}
166+
});
167+
});
168+
169+
describe('markBumpedFilesAsAssumeUnchanged', () => {
170+
let packages: { name: string; version: string }[];
171+
let expectedFiles: string[];
172+
let spawnSync: SinonStub;
173+
174+
beforeEach(() => {
175+
expectedFiles = ['.npmrc', 'lerna.json'];
176+
packages = listNpmPackages();
177+
packages.forEach(({ name }) => {
178+
expectedFiles.push(`packages/${name}/package.json`);
179+
expectedFiles.push(`packages/${name}/package-lock.json`);
180+
});
181+
182+
spawnSync = sinon.stub();
183+
});
184+
185+
it('marks files with --assume-unchanged', () => {
186+
markBumpedFilesAsAssumeUnchanged(packages, true, spawnSync);
187+
expectedFiles.forEach(f => {
188+
expect(spawnSync).to.have.been.calledWith(
189+
'git', ['update-index', '--assume-unchanged', f], sinon.match.any
190+
);
191+
});
192+
});
193+
194+
it('marks files with --no-assume-unchanged', () => {
195+
markBumpedFilesAsAssumeUnchanged(packages, false, spawnSync);
196+
expectedFiles.forEach(f => {
197+
expect(spawnSync).to.have.been.calledWith(
198+
'git', ['update-index', '--no-assume-unchanged', f], sinon.match.any
199+
);
200+
});
201+
});
12202
});
13203
});

packages/build/src/npm-packages.ts

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
1+
import { SpawnSyncOptionsWithStringEncoding, SpawnSyncReturns } from 'child_process';
12
import * as spawn from 'cross-spawn';
23
import path from 'path';
34

45
const PLACEHOLDER_VERSION = '0.0.0-dev.0';
56
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
67
const LERNA_BIN = path.resolve(PROJECT_ROOT, 'node_modules', '.bin', 'lerna');
78

8-
export function bumpNpmPackages(version: string): void {
9+
export function spawnSync(command: string, args: string[], options: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns<string> {
10+
const result = spawn.sync(command, args, options);
11+
if (result.error) {
12+
console.error('spawn.sync returned error', result.error);
13+
console.error(result.stdout);
14+
console.error(result.stderr);
15+
throw new Error(`Failed to spawn ${command}, args: ${args.join(',')}: ${result.error}`);
16+
} else if (result.status !== 0) {
17+
console.error('spawn.sync exited with non-zero', result.status);
18+
console.error(result.stdout);
19+
console.error(result.stderr);
20+
throw new Error(`Spawn exited non-zero for ${command}, args: ${args.join(',')}: ${result.status}`);
21+
}
22+
return result;
23+
}
24+
25+
export function bumpNpmPackages(
26+
version: string,
27+
spawnSyncFn: typeof spawnSync = spawnSync
28+
): void {
929
if (!version || version === PLACEHOLDER_VERSION) {
1030
console.info('mongosh: Not bumping package version, keeping at placeholder');
1131
return;
1232
}
1333

1434
console.info(`mongosh: Bumping package versions to ${version}`);
15-
spawn.sync(LERNA_BIN, [
35+
spawnSyncFn(LERNA_BIN, [
1636
'version',
1737
version,
1838
'--no-changelog',
@@ -23,12 +43,17 @@ export function bumpNpmPackages(version: string): void {
2343
'--yes'
2444
], {
2545
stdio: 'inherit',
26-
cwd: PROJECT_ROOT
46+
cwd: PROJECT_ROOT,
47+
encoding: 'utf8'
2748
});
2849
}
2950

30-
export function publishNpmPackages(): void {
31-
const packages = listNpmPackages();
51+
export function publishNpmPackages(
52+
listNpmPackagesFn: typeof listNpmPackages = listNpmPackages,
53+
markBumpedFilesAsAssumeUnchangedFn: typeof markBumpedFilesAsAssumeUnchanged = markBumpedFilesAsAssumeUnchanged,
54+
spawnSyncFn: typeof spawnSync = spawnSync
55+
): void {
56+
const packages = listNpmPackagesFn();
3257

3358
const versions = Array.from(new Set(packages.map(({ version }) => version)));
3459

@@ -40,23 +65,31 @@ export function publishNpmPackages(): void {
4065
throw new Error('Refusing to publish packages with placeholder version');
4166
}
4267

43-
spawn.sync(LERNA_BIN, [
44-
'publish',
45-
'from-package',
46-
'--no-changelog',
47-
'--no-push',
48-
'--exact',
49-
'--no-git-tag-version',
50-
'--force-publish',
51-
'--yes'
52-
], {
53-
stdio: 'inherit',
54-
cwd: PROJECT_ROOT
55-
});
68+
// Lerna requires a clean repository for a publish from-package (--force-publish does not have any effect here)
69+
// we use git update-index --assume-unchanged on files we know have been bumped
70+
markBumpedFilesAsAssumeUnchangedFn(packages, true);
71+
try {
72+
spawnSyncFn(LERNA_BIN, [
73+
'publish',
74+
'from-package',
75+
'--no-changelog',
76+
'--no-push',
77+
'--exact',
78+
'--no-git-tag-version',
79+
'--force-publish',
80+
'--yes'
81+
], {
82+
stdio: 'inherit',
83+
cwd: PROJECT_ROOT,
84+
encoding: 'utf8'
85+
});
86+
} finally {
87+
markBumpedFilesAsAssumeUnchangedFn(packages, false);
88+
}
5689
}
5790

58-
export function listNpmPackages(): {name: string; version: string}[] {
59-
const lernaListOutput = spawn.sync(
91+
export function listNpmPackages(): { name: string; version: string }[] {
92+
const lernaListOutput = spawnSync(
6093
LERNA_BIN, [
6194
'list',
6295
'--json',
@@ -69,3 +102,30 @@ export function listNpmPackages(): {name: string; version: string}[] {
69102

70103
return JSON.parse(lernaListOutput.stdout);
71104
}
105+
106+
export function markBumpedFilesAsAssumeUnchanged(
107+
packages: { name: string }[], assumeUnchanged: boolean,
108+
spawnSyncFn: typeof spawnSync = spawnSync
109+
): void {
110+
const filesToAssume = [
111+
'.npmrc',
112+
'lerna.json'
113+
];
114+
packages.forEach(({ name }) => {
115+
filesToAssume.push(`packages/${name}/package.json`);
116+
filesToAssume.push(`packages/${name}/package-lock.json`);
117+
});
118+
119+
filesToAssume.forEach(f => {
120+
spawnSyncFn('git', [
121+
'update-index',
122+
assumeUnchanged ? '--assume-unchanged' : '--no-assume-unchanged',
123+
f
124+
], {
125+
stdio: 'inherit',
126+
cwd: PROJECT_ROOT,
127+
encoding: 'utf8'
128+
});
129+
console.info(`File ${f} is now ${assumeUnchanged ? '' : 'NOT '}assumed to be unchanged`);
130+
});
131+
}

0 commit comments

Comments
 (0)