Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ linkStyle default opacity:0.5
changesets_changelog(["@metamask/changesets-changelog"]);
changesets_utils(["@metamask/changesets-utils"]);
create_release_branch(["@metamask/create-release-branch"]);
changesets_changelog --> changesets_utils;
changesets_changelog --> apply_release_plan;
```

<!-- end dependency graph -->
Expand Down
8 changes: 8 additions & 0 deletions packages/changesets-changelog/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add the Changesets changelog module that renders "Keep a Changelog" release entries ([#83](https://github.com/MetaMask/release-tools/pull/83))
- Exports `getReleaseLine`, `getDependencyReleaseLine`, `categorizeReleaseLine`, `categorizeDependencyReleaseLine`, `getVersionHeader`, and `categories`.
- Renders one section per category, in the order `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`, and keeps sub-bullets and continuation lines.
- Prefixes entries of a `major` release with `**BREAKING:**`, and puts dependency bumps into `Changed`.
- Resolves pull request links from the local commit subject with `git show`, so it needs no GitHub token and makes no network call. Rendering fails if no pull request number is found.

[Unreleased]: https://github.com/MetaMask/release-tools/
50 changes: 50 additions & 0 deletions packages/changesets-changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Changesets changelog module for MetaMask

This is the changelog module that [Changesets](https://github.com/changesets/changesets) calls to render release entries. It writes ["Keep a Changelog"](https://keepachangelog.com/) sections instead of the default `### Minor Changes` sections, in the format MetaMask changelogs use.

## Installation

`yarn add @metamask/changesets-changelog`
Expand All @@ -10,6 +12,54 @@ or

`npm install @metamask/changesets-changelog`

## Usage

Point the `changelog` field of your Changesets config at this package, and pass the GitHub repository that the pull request links refer to:

```json
{
"changelog": ["@metamask/changesets-changelog", { "repo": "MetaMask/core" }]
}
```

This module requires [`@metamask/apply-release-plan`](../apply-release-plan), which renders the categorized sections. The stock `@changesets/cli` ignores the categories and the version header.

### Entries

Each changeset holds one entry: a category heading and one top-level bullet. For example:

```markdown
---
'@metamask/base-controller': minor
---

## Added

- Add `getNetworkClient` for looking up a client by chain ID
- The client is cached per chain.
```

That changeset renders as:

```markdown
## [1.1.0]

### Added

- Add `getNetworkClient` for looking up a client by chain ID ([#1234](https://github.com/MetaMask/core/pull/1234))
- The client is cached per chain.
```

The supported categories are `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`. Sections appear in that order. Sub-bullets and continuation lines are kept below the entry.

Entries of a `major` release get a `**BREAKING:**` prefix. Dependency bumps go into `Changed`.

### Pull request links

The pull request number comes from the commit that added the changeset file. The module reads the commit subject with `git show`, so it needs no GitHub token and makes no network call. It supports squash subjects such as `Add a feature (#1234)` and merge subjects such as `Merge pull request #1234 from owner/branch`.

Rendering fails if the pull request number cannot be found, so that no entry is published without a link.

## Contributing

This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/release-tools#readme).
8 changes: 8 additions & 0 deletions packages/changesets-changelog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,16 @@
"test:verbose": "vitest --reporter=verbose",
"test:watch": "vitest --watch"
},
"dependencies": {
"@changesets/types": "^7.0.0",
"@metamask/changesets-utils": "^0.0.0",
"execa": "^8.0.1"
},
"devDependencies": {
"@manypkg/get-packages": "^3.1.0",
"@metamask/apply-release-plan": "^0.0.0",
"@metamask/auto-changelog": "^6.1.0",
"@types/node": "^22.13.14",
"@typescript/native": "npm:typescript@^7.0.2",
"@vitest/coverage-istanbul": "^5.0.1",
"rimraf": "^5.0.5",
Expand Down
16 changes: 16 additions & 0 deletions packages/changesets-changelog/src/categories.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';

import { categories } from './categories.js';

describe('categories', () => {
it('lists the Keep a Changelog categories in render order', () => {
expect(categories).toStrictEqual([
'Added',
'Changed',
'Deprecated',
'Removed',
'Fixed',
'Security',
]);
});
});
13 changes: 13 additions & 0 deletions packages/changesets-changelog/src/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* The changelog sections this module can produce, in the order they are
* rendered. These are the "Keep a Changelog" categories, which are also the
* categories `parseChangesetSummary` accepts in a changeset heading.
*/
export const categories = [
'Added',
'Changed',
'Deprecated',
'Removed',
'Fixed',
'Security',
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { ModCompWithPackage } from '@changesets/types';
import { describe, expect, it } from 'vitest';

import {
categorizeDependencyReleaseLine,
getDependencyReleaseLine,
} from './get-dependency-release-line.js';

/**
* Builds an updated dependency.
*
* @param overrides - The fields to override.
* @returns The dependency.
*/
function buildDependency(
overrides: Partial<ModCompWithPackage> = {},
): ModCompWithPackage {
return {
name: '@metamask/base-controller',
type: 'minor',
oldVersion: '1.2.0',
newVersion: '1.3.0',
changesets: [],
packageJson: { name: '@metamask/base-controller', version: '1.2.0' },
dir: '/packages/base-controller',
...overrides,
} as ModCompWithPackage;
}

describe('getDependencyReleaseLine', () => {
it('returns an empty string when no dependency changed', async () => {
expect(
await getDependencyReleaseLine([], [], { repo: 'MetaMask/core' }),
).toBe('');
});

it('renders one caret-ranged line per dependency', async () => {
expect(
await getDependencyReleaseLine(
[],
[
buildDependency(),
buildDependency({
name: '@metamask/utils',
oldVersion: '^9.0.0',
newVersion: '^10.0.0',
}),
],
{ repo: 'MetaMask/core' },
),
).toBe(
[
'- Bump `@metamask/base-controller` from `^1.2.0` to `^1.3.0`',
'- Bump `@metamask/utils` from `^9.0.0` to `^10.0.0`',
].join('\n'),
);
});

it('throws when the options have no repo', async () => {
await expect(
getDependencyReleaseLine([], [buildDependency()], null),
).rejects.toThrow('Please provide a repo to this changelog generator');
});

it.each([
{ oldVersion: undefined, newVersion: '1.3.0' },
{ oldVersion: '1.2.0', newVersion: undefined },
])('throws when a version is missing', async (versions) => {
await expect(
getDependencyReleaseLine([], [buildDependency(versions)], {
repo: 'MetaMask/core',
}),
).rejects.toThrow(
'Dependency @metamask/base-controller must have old and new versions',
);
});
});

describe('categorizeDependencyReleaseLine', () => {
it('assigns every line to Changed', async () => {
const first = '- Bump `pkg-a` from `^1.0.0` to `^1.1.0`';
const second = '- Bump `pkg-b` from `^2.0.0` to `^2.1.0`';

expect(
await categorizeDependencyReleaseLine(`${first}\n${second}`),
).toStrictEqual([
{ category: 'Changed', line: first },
{ category: 'Changed', line: second },
]);
});

it('drops empty lines', async () => {
expect(await categorizeDependencyReleaseLine('')).toStrictEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
ModCompWithPackage,
NewChangesetWithCommit,
} from '@changesets/types';

import { getRepo } from './options.js';
import type { CategorizedReleaseLine, ChangelogOptions } from './types.js';

/**
* Renders dependency version changes as changelog lines.
*
* @param _changesets - The changesets in the release.
* @param dependenciesUpdated - The dependencies updated by the release.
* @param changelogOpts - The changelog options. Must contain `repo`.
* @returns One line per updated dependency.
*/
export async function getDependencyReleaseLine(
_changesets: NewChangesetWithCommit[],
dependenciesUpdated: ModCompWithPackage[],
changelogOpts: ChangelogOptions,
): Promise<string> {
getRepo(changelogOpts);

return dependenciesUpdated
.map((dependency) => {
if (!dependency.oldVersion || !dependency.newVersion) {
throw new Error(
`Dependency ${dependency.name} must have old and new versions`,
);
}

const oldVersion = dependency.oldVersion.startsWith('^')
? dependency.oldVersion
: `^${dependency.oldVersion}`;
const newVersion = dependency.newVersion.startsWith('^')
? dependency.newVersion
: `^${dependency.newVersion}`;

return `- Bump \`${dependency.name}\` from \`${oldVersion}\` to \`${newVersion}\``;
})
.join('\n');
}

/**
* Assigns dependency lines to the `Changed` category.
*
* @param line - The lines rendered by `getDependencyReleaseLine`.
* @returns The non-empty lines and their category.
*/
export async function categorizeDependencyReleaseLine(
line: string,
): Promise<CategorizedReleaseLine[]> {
return line
.split('\n')
.filter((item) => item.trim() !== '')
.map((item) => ({ category: 'Changed', line: item }));
}
Loading
Loading