Skip to content

Commit 3d0c830

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

File tree

11 files changed

+3847
-9
lines changed

11 files changed

+3847
-9
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

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,41 @@ or
124124

125125
`npm run auto-changelog validate --pr-links`
126126

127+
### Check Dependencies
128+
129+
#### Check and validate dependency bump changelog entries
130+
131+
This command detects dependency and peerDependency version changes from git diffs and validates that corresponding changelog entries exist.
132+
133+
`yarn run auto-changelog check-deps --from <git-ref>`
134+
135+
or
136+
137+
`npm run auto-changelog check-deps --from <git-ref>`
138+
139+
#### Auto-fix missing dependency bump entries
140+
141+
Use the `--fix` flag to automatically add missing changelog entries for detected dependency bumps:
142+
143+
`yarn run auto-changelog check-deps --from <git-ref> --fix --pr 123`
144+
145+
Options:
146+
147+
- `--from <ref>` - Starting git reference (commit, branch, or tag). If not provided, auto-detects from merge base with default branch.
148+
- `--to <ref>` - Ending git reference (default: HEAD)
149+
- `--default-branch <branch>` - Default branch name for auto-detection (default: main)
150+
- `--fix` - Automatically update changelogs with missing dependency bump entries
151+
- `--pr <number>` - PR number to use in changelog entries (uses placeholder if not provided)
152+
153+
Features:
154+
155+
- Automatically detects dependency/peerDependency version changes (skips devDependencies/optionalDependencies)
156+
- Validates changelog entries with exact version matching (catches stale entries)
157+
- Marks peerDependency bumps as **BREAKING** changes
158+
- Smart PR concatenation when same dependency is bumped multiple times
159+
- Detects package releases and adds entries to correct section (Unreleased vs specific version)
160+
- Handles renamed packages via package.json script hints
161+
127162
## API Usage
128163

129164
Each supported command is a separate named export.
@@ -175,6 +210,28 @@ try {
175210
}
176211
```
177212

213+
### `checkDependencyBumps`
214+
215+
This command checks for dependency version bumps and validates/updates changelog entries.
216+
217+
```javascript
218+
import { checkDependencyBumps } from '@metamask/auto-changelog';
219+
220+
const result = await checkDependencyBumps({
221+
projectRoot: '/path/to/project',
222+
fromRef: 'main', // or a commit SHA
223+
toRef: 'HEAD',
224+
fix: true, // automatically update changelogs
225+
prNumber: '123', // PR number for changelog entries
226+
repoUrl: 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository',
227+
stdout: process.stdout,
228+
stderr: process.stderr,
229+
});
230+
231+
// result contains detected dependency changes per package
232+
console.log(result);
233+
```
234+
178235
## Contributing
179236

180237
### Setup

jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ module.exports = {
77
coverageReporters: ['text', 'html'],
88
coverageThreshold: {
99
global: {
10-
branches: 45,
11-
functions: 55,
12-
lines: 40,
13-
statements: 40,
10+
branches: 66,
11+
functions: 77,
12+
lines: 75,
13+
statements: 75,
1414
},
1515
},
1616
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],

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)