Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

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

## [5.3.0]

### Added
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,41 @@ or

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

### Check Dependencies

#### Check and validate dependency bump changelog entries

This command detects dependency and peerDependency version changes from git diffs and validates that corresponding changelog entries exist.

`yarn run auto-changelog check-deps --from <git-ref>`

or

`npm run auto-changelog check-deps --from <git-ref>`

#### Auto-fix missing dependency bump entries

Use the `--fix` flag to automatically add missing changelog entries for detected dependency bumps:

`yarn run auto-changelog check-deps --from <git-ref> --fix --pr 123`

Options:

- `--from <ref>` - Starting git reference (commit, branch, or tag). If not provided, auto-detects from merge base with default branch.
- `--to <ref>` - Ending git reference (default: HEAD)
- `--default-branch <branch>` - Default branch name for auto-detection (default: main)
- `--fix` - Automatically update changelogs with missing dependency bump entries
- `--pr <number>` - PR number to use in changelog entries (uses placeholder if not provided)

Features:

- Automatically detects dependency/peerDependency version changes (skips devDependencies/optionalDependencies)
- Validates changelog entries with exact version matching (catches stale entries)
- Marks peerDependency bumps as **BREAKING** changes
- Smart PR concatenation when same dependency is bumped multiple times
- Detects package releases and adds entries to correct section (Unreleased vs specific version)
- Handles renamed packages via package.json script hints

## API Usage

Each supported command is a separate named export.
Expand Down Expand Up @@ -175,6 +210,28 @@ try {
}
```

### `checkDependencyBumps`

This command checks for dependency version bumps and validates/updates changelog entries.

```javascript
import { checkDependencyBumps } from '@metamask/auto-changelog';

const result = await checkDependencyBumps({
projectRoot: '/path/to/project',
fromRef: 'main', // or a commit SHA
toRef: 'HEAD',
fix: true, // automatically update changelogs
prNumber: '123', // PR number for changelog entries
repoUrl: 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository',
stdout: process.stdout,
stderr: process.stderr,
});

// result contains detected dependency changes per package
console.log(result);
```

## Contributing

### Setup
Expand Down
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ module.exports = {
coverageReporters: ['text', 'html'],
coverageThreshold: {
global: {
branches: 45,
functions: 55,
lines: 40,
statements: 40,
branches: 66,
functions: 77,
lines: 75,
statements: 75,
},
},
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
Expand Down
142 changes: 142 additions & 0 deletions src/check-dependency-bumps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import execa from 'execa';
import * as fs from 'fs';
import path from 'path';

import { checkDependencyBumps } from './check-dependency-bumps';
import {
updateDependencyChangelogs,
validateDependencyChangelogs,
} from './dependency-changelog';

jest.mock('execa');
jest.mock('./dependency-changelog');

const execaMock = execa as unknown as jest.MockedFunction<typeof execa>;
const validateMock = validateDependencyChangelogs as jest.MockedFunction<
typeof validateDependencyChangelogs
>;
const updateMock = updateDependencyChangelogs as jest.MockedFunction<
typeof updateDependencyChangelogs
>;

const stdout = { write: jest.fn() };
const stderr = { write: jest.fn() };

describe('check-dependency-bumps', () => {
beforeEach(() => {
jest.resetAllMocks();
stdout.write.mockReset();
stderr.write.mockReset();
});

it('returns empty when on default branch without fromRef', async () => {
execaMock.mockResolvedValueOnce({ stdout: 'main' } as any);

const result = await checkDependencyBumps({
projectRoot: '/repo',
stdout,
stderr,
});

expect(result).toStrictEqual({});
expect(execaMock).toHaveBeenCalledWith(
'git',
['rev-parse', '--abbrev-ref', 'HEAD'],
expect.objectContaining({ cwd: '/repo' }),
);
});

it('returns empty when no package.json changes are found', async () => {
execaMock.mockResolvedValueOnce({ stdout: '' } as any);

const result = await checkDependencyBumps({
projectRoot: '/repo',
fromRef: 'abc123',
stdout,
stderr,
});

expect(result).toStrictEqual({});
expect(stdout.write).toHaveBeenCalledWith(
expect.stringContaining('No package.json changes found'),
);
});

it('detects dependency bumps and triggers validation and fixing', async () => {
const diffWithDeps = `
diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json
index 1234567..890abcd 100644
--- a/packages/controller-utils/package.json
+++ b/packages/controller-utils/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
- "@metamask/transaction-controller": "^61.0.0"
+ "@metamask/transaction-controller": "^62.0.0"
}
}
`;

execaMock.mockResolvedValueOnce({ stdout: diffWithDeps } as any);

jest
.spyOn(fs.promises, 'readFile')
.mockImplementation(
async (filePath: fs.PathLike | fs.promises.FileHandle) => {
const asString = filePath.toString();

if (
asString.endsWith(
path.join('packages', 'controller-utils', 'package.json'),
)
) {
return JSON.stringify({ name: '@metamask/controller-utils' });
}

throw new Error(`Unexpected read: ${asString}`);
},
);

validateMock.mockResolvedValue([
{
package: 'controller-utils',
hasChangelog: true,
hasUnreleasedSection: true,
missingEntries: [],
existingEntries: ['@metamask/transaction-controller'],
checkedVersion: null,
},
]);
updateMock.mockResolvedValue(1);

const result = await checkDependencyBumps({
projectRoot: '/repo',
fromRef: 'abc123',
repoUrl: 'https://github.com/example/repo',
fix: true,
stdout,
stderr,
});

expect(result).toStrictEqual({
// eslint-disable-next-line @typescript-eslint/naming-convention
'controller-utils': {
packageName: '@metamask/controller-utils',
dependencyChanges: [
{
package: 'controller-utils',
dependency: '@metamask/transaction-controller',
type: 'dependencies',
oldVersion: '^61.0.0',
newVersion: '^62.0.0',
},
],
},
});
expect(validateMock).toHaveBeenCalled();
expect(updateMock).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ repoUrl: 'https://github.com/example/repo' }),
);
});
});
Loading
Loading