diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 3cae922..06f9028 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -16,6 +16,8 @@ jobs: - run: yarn install --frozen-lockfile - run: yarn setup:postinstall - run: yarn build + # below step will fail if there're still changes to commit after "yarn build" + # see if dist/index.js modified after "yarn build". - run: git diff --quiet || { echo 'working directory dirty after "yarn build"'; exit 1; } - run: yarn lint - run: yarn test diff --git a/dist/index.js b/dist/index.js index a2a9fb0..cf5369e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -64800,11 +64800,29 @@ async function updatePackageChangelog(packageMetadata, updateSpecification, root repoUrl: repositoryUrl, formatter: formatChangelog, }); - if (!newChangelogContent) { + if (newChangelogContent) { + return await external_fs_.promises.writeFile(changelogPath, newChangelogContent); + } + const hasUnReleased = hasUnreleasedChanges(changelogContent, repositoryUrl); + if (!hasUnReleased) { const packageName = packageMetadata.manifest.name; throw new Error(`"updateChangelog" returned an empty value for package ${packageName ? `"${packageName}"` : `at "${packagePath}"`}.`); } - return await external_fs_.promises.writeFile(changelogPath, newChangelogContent); + return undefined; +} +/** + * Checks if there are unreleased changes in the changelog. + * @param changelogContent - The string formatted changelog. + * @param repositoryUrl - The repository url. + * @returns The boolean true if there are unreleased changes, otherwise false. + */ +function hasUnreleasedChanges(changelogContent, repositoryUrl) { + const changelog = (0,auto_changelog_dist.parseChangelog)({ + changelogContent, + repoUrl: repositoryUrl, + formatter: formatChangelog, + }); + return Object.keys(changelog.getUnreleasedChanges()).length !== 0; } /** * Updates the given manifest per the update specification as follows: diff --git a/src/package-operations.test.ts b/src/package-operations.test.ts index 4c63eed..cd3f9d8 100644 --- a/src/package-operations.test.ts +++ b/src/package-operations.test.ts @@ -40,6 +40,7 @@ jest.mock('@metamask/action-utils/dist/file-utils', () => { jest.mock('@metamask/auto-changelog', () => { return { updateChangelog: jest.fn(), + parseChangelog: jest.fn(), }; }); @@ -260,6 +261,7 @@ describe('package-operations', () => { const readFileMock = jest.spyOn(fs.promises, 'readFile'); const updateChangelogMock = jest.spyOn(autoChangelog, 'updateChangelog'); + const parseChangelogMock = jest.spyOn(autoChangelog, 'parseChangelog'); const getMockPackageMetadata = ( dirPath: string, @@ -354,6 +356,7 @@ describe('package-operations', () => { getMockWritePath(dir, 'CHANGELOG.md'), mockNewChangelog, ); + expect(parseChangelogMock).toHaveBeenCalledTimes(0); }); it('re-throws changelog read error', async () => { @@ -437,8 +440,19 @@ describe('package-operations', () => { const changelogContent = 'I am a changelog.'; readFileMock.mockImplementationOnce(async () => changelogContent); - // This will cause an error + // no new changelog content and no unreleased changes will cause an error updateChangelogMock.mockImplementation(async () => ''); + const actualChangelog = jest.requireActual( + '@metamask/auto-changelog/dist/changelog', + ); + parseChangelogMock.mockImplementationOnce(() => { + return { + ...actualChangelog, + getUnreleasedChanges() { + return {}; + }, + }; + }); const packageMetadata = getMockPackageMetadata(dir, manifest); const updateSpecification = { @@ -472,6 +486,74 @@ describe('package-operations', () => { repoUrl, formatter: expect.any(Function), }); + expect(parseChangelogMock).toHaveBeenCalledTimes(1); + expect(parseChangelogMock).toHaveBeenCalledWith({ + changelogContent, + repoUrl, + formatter: expect.any(Function), + }); + }); + + it('succeeds if updated changelog is empty, but there are unreleased changes', async () => { + const originalVersion = '1.0.0'; + const newVersion = '1.0.1'; + const dir = mockDirs[0]; + const name = packageNames[0]; + const manifest = getMockManifest(name, originalVersion); + + const repoUrl = 'https://fake'; + const changelogContent = 'I am a changelog.'; + readFileMock.mockImplementationOnce(async () => changelogContent); + + updateChangelogMock.mockImplementation(async () => ''); + const actualChangelog = jest.requireActual( + '@metamask/auto-changelog/dist/changelog', + ); + parseChangelogMock.mockImplementationOnce(() => { + return { + ...actualChangelog, + getUnreleasedChanges() { + return { + Fixed: ['Something'], + }; + }, + }; + }); + + const packageMetadata = getMockPackageMetadata(dir, manifest); + const updateSpecification = { + newVersion, + packagesToUpdate: new Set(packageNames), + repositoryUrl: repoUrl, + shouldUpdateChangelog: true, + synchronizeVersions: false, + }; + + await updatePackage(packageMetadata, updateSpecification); + expect(writeFileMock).toHaveBeenCalledTimes(1); + expect(writeFileMock).toHaveBeenNthCalledWith( + 1, + getMockWritePath(dir, 'package.json'), + jsonStringify({ + ...cloneDeep(manifest), + [ManifestFieldNames.Version]: newVersion, + }), + ); + expect(updateChangelogMock).toHaveBeenCalledTimes(1); + expect(updateChangelogMock).toHaveBeenCalledWith({ + changelogContent, + currentVersion: newVersion, + isReleaseCandidate: true, + projectRootDirectory: dir, + repoUrl, + formatter: expect.any(Function), + }); + expect(parseChangelogMock).toHaveBeenCalledTimes(1); + expect(parseChangelogMock).toHaveBeenCalledWith({ + changelogContent, + repoUrl, + formatter: expect.any(Function), + }); }); it('throws if updated changelog is empty, and handles missing package name', async () => { @@ -486,8 +568,19 @@ describe('package-operations', () => { const changelogContent = 'I am a changelog.'; readFileMock.mockImplementationOnce(async () => changelogContent); - // This will cause an error + // no new changelog content and no unreleased changes will cause an error updateChangelogMock.mockImplementation(async () => ''); + const actualChangelog = jest.requireActual( + '@metamask/auto-changelog/dist/changelog', + ); + parseChangelogMock.mockImplementationOnce(() => { + return { + ...actualChangelog, + getUnreleasedChanges() { + return {}; + }, + }; + }); const packageMetadata = getMockPackageMetadata(dir, manifest); const updateSpecification = { @@ -521,6 +614,12 @@ describe('package-operations', () => { repoUrl, formatter: expect.any(Function), }); + expect(parseChangelogMock).toHaveBeenCalledTimes(1); + expect(parseChangelogMock).toHaveBeenCalledWith({ + changelogContent, + repoUrl, + formatter: expect.any(Function), + }); }); it('updates a package without synchronizing dependency versions', async () => { diff --git a/src/package-operations.ts b/src/package-operations.ts index 2f8cb37..a6132f2 100644 --- a/src/package-operations.ts +++ b/src/package-operations.ts @@ -12,7 +12,7 @@ import { validatePolyrepoPackageManifest, writeJsonFile, } from '@metamask/action-utils'; -import { updateChangelog } from '@metamask/auto-changelog'; +import { parseChangelog, updateChangelog } from '@metamask/auto-changelog'; import { promises as fs } from 'fs'; import pathUtils from 'path'; import prettier from 'prettier'; @@ -276,7 +276,13 @@ async function updatePackageChangelog( repoUrl: repositoryUrl, formatter: formatChangelog, }); - if (!newChangelogContent) { + + if (newChangelogContent) { + return await fs.writeFile(changelogPath, newChangelogContent); + } + + const hasUnReleased = hasUnreleasedChanges(changelogContent, repositoryUrl); + if (!hasUnReleased) { const packageName = packageMetadata.manifest.name; throw new Error( `"updateChangelog" returned an empty value for package ${ @@ -285,7 +291,26 @@ async function updatePackageChangelog( ); } - return await fs.writeFile(changelogPath, newChangelogContent); + return undefined; +} + +/** + * Checks if there are unreleased changes in the changelog. + * @param changelogContent - The string formatted changelog. + * @param repositoryUrl - The repository url. + * @returns The boolean true if there are unreleased changes, otherwise false. + */ +function hasUnreleasedChanges( + changelogContent: string, + repositoryUrl: string, +): boolean { + const changelog = parseChangelog({ + changelogContent, + repoUrl: repositoryUrl, + formatter: formatChangelog, + }); + + return Object.keys(changelog.getUnreleasedChanges()).length !== 0; } /**