This commit is contained in:
Elizabeth Craig 2023-07-26 18:02:54 -07:00 коммит произвёл GitHub
Родитель 6a450f70ea
Коммит 749812afdc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 364 добавлений и 163 удалений

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

@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add an index signature to ChangelogEntry",
"packageName": "beachball",
"email": "elcraig@microsoft.com",
"dependentChangeType": "patch"
}

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

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Stop recording incorrect bump commits in CHANGELOG.json",
"packageName": "beachball",
"email": "elcraig@microsoft.com",
"dependentChangeType": "patch"
}

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

@ -255,7 +255,7 @@ describe('writeChangelog', () => {
expect(readChangelogMd(monoRepo.pathTo('packages/foo'))).toMatchSnapshot();
});
it('Verify that the changeFile transform functions are run, if provided', async () => {
it('runs transform.changeFiles functions if provided', async () => {
const editedComment: string = 'Edited comment for testing';
const monoRepo = monoRepoFactory.cloneRepository();
monoRepo.commitChange('foo');

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

@ -1,123 +1,203 @@
import { describe, expect, it } from '@jest/globals';
import { describe, expect, it, jest } from '@jest/globals';
import { getPackageChangelogs } from '../../changelog/getPackageChangelogs';
import { BumpInfo } from '../../types/BumpInfo';
import { ChangeSet } from '../../types/ChangeInfo';
import { ChangeFileInfo, ChangeSet } from '../../types/ChangeInfo';
import { PackageInfos } from '../../types/PackageInfo';
import { makePackageInfos } from '../../__fixtures__/packageInfos';
// Mock the methods used from workspace-tools so we don't access the filesystem
jest.mock('workspace-tools', () => ({
findProjectRoot: () => '.',
getFileAddedHash: () => 'deadbeef',
}));
function makeChangeInfo(pkg: string, overrides?: Partial<ChangeFileInfo>): ChangeSet[number] {
return {
changeFile: `${pkg}.json`,
change: {
comment: `comment for ${pkg}`,
dependentChangeType: 'patch',
email: 'something@something.com',
packageName: pkg,
type: 'patch',
...overrides,
},
};
}
describe('getPackageChangelogs', () => {
it('should have multiple comment entries when a package has a changefile AND was part of a dependent bump', () => {
it('generates correct changelog entries for a single package', () => {
const changeFileChangeInfos: ChangeSet = [
{
changeFile: 'foo.json',
change: {
comment: 'comment for foo',
commit: 'deadbeef',
dependentChangeType: 'patch',
email: 'something@something.com',
packageName: 'foo',
type: 'patch',
},
},
{
changeFile: 'bar.json',
change: {
comment: 'comment for bar',
commit: 'deadbeef',
dependentChangeType: 'patch',
email: 'something@something.com',
packageName: 'bar',
type: 'patch',
},
},
makeChangeInfo('foo'),
makeChangeInfo('foo', { type: 'minor', comment: 'other comment' }),
];
const packageInfos = makePackageInfos({ foo: { version: '1.0.0' } });
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes: { foo: 'patch' },
packageInfos,
cwd: '.',
});
expect(changelogs.foo).toEqual({
comments: {
minor: [{ author: 'something@something.com', comment: 'other comment', commit: 'deadbeef', package: 'foo' }],
patch: [{ author: 'something@something.com', comment: 'comment for foo', commit: 'deadbeef', package: 'foo' }],
},
date: expect.any(Date),
name: 'foo',
tag: 'foo_v1.0.0',
version: '1.0.0',
});
expect(changelogs.foo.comments.patch).toHaveLength(1);
});
it('generates correct changelog entries for multiple packages', () => {
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo'), makeChangeInfo('bar')];
const packageInfos = makePackageInfos({
foo: { version: '1.0.0' },
bar: { version: '2.0.0' },
});
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
packageInfos,
cwd: '.',
});
expect(changelogs.foo).toEqual({
comments: {
patch: [{ author: 'something@something.com', comment: 'comment for foo', commit: 'deadbeef', package: 'foo' }],
},
date: expect.any(Date),
name: 'foo',
tag: 'foo_v1.0.0',
version: '1.0.0',
});
expect(changelogs.bar).toEqual({
comments: {
patch: [{ author: 'something@something.com', comment: 'comment for bar', commit: 'deadbeef', package: 'bar' }],
},
date: expect.any(Date),
name: 'bar',
tag: 'bar_v2.0.0',
version: '2.0.0',
});
});
it('preserves custom properties from change files', () => {
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo', { extra: 'prop' })];
const packageInfos: PackageInfos = makePackageInfos({ foo: { version: '1.0.0' } });
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes: { foo: 'patch' },
packageInfos,
cwd: '.',
});
expect(changelogs.foo.comments.patch![0]).toMatchObject({ extra: 'prop' });
});
it('records dependent bumps', () => {
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo')];
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {
bar: new Set(['foo']),
};
const packageInfos: PackageInfos = {
foo: {
combinedOptions: {} as any,
name: 'foo',
packageJsonPath: 'packages/foo/package.json',
packageOptions: {},
private: false,
version: '1.0.0',
dependencies: {
bar: '^1.0.0',
},
},
bar: {
combinedOptions: {} as any,
name: 'bar',
packageJsonPath: 'packages/bar/package.json',
packageOptions: {},
private: false,
version: '1.0.0',
},
};
const packageInfos = makePackageInfos({
foo: { version: '1.0.0' },
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
});
const changelogs = getPackageChangelogs(
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
{ foo: 'patch', bar: 'patch' },
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
dependentChangedBy,
packageInfos,
'.'
);
cwd: '.',
});
expect(Object.keys(changelogs.bar.comments.patch!)).toHaveLength(2);
expect(Object.keys(changelogs.foo.comments.patch!)).toHaveLength(1);
expect(changelogs.bar).toEqual({
comments: {
patch: [
{
author: 'beachball',
package: 'bar',
comment: 'Bump foo to v1.0.0',
// IMPORTANT: this should not record an actual commit hash, because it will be incorrect
commit: 'not available',
},
],
},
date: expect.any(Date),
name: 'bar',
tag: 'bar_v2.0.0',
version: '2.0.0',
});
expect(Object.keys(changelogs.bar.comments.patch!)).toHaveLength(1);
});
it('should not generate change logs for dependent bumps of private packages', () => {
const changeFileChangeInfos: ChangeSet = [
{
changeFile: 'bar.json',
change: {
comment: 'comment for bar',
commit: 'deadbeef',
dependentChangeType: 'patch',
email: 'something@something.com',
packageName: 'bar',
type: 'patch',
},
},
];
it('records multiple comment entries when a package has a change file AND was part of a dependent bump', () => {
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('foo'), makeChangeInfo('bar')];
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {
bar: new Set(['foo']),
};
const packageInfos = makePackageInfos({
foo: { version: '1.0.0' },
bar: { version: '2.0.0', dependencies: { foo: '^1.0.0' } },
});
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes: { foo: 'patch', bar: 'patch' },
dependentChangedBy,
packageInfos,
cwd: '.',
});
expect(changelogs.bar.comments).toEqual({
patch: [
expect.objectContaining({ comment: 'comment for bar' }),
expect.objectContaining({ comment: 'Bump foo to v1.0.0' }),
],
});
expect(changelogs.foo.comments).toEqual({
patch: [expect.objectContaining({ comment: 'comment for foo' })],
});
});
it('does not generate changelogs for dependent bumps of private packages', () => {
const changeFileChangeInfos: ChangeSet = [makeChangeInfo('bar')];
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {
'private-pkg': new Set(['bar']),
};
const packageInfos: PackageInfos = {
const packageInfos = makePackageInfos({
'private-pkg': {
combinedOptions: {} as any,
name: 'private-pkg',
packageJsonPath: 'packages/private-pkg/package.json',
packageOptions: {},
version: '1.0.0',
private: true,
version: '1.0.0',
dependencies: {
bar: '^1.0.0',
},
dependencies: { bar: '^1.0.0' },
},
bar: {
combinedOptions: {} as any,
name: 'bar',
packageJsonPath: 'packages/bar/package.json',
packageOptions: {},
private: false,
version: '1.0.0',
},
};
bar: { version: '1.0.0' },
});
const changelogs = getPackageChangelogs(
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
{ bar: 'patch', 'private-pkg': 'patch' },
calculatedChangeTypes: { bar: 'patch', 'private-pkg': 'patch' },
dependentChangedBy,
packageInfos,
'.'
);
cwd: '.',
});
expect(changelogs.bar).toBeTruthy();
expect(changelogs['private-pkg']).toBeUndefined();
});
});

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

@ -1,6 +1,7 @@
import { describe, expect, it } from '@jest/globals';
import { describe, expect, it, jest } from '@jest/globals';
import { initMockLogs } from '../../__fixtures__/mockLogs';
import { MarkdownChangelogRenderOptions, renderChangelog, markerComment } from '../../changelog/renderChangelog';
import { ChangelogEntry } from '../../types/ChangeLog';
const previousHeader = `# Change Log - foo
@ -25,18 +26,8 @@ describe('renderChangelog', () => {
comments: {
major: [],
minor: [
{
comment: 'Awesome change',
author: 'user1@example.com',
commit: 'sha1',
package: 'foo',
},
{
comment: 'Boring change',
author: 'user2@example.com',
commit: 'sha2',
package: 'foo',
},
{ comment: 'Awesome change', author: 'user1@example.com', commit: 'sha1', package: 'foo' },
{ comment: 'Boring change', author: 'user2@example.com', commit: 'sha2', package: 'foo' },
],
patch: [
{ comment: 'Fix', author: 'user1@example.com', commit: 'sha3', package: 'foo' },
@ -116,4 +107,29 @@ describe('renderChangelog', () => {
expect(result).toContain('content here'); // includes previous content
expect(result).toMatchSnapshot();
});
it('passes custom change file properties to renderers', async () => {
const options = getOptions();
options.newVersionChangelog.comments = {
patch: [
{
comment: 'Awesome change',
author: 'user1@example.com',
commit: 'sha1',
package: 'foo',
extra: 'custom',
},
],
};
options.changelogOptions.customRenderers = {
renderEntry: jest.fn(async (entry: ChangelogEntry) => `- ${entry.comment} ${entry.extra})`),
};
const result = await renderChangelog(options);
expect(result).toContain('Awesome change custom');
expect(options.changelogOptions.customRenderers.renderEntry).toHaveBeenCalledWith(
expect.objectContaining({ extra: 'custom' }),
expect.anything()
);
});
});

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

@ -0,0 +1,63 @@
import { describe, expect, it } from '@jest/globals';
import { ChangelogJson, PackageChangelog } from '../..';
import { renderJsonChangelog } from '../../changelog/renderJsonChangelog';
describe('renderJsonChangelog', () => {
function getChangelog(): PackageChangelog {
return {
date: new Date('Thu Aug 22 2019 14:20:40 GMT-0700 (Pacific Daylight Time)'),
name: 'foo',
tag: 'foo_v1.2.3',
version: '1.2.3',
comments: {
minor: [
{ comment: 'Awesome change', author: 'user1@example.com', commit: 'sha1', package: 'foo' },
{ comment: 'Boring change', author: 'user2@example.com', commit: 'sha2', package: 'foo' },
],
patch: [
{ comment: 'Fix', author: 'user1@example.com', commit: 'sha3', package: 'foo' },
{ comment: 'stuff', author: 'user2@example.com', commit: 'sha4', package: 'foo' },
],
},
};
}
it('renders if no previous changelog', () => {
const changelog = getChangelog();
const { name, ...rest } = changelog;
const finalChangeLog = renderJsonChangelog(changelog, undefined);
expect(finalChangeLog).toEqual({
name,
entries: [
{
...rest,
date: 'Thu, 22 Aug 2019 21:20:40 GMT',
},
],
});
});
it('preserves previous entries', () => {
const changelog = getChangelog();
const previousChangelog: ChangelogJson = {
name: 'foo',
entries: [
{
date: 'Thu, 21 Aug 2019 20:20:40 GMT',
version: '1.2.2',
tag: 'foo_v1.2.2',
comments: {
patch: [{ comment: 'Fix', author: 'user1@example.com', commit: 'sha3', package: 'foo' }],
},
},
],
};
const finalChangeLog = renderJsonChangelog(changelog, previousChangelog);
expect(finalChangeLog).toEqual({
name: 'foo',
entries: [expect.objectContaining({ version: '1.2.3' }), expect.objectContaining({ version: '1.2.2' })],
});
});
});

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

@ -19,18 +19,8 @@ describe('changelog renderers -', () => {
comments: {
major: [],
minor: [
{
comment: 'Awesome change',
author: 'user1@example.com',
commit: 'sha1',
package: 'foo',
},
{
comment: 'Boring change',
author: 'user2@example.com',
commit: 'sha2',
package: 'foo',
},
{ comment: 'Awesome change', author: 'user1@example.com', commit: 'sha1', package: 'foo' },
{ comment: 'Boring change', author: 'user2@example.com', commit: 'sha2', package: 'foo' },
],
patch: [
{ comment: 'Fix', author: 'user1@example.com', commit: 'sha3', package: 'foo' },

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

@ -1,35 +1,40 @@
import path from 'path';
import { PackageInfo } from '../types/PackageInfo';
import { PackageInfo, PackageInfos } from '../types/PackageInfo';
import { PackageChangelog } from '../types/ChangeLog';
import { generateTag } from '../git/generateTag';
import { BumpInfo } from '../types/BumpInfo';
import { getChangePath } from '../paths';
import { getCurrentHash, getFileAddedHash } from 'workspace-tools';
import { getFileAddedHash } from 'workspace-tools';
import { ChangeSet } from '../types/ChangeInfo';
export function getPackageChangelogs(
changeFileChangeInfos: ChangeSet,
calculatedChangeTypes: BumpInfo['calculatedChangeTypes'],
dependentChangedBy: BumpInfo['dependentChangedBy'],
packageInfos: {
[pkg: string]: PackageInfo;
},
cwd: string
): { [pkgName: string]: PackageChangelog } {
const changelogs: { [pkgName: string]: PackageChangelog } = {};
/**
* Used for `ChangelogEntry.commit` if the commit hash is not available.
*/
const commitNotAvailable = 'not available';
/**
* Get the preliminary changelog info for each modified package, based on change files and dependent bumps.
* @returns Mapping from package name to package changelog.
*/
export function getPackageChangelogs(params: {
changeFileChangeInfos: ChangeSet;
calculatedChangeTypes: BumpInfo['calculatedChangeTypes'];
dependentChangedBy?: BumpInfo['dependentChangedBy'];
packageInfos: PackageInfos;
cwd: string;
}): Record<string, PackageChangelog> {
const { changeFileChangeInfos, calculatedChangeTypes, dependentChangedBy = {}, packageInfos, cwd } = params;
const changelogs: Record<string, PackageChangelog> = {};
const changeFileCommits: { [changeFile: string]: string } = {};
const changePath = getChangePath(cwd);
for (let { change, changeFile } of changeFileChangeInfos) {
for (const { change, changeFile } of changeFileChangeInfos) {
const { packageName, type: changeType, dependentChangeType, email, ...rest } = change;
if (!changelogs[packageName]) {
changelogs[packageName] = createChangeLog(packageInfos[packageName]);
}
changelogs[packageName] ??= createPackageChangelog(packageInfos[packageName]);
if (!changeFileCommits[changeFile]) {
changeFileCommits[changeFile] = getFileAddedHash(path.join(changePath, changeFile), cwd) || 'not available';
}
changeFileCommits[changeFile] ??= getFileAddedHash(path.join(changePath, changeFile), cwd) || commitNotAvailable;
changelogs[packageName].comments ??= {};
changelogs[packageName].comments[changeType] ??= [];
@ -43,18 +48,14 @@ export function getPackageChangelogs(
});
}
const commit = getCurrentHash(cwd) || 'not available';
for (let [dependent, changedBy] of Object.entries(dependentChangedBy)) {
for (const [dependent, changedBy] of Object.entries(dependentChangedBy)) {
if (packageInfos[dependent].private === true) {
// Avoid creation of change log files for private packages since the version is
// not managed by beachball and the log would only contain bumps to dependencies.
continue;
}
if (!changelogs[dependent]) {
changelogs[dependent] = createChangeLog(packageInfos[dependent]);
}
changelogs[dependent] ??= createPackageChangelog(packageInfos[dependent]);
const changeType = calculatedChangeTypes[dependent];
@ -67,7 +68,11 @@ export function getPackageChangelogs(
author: 'beachball',
package: dependent,
comment: `Bump ${dep} to v${packageInfos[dep].version}`,
commit,
// This change will be made in the commit that is currently being created, so unless we
// split publishing into two commits (one for bumps and one for changelog updates),
// there's no way to know the hash yet. It's better to record nothing than incorrect info.
// https://github.com/microsoft/beachball/issues/901
commit: commitNotAvailable,
});
}
}
@ -76,7 +81,7 @@ export function getPackageChangelogs(
return changelogs;
}
function createChangeLog(packageInfo: PackageInfo): PackageChangelog {
function createPackageChangelog(packageInfo: PackageInfo): PackageChangelog {
const name = packageInfo.name;
const version = packageInfo.version;
return {

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

@ -1,20 +1,18 @@
import { generateTag } from '../git/generateTag';
import { PackageChangelog, ChangelogJson, ChangelogJsonEntry } from '../types/ChangeLog';
import { PackageChangelog, ChangelogJson } from '../types/ChangeLog';
export function renderJsonChangelog(
changelog: PackageChangelog,
previousChangelog: ChangelogJson | undefined
): ChangelogJson {
const result: ChangelogJson = {
name: changelog.name,
entries: previousChangelog?.entries ? [...previousChangelog.entries] : [],
const { name, date, ...rest } = changelog;
return {
name,
entries: [
{
date: changelog.date.toUTCString(),
...rest,
},
...(previousChangelog?.entries || []),
],
};
const newEntry: ChangelogJsonEntry = {
date: changelog.date.toUTCString(),
tag: generateTag(changelog.name, changelog.version),
version: changelog.version,
comments: changelog.comments,
};
result.entries.unshift(newEntry);
return result;
}

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

@ -27,13 +27,13 @@ export async function writeChangelog(
);
const groupedChangelogPathSet = new Set(groupedChangelogPaths);
const changelogs = getPackageChangelogs(
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes,
dependentChangedBy,
packageInfos,
options.path
);
cwd: options.path,
});
// Use a standard for loop here to prevent potentially firing off multiple network requests at once
// (in case any custom renderers have network requests)
for (const pkg of Object.keys(changelogs)) {
@ -62,7 +62,12 @@ async function writeGroupedChangelog(
}
// Grouped changelogs should not contain dependency bump entries
const changelogs = getPackageChangelogs(changeFileChangeInfos, calculatedChangeTypes, {}, packageInfos, options.path);
const changelogs = getPackageChangelogs({
changeFileChangeInfos,
calculatedChangeTypes,
packageInfos,
cwd: options.path,
});
const groupedChangelogs: {
[path: string]: {
changelogs: PackageChangelog[];

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

@ -2,12 +2,17 @@ export type ChangeType = 'prerelease' | 'patch' | 'minor' | 'major' | 'none';
/**
* Info saved in each change file.
* (For entries in CHANGELOG.json, see `ChangelogEntry` in ./ChangeLog.ts.)
*/
export interface ChangeFileInfo {
type: ChangeType;
/** Change comment */
comment: string;
/** Package name the change was in */
packageName: string;
/** Author email */
email: string;
/** How to bump packages that depend on this one */
dependentChangeType: ChangeType;
/** Extra info added to the change file via custom prompts */
[extraInfo: string]: any;
@ -20,11 +25,14 @@ export interface ChangeInfo extends ChangeFileInfo {
commit: string;
}
/**
* Info saved in each grouped change file.
*/
export interface ChangeInfoMultiple {
changes: ChangeInfo[];
}
/**
* List of change file infos
* List of change file infos (not actually a set).
*/
export type ChangeSet = { changeFile: string; change: ChangeFileInfo }[];

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

@ -5,20 +5,37 @@
import { ChangeType } from './ChangeInfo';
/**
* Entry ("comment") in CHANGELOG.json from a change file or dependent bump.
* These objects are saved under the `ChangelogJson`'s `entries[].comments[type]`.
*
* (This is based on an individual `ChangeFileInfo` from ./ChangeInfo.ts, but with some different
* naming and details.)
*/
export interface ChangelogEntry {
/** Change comment */
comment: string;
/** Author email */
author: string;
/** Commit hash */
/**
* Commit hash.
*
* For changelogs generated by beachball versions \>=2.36.0, should be `"not available"` for
* bump entries, because the correct commit doesn't exist yet (see [issue](https://github.com/microsoft/beachball/issues/901)).
*
* Could also be `"not available"` for other commits if there was an issue determing the hash
* at changelog generation time.
*/
commit: string;
/** Package name the change was in */
package: string;
/** Extra info added to the change file via custom prompts */
[extraInfo: string]: any;
}
/**
* Changelog info for an individual version. Usually this is for a single package.
* If using grouped changelogs, it could be for multiple packages.
* Intermediate info used to generate a CHANGELOG.json entry for an individual version.
* Usually this is for a single package. If using grouped changelogs, it could be for multiple packages.
*/
export interface PackageChangelog {
/** Package name (if a grouped changelog, for the primary package) */
@ -34,15 +51,20 @@ export interface PackageChangelog {
}
/**
* CHANGELOG.json entry for an individual version. Usually this is for a single package.
* If using grouped changelogs, it could be for multiple packages.
* CHANGELOG.json entry for an individual version (under `ChangelogJson`'s `entries`).
* Usually this is for a single package. If using grouped changelogs, it could be for multiple packages.
*/
export type ChangelogJsonEntry = Omit<PackageChangelog, 'name' | 'date'> & {
/** Version creation date as a string */
date: string;
};
/**
* CHANGELOG.json file contents.
*/
export interface ChangelogJson {
/** Package name */
name: string;
/** Entries for each package version */
entries: ChangelogJsonEntry[];
}