diff --git a/change/beachball-526ee2eb-1931-449a-b24e-cc00cf631525.json b/change/beachball-526ee2eb-1931-449a-b24e-cc00cf631525.json new file mode 100644 index 000000000..7f0a0f76e --- /dev/null +++ b/change/beachball-526ee2eb-1931-449a-b24e-cc00cf631525.json @@ -0,0 +1,7 @@ +{ + "comment": "Add `'md'` and `'json'` options for `generateChangelog`", + "type": "minor", + "packageName": "beachball", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/docs/overview/configuration.md b/docs/overview/configuration.md index 2e2afd467..966599af9 100644 --- a/docs/overview/configuration.md +++ b/docs/overview/configuration.md @@ -67,7 +67,7 @@ For the latest full list of supported options, see `RepoOptions` [in this file]( | `defaultNpmTag` | string | `'latest'` | package | the default dist-tag used for NPM publish | | `disallowedChangeTypes` | string[] | | repo, group, package | what change types are disallowed | | `fetch` | bool | `true` | repo | fetch from remote before doing diff comparisons | -| `generateChangelog` | bool | `true` | repo | whether to generate changelog files | +| `generateChangelog` | bool, `'md'`, or `'json'` | `true` | repo | whether to generate `CHANGELOG.md/json` (`'md'` or `'json'` to generate only that type) | | `gitTags` | bool | `true` | repo, package | whether to create git tags for published packages (eg: foo_v1.0.1) | | `groups` | `VersionGroupOptions[]` ([details][3]) | | repo | specifies groups of packages that need to be version bumped at the same time | | `groupChanges` | bool | `false` | repo | will write multiple changes to a single changefile | diff --git a/src/__e2e__/bump.test.ts b/src/__e2e__/bump.test.ts index a8c488750..741632c94 100644 --- a/src/__e2e__/bump.test.ts +++ b/src/__e2e__/bump.test.ts @@ -602,7 +602,7 @@ describe('version bumping', () => { expect(modified).toContain('package2'); const changelogJson = readChangelogJson(repo.pathTo('packages/package2')); - expect(changelogJson.entries[0].comments.patch![0].comment).toBe('Bump package1 to v0.0.2'); + expect(changelogJson?.entries[0].comments.patch![0].comment).toBe('Bump package1 to v0.0.2'); }); it('calls sync prebump hook before packages are bumped', async () => { diff --git a/src/__fixtures__/changelog.ts b/src/__fixtures__/changelog.ts index d9ce88c2f..42c11b35d 100644 --- a/src/__fixtures__/changelog.ts +++ b/src/__fixtures__/changelog.ts @@ -4,16 +4,28 @@ import _ from 'lodash'; import { SortedChangeTypes } from '../changefile/changeTypes'; import { ChangelogJson } from '../types/ChangeLog'; -/** Read the CHANGELOG.md under the given package path, sanitizing any dates for snapshots */ -export function readChangelogMd(packagePath: string): string { +/** + * Read the CHANGELOG.md under the given package path, sanitizing any dates for snapshots. + * Returns null if it doesn't exist. + */ +export function readChangelogMd(packagePath: string): string | null { const changelogFile = path.join(packagePath, 'CHANGELOG.md'); + if (!fs.existsSync(changelogFile)) { + return null; + } const text = fs.readFileSync(changelogFile, { encoding: 'utf-8' }); return text.replace(/\w\w\w, \d\d \w\w\w [\d :]+?GMT/gm, '(date)'); } -/** Read the CHANGELOG.json under the given package path */ -export function readChangelogJson(packagePath: string, cleanForSnapshot: boolean = false): ChangelogJson { +/** + * Read the CHANGELOG.json under the given package path. + * Returns null if it doesn't exist. + */ +export function readChangelogJson(packagePath: string, cleanForSnapshot: boolean = false): ChangelogJson | null { const changelogJsonFile = path.join(packagePath, 'CHANGELOG.json'); + if (!fs.existsSync(changelogJsonFile)) { + return null; + } const json = fs.readJSONSync(changelogJsonFile, { encoding: 'utf-8' }); return cleanForSnapshot ? cleanChangelogJson(json) : json; } @@ -22,7 +34,10 @@ export function readChangelogJson(packagePath: string, cleanForSnapshot: boolean * Clean changelog json for a snapshot: replace dates and SHAs with placeholders. * Note: this clones the changelog object rather than modifying the original. */ -export function cleanChangelogJson(changelog: ChangelogJson): ChangelogJson { +export function cleanChangelogJson(changelog: ChangelogJson | null): ChangelogJson | null { + if (!changelog) { + return null; + } changelog = _.cloneDeep(changelog); // for a better snapshot, make the fake commit match if the real commit did const fakeCommits: { [commit: string]: string } = {}; diff --git a/src/__functional__/changelog/writeChangelog.test.ts b/src/__functional__/changelog/writeChangelog.test.ts index a62500dc0..45fa80217 100644 --- a/src/__functional__/changelog/writeChangelog.test.ts +++ b/src/__functional__/changelog/writeChangelog.test.ts @@ -8,16 +8,16 @@ import { writeChangelog } from '../../changelog/writeChangelog'; import { getPackageInfos } from '../../monorepo/getPackageInfos'; import { readChangeFiles } from '../../changefile/readChangeFiles'; import { BeachballOptions } from '../../types/BeachballOptions'; -import { ChangeFileInfo } from '../../types/ChangeInfo'; +import { ChangeFileInfo, ChangeType } from '../../types/ChangeInfo'; import type { Repository } from '../../__fixtures__/repository'; import { getDefaultOptions } from '../../options/getDefaultOptions'; -function getChange(packageName: string, comment: string): ChangeFileInfo { +function getChange(packageName: string, comment: string, type: ChangeType = 'patch'): ChangeFileInfo { return { comment, email: 'test@testtestme.com', packageName, - type: 'patch', + type, dependentChangeType: 'patch', }; } @@ -77,7 +77,7 @@ describe('writeChangelog', () => { expect(cleanChangelogJson(changelogJson)).toMatchSnapshot('changelog json'); // Every entry should have a different commit hash - const patchComments = changelogJson.entries[0].comments.patch!; + const patchComments = changelogJson!.entries[0].comments.patch!; const commits = patchComments.map(entry => entry.commit); expect(new Set(commits).size).toEqual(patchComments.length); @@ -111,7 +111,7 @@ describe('writeChangelog', () => { expect(cleanChangelogJson(changelogJson)).toMatchSnapshot('changelog json'); // Every entry should have a different commit hash - const patchComments = changelogJson.entries[0].comments.patch!; + const patchComments = changelogJson!.entries[0].comments.patch!; const commits = patchComments.map(entry => entry.commit); expect(new Set(commits).size).toEqual(patchComments.length); @@ -151,7 +151,7 @@ describe('writeChangelog', () => { expect(readChangelogJson(repo.pathTo('packages/bar'), true /*clean*/)).toMatchSnapshot('bar CHANGELOG.json'); // Every entry should have a different commit hash - const patchComments = fooJson.entries[0].comments.patch!; + const patchComments = fooJson!.entries[0].comments.patch!; const commits = patchComments.map(entry => entry.commit); expect(new Set(commits).size).toEqual(patchComments.length); @@ -296,4 +296,44 @@ describe('writeChangelog', () => { // Validate grouped changelog for foo and bar packages expect(readChangelogMd(repo.pathTo('packages/foo'))).toMatchSnapshot(); }); + + it('writes only CHANGELOG.md if generateChangelog is "md"', async () => { + repo = repositoryFactory.cloneRepository(); + const options = getOptions({ generateChangelog: 'md' }); + + repo.commitChange('foo'); + generateChangeFiles([getChange('foo', 'comment 1')], options); + + const packageInfos = getPackageInfos(repo.rootPath); + const changes = readChangeFiles(options, packageInfos); + + await writeChangelog(options, changes, { foo: 'patch' }, { foo: new Set(['foo']) }, packageInfos); + + // CHANGELOG.md is written + expect(readChangelogMd(repo.rootPath)).toContain('## 1.0.0'); + + // CHANGELOG.json is not written + expect(readChangelogJson(repo.rootPath)).toBeNull(); + }); + + it('writes only CHANGELOG.json if generateChangelog is "json"', async () => { + repo = repositoryFactory.cloneRepository(); + const options = getOptions({ generateChangelog: 'json' }); + + repo.commitChange('foo'); + generateChangeFiles([getChange('foo', 'comment 1')], options); + + const packageInfos = getPackageInfos(repo.rootPath); + const changes = readChangeFiles(options, packageInfos); + + await writeChangelog(options, changes, { foo: 'patch' }, { foo: new Set(['foo']) }, packageInfos); + + // CHANGELOG.md is not written + expect(readChangelogMd(repo.rootPath)).toBeNull(); + + // 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' })]); + }); }); diff --git a/src/changelog/writeChangelog.ts b/src/changelog/writeChangelog.ts index 435b0aa23..26ea6ec84 100644 --- a/src/changelog/writeChangelog.ts +++ b/src/changelog/writeChangelog.ts @@ -123,25 +123,25 @@ async function writeChangelogFiles( let previousJson: ChangelogJson | undefined; // Update CHANGELOG.json - const changelogJsonFile = path.join(changelogPath, 'CHANGELOG.json'); - try { - previousJson = fs.existsSync(changelogJsonFile) ? fs.readJSONSync(changelogJsonFile) : undefined; - } catch (e) { - console.warn(`${changelogJsonFile} is invalid: ${e}`); - } - try { - const nextJson = renderJsonChangelog(newVersionChangelog, previousJson); - fs.writeJSONSync(changelogJsonFile, nextJson, { spaces: 2 }); - } catch (e) { - console.warn(`Problem writing to ${changelogJsonFile}: ${e}`); + if (options.generateChangelog === true || options.generateChangelog === 'json') { + const changelogJsonFile = path.join(changelogPath, 'CHANGELOG.json'); + try { + previousJson = fs.existsSync(changelogJsonFile) ? fs.readJSONSync(changelogJsonFile) : undefined; + } catch (e) { + console.warn(`${changelogJsonFile} is invalid: ${e}`); + } + try { + const nextJson = renderJsonChangelog(newVersionChangelog, previousJson); + fs.writeJSONSync(changelogJsonFile, nextJson, { spaces: 2 }); + } catch (e) { + console.warn(`Problem writing to ${changelogJsonFile}: ${e}`); + } } - // Update CHANGELOG.md + // Update CHANGELOG.md if there are changes of types besides "none" if ( - newVersionChangelog.comments.major || - newVersionChangelog.comments.minor || - newVersionChangelog.comments.patch || - newVersionChangelog.comments.prerelease + (options.generateChangelog === true || options.generateChangelog === 'md') && + Object.entries(newVersionChangelog.comments).some(([type, comments]) => type !== 'none' && comments?.length) ) { const changelogFile = path.join(changelogPath, 'CHANGELOG.md'); const previousContent = fs.existsSync(changelogFile) ? fs.readFileSync(changelogFile).toString() : ''; diff --git a/src/types/BeachballOptions.ts b/src/types/BeachballOptions.ts index 9b33ce030..8dec4fba0 100644 --- a/src/types/BeachballOptions.ts +++ b/src/types/BeachballOptions.ts @@ -109,10 +109,13 @@ export interface RepoOptions { */ fetch: boolean; /** - * Whether to generate changelog files - * @default true + * Whether to generate changelog files. + * - `true` (default) to generate both CHANGELOG.md and CHANGELOG.json + * - `false` to skip changelog generation + * - `'md'` to generate only CHANGELOG.md + * - `'json'` to generate only CHANGELOG.json */ - generateChangelog: boolean; + generateChangelog: boolean | 'md' | 'json'; /** Options for bumping package versions together */ groups?: VersionGroupOptions[]; /**