Skip to content

Commit 269e2f2

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

File tree

8 files changed

+1769
-5
lines changed

8 files changed

+1769
-5
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `check-deps` command to validate and update dependency bump changelog entries ([#267](https://github.com/MetaMask/auto-changelog/pull/267))
13+
- Automatically detects dependency/peerDependency version changes from git diffs (skips dev/optional deps)
14+
- Validates changelog entries with exact version matching (catches stale entries)
15+
- Auto-updates changelogs with `--fix` flag, preserving PR history
16+
- Detects package releases and validates/updates in correct changelog section (Unreleased vs specific version)
17+
- Smart PR concatenation when same dependency bumped multiple times
18+
- Handles renamed packages via package.json script hints
19+
- Usage: `yarn auto-changelog check-deps --from <ref> [--fix] [--pr <number>]`
20+
1021
## [5.3.0]
1122

1223
### Added

src/check-dependency-bumps.test.ts

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

0 commit comments

Comments
 (0)