Prevent accidentally writing dep bumps to grouped changelog (and test improvements) (#984)

This commit is contained in:
Elizabeth Craig 2024-09-06 21:32:29 -07:00 коммит произвёл GitHub
Родитель eade30dbb4
Коммит ff4f5da25e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 546 добавлений и 671 удалений

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Prevent accidentally writing dep bumps to grouped changelog",
"packageName": "beachball",
"email": "elcraig@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -2,29 +2,51 @@ import fs from 'fs';
import path from 'path';
import { writeChangeFiles } from '../changefile/writeChangeFiles';
import { getChangePath } from '../paths';
import { ChangeFileInfo } from '../types/ChangeInfo';
import { ChangeFileInfo, ChangeType } from '../types/ChangeInfo';
import type { BeachballOptions } from '../types/BeachballOptions';
/** Change file with `packageName` required and other props optional */
export type PartialChangeFile = { packageName: string } & Partial<ChangeFileInfo>;
/** Placeholder email/author */
export const fakeEmail = 'test@test.com';
/**
* Generate a change file for the given package.
*/
export function getChange(
packageName: string,
comment: string = `${packageName} comment`,
type: ChangeType = 'minor'
): ChangeFileInfo {
return {
comment,
email: fakeEmail,
packageName,
type,
dependentChangeType: 'patch',
};
}
/**
* Generates and writes change files for the given packages.
* Also commits if `options.commit` is true.
* @param changes Array of package names or partial change files (which must include `packageName`).
* Default values are `type: 'minor'`, `dependentChangeType: 'patch'`, and placeholders for other fields.
* Default values:
* - `type: 'minor'`
* - `dependentChangeType: 'patch'`
* - `comment: '<packageName> comment'`
* - `email: 'test@test.com'`
*/
export function generateChangeFiles(
changes: (string | PartialChangeFile)[],
options: Pick<BeachballOptions, 'path' | 'groupChanges' | 'changeDir'>
options: Parameters<typeof writeChangeFiles>[1]
): void {
writeChangeFiles(
changes.map(change => {
change = typeof change === 'string' ? { packageName: change } : change;
return {
comment: `${change.packageName} test comment`,
email: 'test@test.com',
type: 'minor',
dependentChangeType: 'patch',
...getChange(change.packageName, undefined, 'minor'),
...change,
};
}),

Просмотреть файл

@ -1,8 +1,11 @@
import fs from 'fs-extra';
import path from 'path';
import _ from 'lodash';
import { SortedChangeTypes } from '../changefile/changeTypes';
import { ChangelogJson } from '../types/ChangeLog';
import { markerComment } from '../changelog/renderChangelog';
/** Placeholder commit as replaced by cleanChangelogJson */
export const fakeCommit = '(sha1)';
/**
* Read the CHANGELOG.md under the given package path, sanitizing any dates for snapshots.
@ -17,6 +20,11 @@ export function readChangelogMd(packagePath: string): string | null {
return text.replace(/\w\w\w, \d\d \w\w\w [\d :]+?GMT/gm, '(date)');
}
/** Get only the part of CHANGELOG.md after the marker comment. */
export function trimChangelogMd(changelogMd: string): string {
return changelogMd.split(markerComment)[1].trim();
}
/**
* Read the CHANGELOG.json under the given package path.
* Returns null if it doesn't exist.
@ -39,19 +47,17 @@ export function cleanChangelogJson(changelog: ChangelogJson | null): ChangelogJs
return null;
}
changelog = _.cloneDeep(changelog);
// for a better snapshot, make the fake commit match if the real commit did
const fakeCommits: { [commit: string]: string } = {};
let fakeHashNum = 0;
for (const entry of changelog.entries) {
entry.date = '(date)';
for (const changeType of SortedChangeTypes) {
if (entry.comments[changeType]) {
for (const comment of entry.comments[changeType]!) {
if (!fakeCommits[comment.commit]) {
fakeCommits[comment.commit] = `(sha1-${fakeHashNum++})`;
}
comment.commit = fakeCommits[comment.commit];
// Only replace properties if they existed, to help catch bugs if things are no longer written
if (entry.date) {
entry.date = '(date)';
}
for (const comments of Object.values(entry.comments)) {
for (const comment of comments!) {
if (comment.commit) {
comment.commit = fakeCommit;
}
}
}

Просмотреть файл

@ -2,7 +2,13 @@ import path from 'path';
import * as fs from 'fs-extra';
import { tmpdir } from './tmpdir';
import { git } from 'workspace-tools';
import { defaultBranchName, defaultRemoteName, optsWithLang, setDefaultBranchName } from './gitDefaults';
import {
defaultBranchName,
defaultRemoteBranchName,
defaultRemoteName,
optsWithLang,
setDefaultBranchName,
} from './gitDefaults';
import { env } from '../env';
/**
@ -204,6 +210,12 @@ ${gitResult.stderr.toString()}`);
this.git(['push', defaultRemoteName, `HEAD:${branchName}`]);
}
/** `git reset --hard <ref>` and `git clean -dfx` */
resetAndClean(ref: string = defaultRemoteBranchName) {
this.git(['reset', '--hard', ref]);
this.git(['clean', '-dfx']);
}
/**
* Clean up the repo IF this is a local build.
*

Просмотреть файл

@ -15,6 +15,8 @@ describe('readChangeFiles', () => {
let repositoryFactory: RepositoryFactory;
let monoRepoFactory: RepositoryFactory;
let repo: Repository | undefined;
let sharedSingleRepo: Repository;
let sharedMonoRepo: Repository;
const logs = initMockLogs();
@ -28,13 +30,17 @@ describe('readChangeFiles', () => {
}
beforeAll(() => {
// These tests can share the same repo factories because they don't push to origin
// (the actual tests run against a clone)
// These tests can share the same factories and repos because they don't push to the remote,
// and the repo used is reset after each test (which is faster than making new clones).
repositoryFactory = new RepositoryFactory('single');
monoRepoFactory = new RepositoryFactory('monorepo');
sharedSingleRepo = repositoryFactory.cloneRepository();
sharedMonoRepo = monoRepoFactory.cloneRepository();
});
afterEach(() => {
// Revert whichever shared repo was used to the original state
repo?.resetAndClean();
repo = undefined;
});
@ -44,7 +50,7 @@ describe('readChangeFiles', () => {
});
it('does not add commit hash', () => {
repo = repositoryFactory.cloneRepository();
repo = sharedSingleRepo;
repo.commitChange('foo');
const options = getOptions();
@ -57,7 +63,7 @@ describe('readChangeFiles', () => {
});
it('reads from a custom changeDir', () => {
repo = repositoryFactory.cloneRepository();
repo = sharedSingleRepo;
repo.commitChange('foo');
const options = getOptions({ changeDir: 'changeDir' });
@ -69,7 +75,7 @@ describe('readChangeFiles', () => {
});
it('excludes invalid change files', () => {
repo = monoRepoFactory.cloneRepository();
repo = sharedMonoRepo;
repo.updateJsonFile('packages/bar/package.json', { private: true });
const options = getOptions();
@ -87,7 +93,7 @@ describe('readChangeFiles', () => {
});
it('excludes invalid changes from grouped change file in monorepo', () => {
repo = monoRepoFactory.cloneRepository();
repo = sharedMonoRepo;
repo.updateJsonFile('packages/bar/package.json', { private: true });
const options = getOptions({ groupChanges: true });
@ -106,7 +112,7 @@ describe('readChangeFiles', () => {
});
it('excludes out of scope change files in monorepo', () => {
repo = monoRepoFactory.cloneRepository();
repo = sharedMonoRepo;
const options = getOptions({ scope: ['packages/foo'] });
@ -119,7 +125,7 @@ describe('readChangeFiles', () => {
});
it('excludes out of scope changes from grouped change file in monorepo', () => {
repo = monoRepoFactory.cloneRepository();
repo = sharedMonoRepo;
const options = getOptions({ scope: ['packages/foo'], groupChanges: true });
@ -133,7 +139,7 @@ describe('readChangeFiles', () => {
it('runs transform.changeFiles functions if provided', async () => {
const editedComment: string = 'Edited comment for testing';
repo = monoRepoFactory.cloneRepository();
repo = sharedMonoRepo;
const options = getOptions({
command: 'change',

Просмотреть файл

@ -23,7 +23,7 @@ function cleanChangeFilePaths(root: string, changeFiles: string[]) {
describe('writeChangeFiles', () => {
let monorepoFactory: RepositoryFactory;
let repo: Repository | undefined;
let repo: Repository;
initMockLogs();
@ -40,10 +40,11 @@ describe('writeChangeFiles', () => {
// These tests can share the same repo factories because they don't push to origin
// (the actual tests run against a clone)
monorepoFactory = new RepositoryFactory('monorepo');
repo = monorepoFactory.cloneRepository();
});
afterEach(() => {
repo = undefined;
repo?.resetAndClean();
});
afterAll(() => {
@ -51,7 +52,6 @@ describe('writeChangeFiles', () => {
});
it('writes individual change files', () => {
repo = monorepoFactory.cloneRepository();
const previousHead = repo.getCurrentHash();
const options = getOptions();
@ -76,8 +76,6 @@ describe('writeChangeFiles', () => {
});
it('respects changeDir option', () => {
repo = monorepoFactory.cloneRepository();
const testChangeDir = 'myChangeDir';
const options = getOptions({ changeDir: testChangeDir });
@ -95,7 +93,6 @@ describe('writeChangeFiles', () => {
});
it('respects commit=false', () => {
repo = monorepoFactory.cloneRepository();
const previousHead = repo.getCurrentHash();
const options = getOptions({ commit: false });
@ -117,8 +114,6 @@ describe('writeChangeFiles', () => {
});
it('writes grouped change files', () => {
repo = monorepoFactory.cloneRepository();
const options = getOptions({
groupChanges: true,
});

Просмотреть файл

@ -1,87 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`writeChangelog generates correct changelog in monorepo with groupChanges (grouped change FILES): bar CHANGELOG.json 1`] = `
{
"entries": [
{
"comments": {
"patch": [
{
"author": "test@testtestme.com",
"comment": "comment from bar change ",
"commit": "(sha1-0)",
"package": "bar",
},
],
},
"date": "(date)",
"tag": "bar_v1.3.4",
"version": "1.3.4",
},
],
"name": "bar",
}
`;
exports[`writeChangelog generates correct changelog in monorepo with groupChanges (grouped change FILES): bar CHANGELOG.md 1`] = `
"# Change Log - bar
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.3.4
(date)
### Patches
- comment from bar change (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct changelog in monorepo with groupChanges (grouped change FILES): foo CHANGELOG.json 1`] = `
{
"entries": [
{
"comments": {
"patch": [
{
"author": "test@testtestme.com",
"comment": "comment 2",
"commit": "(sha1-0)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "comment 1",
"commit": "(sha1-1)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "additional comment 1",
"commit": "(sha1-2)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "additional comment 2",
"commit": "(sha1-3)",
"package": "foo",
},
],
},
"date": "(date)",
"tag": "foo_v1.0.0",
"version": "1.0.0",
},
],
"name": "foo",
}
`;
exports[`writeChangelog generates correct changelog in monorepo with groupChanges (grouped change FILES): foo CHANGELOG.md 1`] = `
exports[`writeChangelog generates basic changelog: changelog md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
@ -92,286 +11,18 @@ exports[`writeChangelog generates correct changelog in monorepo with groupChange
(date)
### Patches
### Minor changes
- comment 2 (test@testtestme.com)
- comment 1 (test@testtestme.com)
- additional comment 1 (test@testtestme.com)
- additional comment 2 (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct changelog with changeDir set: changelog json 1`] = `
{
"entries": [
{
"comments": {
"patch": [
{
"author": "test@testtestme.com",
"comment": "comment 2",
"commit": "(sha1-0)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "comment 1",
"commit": "(sha1-1)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "additional comment 1",
"commit": "(sha1-2)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "additional comment 2",
"commit": "(sha1-3)",
"package": "foo",
},
],
},
"date": "(date)",
"tag": "foo_v1.0.0",
"version": "1.0.0",
},
],
"name": "foo",
}
`;
exports[`writeChangelog generates correct changelog with changeDir set: changelog md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
- new minor comment (test@test.com)
- old minor comment (test@test.com)
### Patches
- comment 2 (test@testtestme.com)
- comment 1 (test@testtestme.com)
- additional comment 1 (test@testtestme.com)
- additional comment 2 (test@testtestme.com)
- patch comment (test@test.com)
"
`;
exports[`writeChangelog generates correct changelog: changelog json 1`] = `
{
"entries": [
{
"comments": {
"patch": [
{
"author": "test@testtestme.com",
"comment": "comment 2",
"commit": "(sha1-0)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "comment 1",
"commit": "(sha1-1)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "additional comment 1",
"commit": "(sha1-2)",
"package": "foo",
},
{
"author": "test@testtestme.com",
"comment": "additional comment 2",
"commit": "(sha1-3)",
"package": "foo",
},
],
},
"date": "(date)",
"tag": "foo_v1.0.0",
"version": "1.0.0",
},
],
"name": "foo",
}
`;
exports[`writeChangelog generates correct changelog: changelog md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
### Patches
- comment 2 (test@testtestme.com)
- comment 1 (test@testtestme.com)
- additional comment 1 (test@testtestme.com)
- additional comment 2 (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct grouped changelog in monorepo: bar CHANGELOG.md 1`] = `
"# Change Log - bar
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.3.4
(date)
### Patches
- comment 3 (test@testtestme.com)
- comment 2 (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct grouped changelog in monorepo: foo CHANGELOG.md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
### Patches
- comment 1 (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct grouped changelog in monorepo: grouped CHANGELOG.md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
### Patches
- \`bar\`
- comment 3 (test@testtestme.com)
- comment 2 (test@testtestme.com)
- \`foo\`
- comment 1 (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct grouped changelog when grouped change log is saved to the same dir as a regular changelog 1`] = `
"# Change Log - bar
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.3.4
(date)
### Patches
- comment 2 (test@testtestme.com)
"
`;
exports[`writeChangelog generates correct grouped changelog when grouped change log is saved to the same dir as a regular changelog 2`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
### Patches
- \`bar\`
- comment 2 (test@testtestme.com)
- \`foo\`
- comment 1 (test@testtestme.com)
"
`;
exports[`writeChangelog generates grouped changelog without dependent change entries where packages have normal changes and dependency changes: bar CHANGELOG.md 1`] = `
"# Change Log - bar
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.3.4
(date)
### Patches
- comment 1 (test@testtestme.com)
- Bump baz to v1.3.4
"
`;
exports[`writeChangelog generates grouped changelog without dependent change entries where packages have normal changes and dependency changes: baz CHANGELOG.md 1`] = `
"# Change Log - baz
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.3.4
(date)
### Patches
- comment 1 (test@testtestme.com)
"
`;
exports[`writeChangelog generates grouped changelog without dependent change entries where packages have normal changes and dependency changes: grouped CHANGELOG.md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
### Patches
- \`bar\`
- comment 1 (test@testtestme.com)
- \`baz\`
- comment 1 (test@testtestme.com)
"
`;
exports[`writeChangelog generates grouped changelog without dependent change entries: bar CHANGELOG.md 1`] = `
exports[`writeChangelog generates changelogs with dependent changes in monorepo: bar CHANGELOG.md 1`] = `
"# Change Log - bar
<!-- This log was last generated on (date) and should not be manually modified. -->
@ -388,7 +39,7 @@ exports[`writeChangelog generates grouped changelog without dependent change ent
"
`;
exports[`writeChangelog generates grouped changelog without dependent change entries: baz CHANGELOG.md 1`] = `
exports[`writeChangelog generates changelogs with dependent changes in monorepo: baz CHANGELOG.md 1`] = `
"# Change Log - baz
<!-- This log was last generated on (date) and should not be manually modified. -->
@ -399,13 +50,13 @@ exports[`writeChangelog generates grouped changelog without dependent change ent
(date)
### Patches
### Minor changes
- comment 1 (test@testtestme.com)
- baz comment (test@test.com)
"
`;
exports[`writeChangelog generates grouped changelog without dependent change entries: grouped CHANGELOG.md 1`] = `
exports[`writeChangelog generates changelogs with dependent changes in monorepo: foo CHANGELOG.md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
@ -416,9 +67,30 @@ exports[`writeChangelog generates grouped changelog without dependent change ent
(date)
### Patches
### Minor changes
- \`baz\`
- comment 1 (test@testtestme.com)
- foo comment (test@test.com)
- Bump bar to v1.3.4
"
`;
exports[`writeChangelog generates grouped changelog in monorepo: grouped CHANGELOG.md 1`] = `
"# Change Log - foo
<!-- This log was last generated on (date) and should not be manually modified. -->
<!-- Start content -->
## 1.0.0
(date)
### Minor changes
- \`foo\`
- foo comment 2 (test@test.com)
- foo comment (test@test.com)
- \`baz\`
- baz comment (test@test.com)
"
`;

Просмотреть файл

@ -1,51 +1,83 @@
import { describe, expect, it, beforeAll, afterAll, afterEach } from '@jest/globals';
import { generateChangeFiles } from '../../__fixtures__/changeFiles';
import { cleanChangelogJson, readChangelogJson, readChangelogMd } from '../../__fixtures__/changelog';
import fs from 'fs-extra';
import { generateChangeFiles, getChange, fakeEmail as author } from '../../__fixtures__/changeFiles';
import {
cleanChangelogJson,
readChangelogJson,
readChangelogMd,
fakeCommit as commit,
trimChangelogMd,
} from '../../__fixtures__/changelog';
import { initMockLogs } from '../../__fixtures__/mockLogs';
import { RepositoryFactory } from '../../__fixtures__/repositoryFactory';
import { writeChangelog } from '../../changelog/writeChangelog';
import { getPackageInfos } from '../../monorepo/getPackageInfos';
import { readChangeFiles } from '../../changefile/readChangeFiles';
import { BeachballOptions } from '../../types/BeachballOptions';
import { ChangeFileInfo, ChangeType } from '../../types/ChangeInfo';
import type { BeachballOptions } from '../../types/BeachballOptions';
import type { Repository } from '../../__fixtures__/repository';
import { getDefaultOptions } from '../../options/getDefaultOptions';
function getChange(packageName: string, comment: string, type: ChangeType = 'patch'): ChangeFileInfo {
return {
comment,
email: 'test@testtestme.com',
packageName,
type,
dependentChangeType: 'patch',
};
}
import type { BumpInfo } from '../../types/BumpInfo';
import { getMaxChangeType } from '../../changefile/changeTypes';
import { getChangePath } from '../../paths';
describe('writeChangelog', () => {
let repositoryFactory: RepositoryFactory;
let monoRepoFactory: RepositoryFactory;
let repo: Repository | undefined;
let sharedSingleRepo: Repository;
let sharedMonoRepo: Repository;
initMockLogs();
/**
* Read package infos and change files, fill in default options, and call `writeChangelog`.
*
* `calculatedChangeTypes` will be generated based on the max change type of each package's change files,
* and assuming every `dependentChangedBy` package has change type `patch`.
*/
async function writeChangelogWrapper(
params: Partial<Pick<BumpInfo, 'dependentChangedBy'>> & {
options: BeachballOptions;
}
) {
const { options, dependentChangedBy = {} } = params;
const packageInfos = getPackageInfos(repo!.rootPath);
const changeFileChangeInfos = readChangeFiles(options, packageInfos);
// Generate a basic best guess at calculatedChangeTypes
const calculatedChangeTypes: BumpInfo['calculatedChangeTypes'] = {};
for (const { change } of changeFileChangeInfos) {
const { packageName, type } = change;
calculatedChangeTypes[packageName] = getMaxChangeType(type, calculatedChangeTypes[packageName]);
}
for (const pkgName of Object.keys(dependentChangedBy)) {
calculatedChangeTypes[pkgName] = getMaxChangeType('patch', calculatedChangeTypes[pkgName]);
}
await writeChangelog({ dependentChangedBy, calculatedChangeTypes, changeFileChangeInfos, packageInfos }, options);
}
function getOptions(options?: Partial<BeachballOptions>): BeachballOptions {
return {
...getDefaultOptions(),
// change to ?. if a future test uses a non-standard repo
// change to ?. if a future test uses a non-standard repo var name
path: repo!.rootPath,
...options,
};
}
beforeAll(() => {
// These tests can share the same repo factories because they don't push to origin
// (the actual tests run against a clone)
// These tests can share the same factories and repos because they don't push to the remote,
// and the repo used is reset after each test (which is faster than making new clones).
repositoryFactory = new RepositoryFactory('single');
monoRepoFactory = new RepositoryFactory('monorepo');
sharedSingleRepo = repositoryFactory.cloneRepository();
sharedMonoRepo = monoRepoFactory.cloneRepository();
});
afterEach(() => {
// Revert whichever shared repo was used to the original state
repo?.resetAndClean();
repo = undefined;
});
@ -54,247 +86,343 @@ describe('writeChangelog', () => {
monoRepoFactory.cleanUp();
});
it('generates correct changelog', async () => {
repo = repositoryFactory.cloneRepository();
it('does not write changelogs if there are no changes', async () => {
repo = sharedSingleRepo;
const options = getOptions();
repo.commitChange('foo');
generateChangeFiles([getChange('foo', 'additional comment 2')], options);
generateChangeFiles([getChange('foo', 'additional comment 1')], options);
generateChangeFiles([getChange('foo', 'comment 1')], options);
await writeChangelogWrapper({ options });
repo.commitChange('bar');
generateChangeFiles([getChange('foo', 'comment 2')], options);
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, { foo: 'patch' }, { foo: new Set(['foo']) }, packageInfos);
expect(readChangelogMd(repo.rootPath)).toMatchSnapshot('changelog md');
const changelogJson = readChangelogJson(repo.rootPath);
expect(cleanChangelogJson(changelogJson)).toMatchSnapshot('changelog json');
// Every entry should have a different commit hash
const patchComments = changelogJson!.entries[0].comments.patch!;
const commits = patchComments.map(entry => entry.commit);
expect(new Set(commits).size).toEqual(patchComments.length);
// The first entry should be the newest
expect(patchComments[0].commit).toBe(repo.getCurrentHash());
expect(readChangelogMd(repo.rootPath)).toBeNull();
expect(readChangelogJson(repo.rootPath)).toBeNull();
});
it('generates correct changelog with changeDir set', async () => {
repo = repositoryFactory.cloneRepository();
it('generates basic changelog', async () => {
repo = sharedSingleRepo;
const options = getOptions();
const options = getOptions({
changeDir: 'myChangeDir',
});
generateChangeFiles([getChange('foo', 'old minor comment')], options);
generateChangeFiles([getChange('foo', 'patch comment', 'patch')], options);
generateChangeFiles([getChange('foo', 'no comment', 'none')], options);
generateChangeFiles([getChange('foo', 'new minor comment', 'minor')], options);
repo.commitChange('foo');
generateChangeFiles([getChange('foo', 'additional comment 2')], options);
generateChangeFiles([getChange('foo', 'additional comment 1')], options);
generateChangeFiles([getChange('foo', 'comment 1')], options);
await writeChangelogWrapper({ options });
repo.commitChange('bar');
generateChangeFiles([getChange('foo', 'comment 2')], options);
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, { foo: 'patch' }, { foo: new Set(['foo']) }, packageInfos);
expect(readChangelogMd(repo.rootPath)).toMatchSnapshot('changelog md');
const changelogMd = readChangelogMd(repo.rootPath);
// Do some explicit tests since snapshot changes are too easy to ignore
expect(changelogMd).toMatch(/^# Change Log - foo/);
expect(changelogMd).toMatch(/### Minor changes\n\n- new minor comment.*\n- old minor comment/);
expect(changelogMd).toContain('### Patches\n\n- patch comment');
expect(changelogMd).not.toContain('no comment');
expect(changelogMd).toMatchSnapshot('changelog md');
const changelogJson = readChangelogJson(repo.rootPath);
expect(cleanChangelogJson(changelogJson)).toMatchSnapshot('changelog json');
// Every entry should have a different commit hash
const patchComments = changelogJson!.entries[0].comments.patch!;
const commits = patchComments.map(entry => entry.commit);
expect(new Set(commits).size).toEqual(patchComments.length);
// The first entry should be the newest
expect(patchComments[0].commit).toBe(repo.getCurrentHash());
});
it('generates correct changelog in monorepo with groupChanges (grouped change FILES)', async () => {
repo = monoRepoFactory.cloneRepository();
const options = getOptions({
groupChanges: true,
expect(changelogJson).toEqual({ name: 'foo', entries: [expect.anything()] });
expect(cleanChangelogJson(changelogJson)!.entries[0]).toEqual({
version: '1.0.0',
date: '(date)',
tag: 'foo_v1.0.0',
comments: {
minor: [
{ comment: 'new minor comment', package: 'foo', author, commit },
{ comment: 'old minor comment', package: 'foo', author, commit },
],
patch: [{ comment: 'patch comment', package: 'foo', author, commit }],
none: [{ comment: 'no comment', package: 'foo', author, commit }],
},
});
repo.commitChange('foo');
generateChangeFiles(
[getChange('foo', 'additional comment 2'), getChange('bar', 'comment from bar change ')],
options
);
generateChangeFiles([getChange('foo', 'additional comment 1')], options);
generateChangeFiles([getChange('foo', 'comment 1')], options);
// Every entry should have a different commit hash
const minorComments = changelogJson!.entries[0].comments.minor!;
expect(minorComments).toBeTruthy();
const commits = minorComments.map(entry => entry.commit);
expect(new Set(commits).size).toEqual(minorComments.length);
repo.commitChange('bar');
generateChangeFiles([getChange('foo', 'comment 2')], options);
// The first entry should be the newest
expect(minorComments[0].commit).toBe(repo!.getCurrentHash());
});
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
it('generates changelog with custom changeDir', async () => {
repo = sharedSingleRepo;
const changeDir = 'myChangeDir';
const options = getOptions({ changeDir });
await writeChangelog(options, changes, { foo: 'patch', bar: 'patch' }, {}, packageInfos);
generateChangeFiles([{ packageName: 'foo', comment: 'comment 1' }], options);
// make sure the setup worked as expected
expect(fs.readdirSync(repo.pathTo(changeDir))).toEqual([expect.stringMatching(/^foo-.*\.json$/)]);
// check changelogs for both foo and bar
expect(readChangelogMd(repo.pathTo('packages/foo'))).toMatchSnapshot('foo CHANGELOG.md');
expect(readChangelogMd(repo.pathTo('packages/bar'))).toMatchSnapshot('bar CHANGELOG.md');
await writeChangelogWrapper({ options });
// Just check for a comment in the md to verify that the change file was found
expect(readChangelogMd(repo.rootPath)).toContain('### Minor changes\n\n- comment 1');
});
it('generates changelogs with dependent changes in monorepo', async () => {
repo = sharedMonoRepo;
const options = getOptions();
generateChangeFiles([{ packageName: 'foo', comment: 'foo comment' }], options);
generateChangeFiles([{ packageName: 'baz', comment: 'baz comment' }], options);
await writeChangelogWrapper({
options,
// Per the fixture, bar depends on baz (and is bumped), and foo depends on bar.
// Note that the changelogs will only include dependent bump entries as specified here
// (which may be different than what would actually be calculated while bumping), and
// NO actual bumping will occur (so the versions will be the same as the fixture).
dependentChangedBy: { bar: new Set(['baz']), foo: new Set(['bar']) },
});
// check changelogs for foo, bar, and baz
const fooText = readChangelogMd(repo.pathTo('packages/foo'));
expect(fooText).toMatch(/### Minor changes\n\n- foo comment.*\n- Bump bar to/);
expect(fooText).not.toContain('baz comment');
expect(fooText).toMatchSnapshot('foo CHANGELOG.md');
const barText = readChangelogMd(repo.pathTo('packages/bar'));
expect(barText).toContain('### Patches\n\n- Bump baz to');
expect(barText).not.toMatch(/(foo|baz) comment/);
expect(barText).toMatchSnapshot('bar CHANGELOG.md');
const bazText = readChangelogMd(repo.pathTo('packages/baz'));
expect(bazText).toContain('baz comment');
expect(bazText).not.toContain('Bump');
expect(bazText).toMatchSnapshot('baz CHANGELOG.md');
const fooJson = readChangelogJson(repo.pathTo('packages/foo'));
expect(cleanChangelogJson(fooJson)).toMatchSnapshot('foo CHANGELOG.json');
expect(readChangelogJson(repo.pathTo('packages/bar'), true /*clean*/)).toMatchSnapshot('bar CHANGELOG.json');
expect(fooJson).toEqual({ name: 'foo', entries: [expect.anything()] });
expect(cleanChangelogJson(fooJson)!.entries[0]).toEqual({
version: '1.0.0',
date: '(date)',
tag: 'foo_v1.0.0',
comments: {
minor: [
{ package: 'foo', comment: 'foo comment', author, commit },
{ package: 'foo', comment: 'Bump bar to v1.3.4', author: 'beachball', commit },
],
},
});
// Every entry should have a different commit hash
const patchComments = fooJson!.entries[0].comments.patch!;
const commits = patchComments.map(entry => entry.commit);
expect(new Set(commits).size).toEqual(patchComments.length);
const barJson = readChangelogJson(repo.pathTo('packages/bar'));
expect(barJson).toEqual({ name: 'bar', entries: [expect.anything()] });
expect(cleanChangelogJson(barJson)!.entries[0]).toEqual({
comments: {
patch: [{ package: 'bar', comment: 'Bump baz to v1.3.4', author: 'beachball', commit }],
},
date: '(date)',
tag: 'bar_v1.3.4',
version: '1.3.4',
});
// The first entry should be the newest
expect(patchComments[0].commit).toBe(repo.getCurrentHash());
const bazJson = readChangelogJson(repo.pathTo('packages/baz'));
expect(bazJson).toEqual({ name: 'baz', entries: [expect.anything()] });
expect(cleanChangelogJson(bazJson)!.entries[0]).toEqual({
version: '1.3.4',
date: '(date)',
tag: 'baz_v1.3.4',
comments: {
minor: [{ package: 'baz', comment: 'baz comment', author, commit }],
},
});
});
it('generates correct grouped changelog in monorepo', async () => {
repo = monoRepoFactory.cloneRepository();
it('generates changelog in monorepo with grouped change files (groupChanges)', async () => {
repo = sharedMonoRepo;
const options = getOptions({ groupChanges: true });
// these will be in one change file
generateChangeFiles([getChange('foo', 'comment 2'), getChange('bar', 'bar comment')], options);
// separate change file
generateChangeFiles([getChange('foo', 'comment 1')], options);
await writeChangelogWrapper({ options, dependentChangedBy: { foo: new Set(['bar']) } });
// check changelogs for both foo and bar
const fooText = readChangelogMd(repo.pathTo('packages/foo'));
expect(fooText).toMatch(/- comment 1.*\n- comment 2/);
expect(fooText).not.toContain('bar comment');
const barText = readChangelogMd(repo.pathTo('packages/bar'));
expect(barText).toContain('bar comment');
expect(barText).not.toMatch(/comment (1|2)/);
const fooJson = readChangelogJson(repo.pathTo('packages/foo'));
expect(fooJson).toEqual({ name: 'foo', entries: [expect.anything()] });
expect(cleanChangelogJson(fooJson)!.entries[0].comments).toEqual({
minor: [
expect.objectContaining({ comment: 'comment 1', package: 'foo' }),
expect.objectContaining({ comment: 'comment 2', package: 'foo' }),
expect.objectContaining({ comment: 'Bump bar to v1.3.4', package: 'foo' }),
],
});
const barJson = readChangelogJson(repo.pathTo('packages/bar'));
expect(barJson).toEqual({ name: 'bar', entries: [expect.anything()] });
expect(cleanChangelogJson(barJson)!.entries[0].comments).toEqual({
minor: [expect.objectContaining({ comment: 'bar comment', package: 'bar' })],
});
});
it('generates grouped changelog in monorepo', async () => {
repo = sharedMonoRepo;
const options = getOptions({
changelog: {
groups: [
{
masterPackageName: 'foo',
changelogPath: repo.rootPath,
changelogPath: '.',
include: ['packages/*'],
},
],
},
});
// foo and baz have changes.
// bar has no direct changes, but it depends on baz.
generateChangeFiles(['foo', 'baz'], options);
generateChangeFiles([getChange('foo', 'foo comment 2')], options);
await writeChangelogWrapper({
options,
// Per the fixture structure, bar will have a dependent change from baz, which changes foo
dependentChangedBy: { bar: new Set(['baz']), foo: new Set(['bar']) },
});
// Validate package changelogs
const fooText = readChangelogMd(repo.pathTo('packages/foo'));
// includes the dependent change from bar
expect(fooText).toMatch(/- foo comment.*\n- Bump bar to/);
expect(fooText).not.toContain('baz comment');
const barText = readChangelogMd(repo.pathTo('packages/bar'));
// includes the dependent change from baz
expect(barText).toContain('Bump baz to');
expect(barText).not.toMatch(/(foo|baz) comment/);
const bazText = readChangelogMd(repo.pathTo('packages/baz'));
expect(bazText).toContain('baz comment');
expect(bazText).not.toMatch(/Bump|foo comment/);
// Verify that dependent entries are in foo CHANGELOG.json
const fooJson = readChangelogJson(repo.pathTo('packages/foo'));
expect(fooJson).toEqual({ name: 'foo', entries: [expect.anything()] });
expect(fooJson!.entries[0].comments.minor).toContainEqual(
expect.objectContaining({ comment: 'Bump bar to v1.3.4' })
);
// Validate grouped changelog: it shouldn't have dependent entries
const groupedText = readChangelogMd(repo.rootPath);
expect(groupedText).not.toContain('Bump');
expect(groupedText).toMatch(/- `foo`.*\n - foo comment 2.*\n - foo comment/);
expect(groupedText).toMatch(/- `baz`.*\n - baz comment/);
expect(groupedText).toMatchSnapshot('grouped CHANGELOG.md');
// Validate grouped CHANGELOG.json
const groupedJson = readChangelogJson(repo.rootPath);
expect(groupedJson).toEqual({ name: 'foo', entries: [expect.anything()] });
expect(cleanChangelogJson(groupedJson)!.entries[0]).toEqual({
comments: {
minor: [
{ comment: 'foo comment 2', package: 'foo', author, commit },
{ comment: 'foo comment', package: 'foo', author, commit },
{ comment: 'baz comment', package: 'baz', author, commit },
],
},
date: '(date)',
tag: 'foo_v1.0.0',
version: '1.0.0',
});
});
it('generates grouped changelog when path overlaps with regular changelog', async () => {
repo = sharedMonoRepo;
const options = getOptions({
changelog: {
groups: [
{
masterPackageName: 'foo',
changelogPath: 'packages/foo',
include: ['packages/foo', 'packages/bar'],
},
],
},
});
repo.commitChange('foo');
generateChangeFiles([getChange('foo', 'comment 1')], options);
generateChangeFiles(['foo', 'bar'], options);
repo.commitChange('bar');
generateChangeFiles([getChange('bar', 'comment 2')], options);
generateChangeFiles([getChange('bar', 'comment 3')], options);
await writeChangelogWrapper({ options, dependentChangedBy: { foo: new Set(['bar']) } });
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
// packages/foo changelog should be grouped, not regular.
// We can verify this by just looking for the bar entry.
const groupedChangelogMd = readChangelogMd(repo.pathTo('packages/foo'));
expect(groupedChangelogMd).toContain('- `bar`\n - bar comment');
await writeChangelog(options, changes, {}, {}, packageInfos);
// Validate changelog for foo and bar packages
expect(readChangelogMd(repo.pathTo('packages/foo'))).toMatchSnapshot('foo CHANGELOG.md');
expect(readChangelogMd(repo.pathTo('packages/bar'))).toMatchSnapshot('bar CHANGELOG.md');
// Validate grouped changelog for foo and bar packages
expect(readChangelogMd(repo.rootPath)).toMatchSnapshot('grouped CHANGELOG.md');
const groupedJson = readChangelogJson(repo.pathTo('packages/foo'));
expect(groupedJson).toEqual({ name: 'foo', entries: [expect.anything()] });
expect(groupedJson!.entries[0].comments.minor).toContainEqual(expect.objectContaining({ comment: 'bar comment' }));
});
it('generates grouped changelog without dependent change entries', async () => {
repo = monoRepoFactory.cloneRepository();
it('does not write grouped changelog if group would only have dependent bumps', async () => {
repo = sharedMonoRepo;
const options = getOptions({
changelog: {
groups: [{ masterPackageName: 'foo', changelogPath: '.', include: ['packages/foo', 'packages/baz'] }],
},
});
// bar is not in the group, but it causes a dependent change for foo
generateChangeFiles(['bar'], options);
await writeChangelogWrapper({ options, dependentChangedBy: { foo: new Set(['bar']) } });
// grouped changelog was not written
expect(readChangelogMd(repo.rootPath)).toBeNull();
expect(readChangelogJson(repo.rootPath)).toBeNull();
// foo changelog was written with the dependent bump
expect(readChangelogMd(repo.pathTo('packages/foo'))).toBeTruthy();
});
it('does not write grouped changelog overlapping regular changelog if it would contain only dependent bumps', async () => {
repo = sharedMonoRepo;
const options = getOptions({
changelog: {
groups: [
{
masterPackageName: 'foo',
changelogPath: repo.rootPath,
include: ['packages/foo', 'packages/bar', 'packages/baz'],
},
// The grouped changelog overlaps with the changelog for packages/foo.
{ masterPackageName: 'foo', changelogPath: 'packages/foo', include: ['packages/foo', 'packages/baz'] },
],
},
});
repo.commitChange('baz');
generateChangeFiles([getChange('baz', 'comment 1')], options);
// bar is not in the group
generateChangeFiles(['bar'], options);
// but it causes a dependent change for foo (so normally foo's non-grouped changelog would be written)
await writeChangelogWrapper({ options, dependentChangedBy: { foo: new Set(['bar']) } });
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, { bar: 'patch', baz: 'patch' }, { bar: new Set(['baz']) }, packageInfos);
// Validate changelog for bar package
const barChangelogText = readChangelogMd(repo.pathTo('packages/bar'));
expect(barChangelogText).toContain('- Bump baz');
expect(barChangelogText).toMatchSnapshot('bar CHANGELOG.md');
// Validate changelog for baz package
expect(readChangelogMd(repo.pathTo('packages/baz'))).toMatchSnapshot('baz CHANGELOG.md');
// Validate grouped changelog for foo master package
const groupedChangelogText = readChangelogMd(repo.rootPath);
expect(groupedChangelogText).toContain('- comment 1');
expect(groupedChangelogText).not.toContain('- Bump baz');
expect(groupedChangelogText).toMatchSnapshot('grouped CHANGELOG.md');
// Nothing was written (not the grouped changelog, and not a normal changelog for foo)
expect(readChangelogMd(repo.pathTo('packages/foo'))).toBeNull();
expect(readChangelogJson(repo.pathTo('packages/foo'))).toBeNull();
});
it('generates grouped changelog without dependent change entries where packages have normal changes and dependency changes', async () => {
repo = monoRepoFactory.cloneRepository();
it('includes pre* changes', async () => {
repo = sharedSingleRepo;
const options = getOptions();
const options = getOptions({
changelog: {
groups: [
{
masterPackageName: 'foo',
changelogPath: repo.rootPath,
include: ['packages/foo', 'packages/bar', 'packages/baz'],
},
],
},
});
generateChangeFiles(
[
{ packageName: 'foo', comment: 'comment 1', type: 'premajor' },
{ packageName: 'foo', comment: 'comment 2', type: 'preminor' },
{ packageName: 'foo', comment: 'comment 3', type: 'prepatch' },
{ packageName: 'foo', comment: 'comment 4', type: 'prerelease' },
],
options
);
repo.commitChange('baz');
generateChangeFiles([getChange('baz', 'comment 1')], options);
generateChangeFiles([getChange('bar', 'comment 1')], options);
await writeChangelogWrapper({ options });
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, { bar: 'patch', baz: 'patch' }, { bar: new Set(['baz']) }, packageInfos);
// Validate changelog for bar and baz packages
expect(readChangelogMd(repo.pathTo('packages/bar'))).toMatchSnapshot('bar CHANGELOG.md');
expect(readChangelogMd(repo.pathTo('packages/baz'))).toMatchSnapshot('baz CHANGELOG.md');
// Validate grouped changelog for foo master package
expect(readChangelogMd(repo.rootPath)).toMatchSnapshot('grouped CHANGELOG.md');
});
it('generates correct grouped changelog when grouped change log is saved to the same dir as a regular changelog', async () => {
repo = monoRepoFactory.cloneRepository();
const options = getOptions({
changelog: {
groups: [
{
masterPackageName: 'foo',
changelogPath: repo.pathTo('packages/foo'),
include: ['packages/foo', 'packages/bar'],
},
],
},
});
repo.commitChange('foo');
generateChangeFiles([getChange('foo', 'comment 1')], options);
repo.commitChange('bar');
generateChangeFiles([getChange('bar', 'comment 2')], options);
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, {}, {}, packageInfos);
// Validate changelog for bar package
expect(readChangelogMd(repo.pathTo('packages/bar'))).toMatchSnapshot();
// Validate grouped changelog for foo and bar packages
expect(readChangelogMd(repo.pathTo('packages/foo'))).toMatchSnapshot();
const changelogMd = readChangelogMd(repo.rootPath);
expect(changelogMd).toContain('### Major changes (pre-release)\n\n- comment 1');
expect(changelogMd).toContain('### Minor changes (pre-release)\n\n- comment 2');
expect(changelogMd).toContain('### Patches (pre-release)\n\n- comment 3');
expect(changelogMd).toContain('### Changes\n\n- comment 4');
});
it('includes pre* changes', async () => {
@ -303,16 +431,14 @@ describe('writeChangelog', () => {
generateChangeFiles(
[
{ packageName: 'foo', comment: 'comment 1', type: 'premajor' },
{ packageName: 'foo', comment: 'comment 2', type: 'preminor' },
{ packageName: 'foo', comment: 'comment 3', type: 'prepatch' },
getChange('foo', 'comment 1', 'premajor'),
getChange('foo', 'comment 2', 'preminor'),
getChange('foo', 'comment 3', 'prepatch'),
],
options
);
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, {}, {}, packageInfos);
await writeChangelogWrapper({ options });
const changelogMd = readChangelogMd(repo.rootPath);
expect(changelogMd).toContain('### Major changes (pre-release)\n\n- comment 1');
@ -321,16 +447,12 @@ describe('writeChangelog', () => {
});
it('writes only CHANGELOG.md if generateChangelog is "md"', async () => {
repo = repositoryFactory.cloneRepository();
repo = sharedSingleRepo;
const options = getOptions({ generateChangelog: 'md' });
repo.commitChange('foo');
generateChangeFiles([getChange('foo', 'comment 1')], options);
generateChangeFiles(['foo'], options);
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, { foo: 'patch' }, { foo: new Set(['foo']) }, packageInfos);
await writeChangelogWrapper({ options });
// CHANGELOG.md is written
expect(readChangelogMd(repo.rootPath)).toContain('## 1.0.0');
@ -340,16 +462,12 @@ describe('writeChangelog', () => {
});
it('writes only CHANGELOG.json if generateChangelog is "json"', async () => {
repo = repositoryFactory.cloneRepository();
repo = sharedSingleRepo;
const options = getOptions({ generateChangelog: 'json' });
repo.commitChange('foo');
generateChangeFiles([getChange('foo', 'comment 1')], options);
generateChangeFiles(['foo'], options);
const packageInfos = getPackageInfos(repo.rootPath);
const changes = readChangeFiles(options, packageInfos);
await writeChangelog(options, changes, { foo: 'patch' }, { foo: new Set(['foo']) }, packageInfos);
await writeChangelogWrapper({ options });
// CHANGELOG.md is not written
expect(readChangelogMd(repo.rootPath)).toBeNull();
@ -357,6 +475,39 @@ describe('writeChangelog', () => {
// CHANGELOG.json is written
const changelogJson = readChangelogJson(repo.rootPath);
expect(changelogJson).not.toBeNull();
expect(changelogJson!.entries[0].comments.patch).toEqual([expect.objectContaining({ comment: 'comment 1' })]);
expect(changelogJson!.entries[0].comments.minor).toEqual([expect.objectContaining({ comment: 'foo comment' })]);
});
it('appends to existing changelog', async () => {
// Most of the previous content tests are handled by renderChangelog, but writeChangelog is
// responsible for reading that content and passing it in.
repo = sharedSingleRepo;
const options = getOptions();
// Write some changes and generate changelogs
generateChangeFiles(['foo'], options);
await writeChangelogWrapper({ options });
// Read and save the initial changelogs
const firstChangelogMd = readChangelogMd(repo.rootPath);
expect(firstChangelogMd).toContain('foo comment');
const firstChangelogJson = cleanChangelogJson(readChangelogJson(repo.rootPath));
expect(firstChangelogJson).toEqual({ name: 'foo', entries: [expect.anything()] });
// Delete the change files, generate new ones, and re-generate changelogs
fs.emptyDirSync(getChangePath(options));
generateChangeFiles([getChange('foo', 'extra change')], options);
await writeChangelogWrapper({ options });
// Read the changelogs again and verify that the previous content is still there
const secondChangelogMd = readChangelogMd(repo.rootPath);
expect(secondChangelogMd).toContain('extra change');
expect(secondChangelogMd).toContain(trimChangelogMd(firstChangelogMd!));
const secondChangelogJson = cleanChangelogJson(readChangelogJson(repo.rootPath));
expect(secondChangelogJson).toEqual({
name: 'foo',
entries: [expect.anything(), firstChangelogJson!.entries[0]],
});
});
});

Просмотреть файл

@ -89,7 +89,7 @@ export async function updatePackageLock(cwd: string): Promise<void> {
* deletes change files, update package.json, and changelogs
*/
export async function performBump(bumpInfo: BumpInfo, options: BeachballOptions): Promise<BumpInfo> {
const { modifiedPackages, packageInfos, changeFileChangeInfos, dependentChangedBy, calculatedChangeTypes } = bumpInfo;
const { modifiedPackages, packageInfos, changeFileChangeInfos } = bumpInfo;
await callHook(options.hooks?.prebump, modifiedPackages, bumpInfo.packageInfos);
@ -98,7 +98,7 @@ export async function performBump(bumpInfo: BumpInfo, options: BeachballOptions)
if (options.generateChangelog) {
// Generate changelog
await writeChangelog(options, changeFileChangeInfos, calculatedChangeTypes, dependentChangedBy, packageInfos);
await writeChangelog(bumpInfo, options);
}
if (!options.keepChangeFiles) {

Просмотреть файл

@ -13,19 +13,17 @@ import { mergeChangelogs } from './mergeChangelogs';
import { ChangeSet } from '../types/ChangeInfo';
export async function writeChangelog(
options: BeachballOptions,
changeFileChangeInfos: ChangeSet,
calculatedChangeTypes: BumpInfo['calculatedChangeTypes'],
dependentChangedBy: BumpInfo['dependentChangedBy'],
packageInfos: PackageInfos
bumpInfo: Pick<BumpInfo, 'changeFileChangeInfos' | 'calculatedChangeTypes' | 'dependentChangedBy' | 'packageInfos'>,
options: BeachballOptions
): Promise<void> {
const groupedChangelogPaths = await writeGroupedChangelog(
const { changeFileChangeInfos, calculatedChangeTypes, dependentChangedBy, packageInfos } = bumpInfo;
const groupedChangelogDirs = await writeGroupedChangelog(
options,
changeFileChangeInfos,
calculatedChangeTypes,
packageInfos
);
const groupedChangelogPathSet = new Set(groupedChangelogPaths);
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
@ -34,84 +32,90 @@ export async function writeChangelog(
packageInfos,
options,
});
// Write package changelogs.
// Use a standard for loop here to prevent potentially firing off multiple network requests at once
// (in case any custom renderers have network requests)
// (in case any custom renderers have network requests).
for (const pkg of Object.keys(changelogs)) {
const packagePath = path.dirname(packageInfos[pkg].packageJsonPath);
if (groupedChangelogPathSet?.has(packagePath)) {
console.log(`Changelog for ${pkg} has been written as a group here: ${packagePath}`);
} else {
if (!groupedChangelogDirs.includes(packagePath)) {
await writeChangelogFiles(options, changelogs[pkg], packagePath, false);
}
}
}
/**
* Write grouped changelogs.
* @returns The list of directories where grouped changelogs were written.
*/
async function writeGroupedChangelog(
options: BeachballOptions,
changeFileChangeInfos: ChangeSet,
calculatedChangeTypes: BumpInfo['calculatedChangeTypes'],
packageInfos: PackageInfos
): Promise<string[]> {
if (!options.changelog) {
return [];
}
const { groups: changelogGroups } = options.changelog;
// Get the changelog groups with absolute paths.
const changelogGroups = options.changelog?.groups?.map(({ changelogPath, ...rest }) => ({
...rest,
changelogAbsDir: path.resolve(options.path, changelogPath),
}));
if (!changelogGroups?.length) {
return [];
}
// Grouped changelogs should not contain dependency bump entries
// Get changelogs without dependency bump entries
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes,
packageInfos,
options,
});
const groupedChangelogs: {
[path: string]: {
changelogs: PackageChangelog[];
masterPackage: PackageInfo;
};
[changelogAbsDir: string]: { changelogs: PackageChangelog[]; masterPackage: PackageInfo };
} = {};
// Validate groups and initialize groupedChangelogs
for (const { masterPackageName, changelogAbsDir } of changelogGroups) {
const masterPackage = packageInfos[masterPackageName];
if (!masterPackage) {
console.warn(`master package ${masterPackageName} does not exist.`);
continue;
}
if (!fs.existsSync(changelogAbsDir)) {
console.warn(`changelog path ${changelogAbsDir} does not exist.`);
continue;
}
groupedChangelogs[changelogAbsDir] = { masterPackage, changelogs: [] };
}
// Put changelogs into groups
for (const pkg of Object.keys(changelogs)) {
const packagePath = path.dirname(packageInfos[pkg].packageJsonPath);
const relativePath = path.relative(options.path, packagePath);
for (const group of changelogGroups) {
const { changelogPath, masterPackageName } = group;
const masterPackage = packageInfos[masterPackageName];
if (!masterPackage) {
console.warn(`master package ${masterPackageName} does not exist.`);
continue;
}
if (!fs.existsSync(changelogPath)) {
console.warn(`changelog path ${changelogPath} does not exist.`);
continue;
}
for (const group of changelogGroups) {
const isInGroup = isPathIncluded(relativePath, group.include, group.exclude);
if (isInGroup) {
groupedChangelogs[changelogPath] ??= {
changelogs: [],
masterPackage,
};
groupedChangelogs[changelogPath].changelogs.push(changelogs[pkg]);
groupedChangelogs[group.changelogAbsDir].changelogs.push(changelogs[pkg]);
}
}
}
const changelogAbsolutePaths: string[] = [];
for (const changelogPath in groupedChangelogs) {
const { masterPackage, changelogs } = groupedChangelogs[changelogPath];
// Write each grouped changelog if it's not empty
for (const [changelogAbsDir, { masterPackage, changelogs }] of Object.entries(groupedChangelogs)) {
const groupedChangelog = mergeChangelogs(changelogs, masterPackage);
if (groupedChangelog) {
await writeChangelogFiles(options, groupedChangelog, changelogPath, true);
changelogAbsolutePaths.push(path.resolve(changelogPath));
await writeChangelogFiles(options, groupedChangelog, changelogAbsDir, true);
}
}
return changelogAbsolutePaths;
// Return all the possible grouped changelog directories (even if there was nothing to write).
// Otherwise if a grouped changelog location overlaps with a package changelog location, and
// on one publish there are only dependent bump changes for that package (and no changes for
// other packages in the group), we'd get the package changelog updates with dependent bumps
// added to the otherwise-grouped changelog file.
return Object.keys(groupedChangelogs);
}
async function writeChangelogFiles(