Skip to content

Commit a6b2b18

Browse files
committed
feat: add automated dependency bump checker and changelog validator
1 parent 55c8ffe commit a6b2b18

File tree

7 files changed

+1477
-5
lines changed

7 files changed

+1477
-5
lines changed

src/check-dependency-bumps.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import path from 'path';
2+
import * as fs from 'fs';
3+
4+
import execa from 'execa';
5+
6+
import { checkDependencyBumps } from './check-dependency-bumps';
7+
import {
8+
updateDependencyChangelogs,
9+
validateDependencyChangelogs,
10+
} from './dependency-changelog';
11+
12+
jest.mock('execa');
13+
jest.mock('./dependency-changelog');
14+
15+
const execaMock = execa as unknown as jest.MockedFunction<typeof execa>;
16+
const validateMock = validateDependencyChangelogs as jest.MockedFunction<
17+
typeof validateDependencyChangelogs
18+
>;
19+
const updateMock = updateDependencyChangelogs as jest.MockedFunction<
20+
typeof updateDependencyChangelogs
21+
>;
22+
23+
const stdout = { write: jest.fn() };
24+
const stderr = { write: jest.fn() };
25+
26+
describe('check-dependency-bumps', () => {
27+
beforeEach(() => {
28+
jest.resetAllMocks();
29+
stdout.write.mockReset();
30+
stderr.write.mockReset();
31+
});
32+
33+
it('returns empty when on default branch without fromRef', async () => {
34+
execaMock.mockResolvedValueOnce({ stdout: 'main' } as any);
35+
36+
const result = await checkDependencyBumps({
37+
projectRoot: '/repo',
38+
stdout,
39+
stderr,
40+
});
41+
42+
expect(result).toStrictEqual({});
43+
expect(execaMock).toHaveBeenCalledWith(
44+
'git',
45+
['rev-parse', '--abbrev-ref', 'HEAD'],
46+
expect.objectContaining({ cwd: '/repo' }),
47+
);
48+
});
49+
50+
it('returns empty when no package.json changes are found', async () => {
51+
execaMock.mockResolvedValueOnce({ stdout: '' } as any);
52+
53+
const result = await checkDependencyBumps({
54+
projectRoot: '/repo',
55+
fromRef: 'abc123',
56+
stdout,
57+
stderr,
58+
});
59+
60+
expect(result).toStrictEqual({});
61+
expect(stdout.write).toHaveBeenCalledWith(
62+
expect.stringContaining('No package.json changes found'),
63+
);
64+
});
65+
66+
it('detects dependency bumps and triggers validation and fixing', async () => {
67+
const diffWithDeps = `
68+
diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json
69+
index 1234567..890abcd 100644
70+
--- a/packages/controller-utils/package.json
71+
+++ b/packages/controller-utils/package.json
72+
@@ -10,7 +10,7 @@
73+
},
74+
"dependencies": {
75+
- "@metamask/transaction-controller": "^61.0.0"
76+
+ "@metamask/transaction-controller": "^62.0.0"
77+
}
78+
}
79+
`;
80+
81+
execaMock.mockResolvedValueOnce({ stdout: diffWithDeps } as any);
82+
83+
jest
84+
.spyOn(fs.promises, 'readFile')
85+
.mockImplementation(async (filePath: fs.PathLike | fs.promises.FileHandle) => {
86+
const asString = filePath.toString();
87+
88+
if (asString.endsWith(path.join('packages', 'controller-utils', 'package.json'))) {
89+
return JSON.stringify({ name: '@metamask/controller-utils' });
90+
}
91+
92+
throw new Error(`Unexpected read: ${asString}`);
93+
});
94+
95+
validateMock.mockResolvedValue([
96+
{
97+
package: 'controller-utils',
98+
hasChangelog: true,
99+
hasUnreleasedSection: true,
100+
missingEntries: [],
101+
existingEntries: ['@metamask/transaction-controller'],
102+
checkedVersion: null,
103+
},
104+
]);
105+
updateMock.mockResolvedValue(1);
106+
107+
const result = await checkDependencyBumps({
108+
projectRoot: '/repo',
109+
fromRef: 'abc123',
110+
repoUrl: 'https://github.com/example/repo',
111+
fix: true,
112+
stdout,
113+
stderr,
114+
});
115+
116+
expect(result).toStrictEqual({
117+
'controller-utils': {
118+
packageName: '@metamask/controller-utils',
119+
dependencyChanges: [
120+
{
121+
package: 'controller-utils',
122+
dependency: '@metamask/transaction-controller',
123+
type: 'dependencies',
124+
oldVersion: '^61.0.0',
125+
newVersion: '^62.0.0',
126+
},
127+
],
128+
},
129+
});
130+
expect(validateMock).toHaveBeenCalled();
131+
expect(updateMock).toHaveBeenCalledWith(
132+
expect.any(Object),
133+
expect.objectContaining({ repoUrl: 'https://github.com/example/repo' }),
134+
);
135+
});
136+
});
137+

0 commit comments

Comments
 (0)