From 545bd40f7d0d17a3f791f59bb9bb0b66323569c4 Mon Sep 17 00:00:00 2001 From: Matt Seccafien Date: Mon, 8 Nov 2021 18:03:00 +0100 Subject: [PATCH 1/3] feat: Lint changelogs for "Keep a changelog" semantics --- package.json | 3 +- tests/changelogs.test.ts | 154 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 tests/changelogs.test.ts diff --git a/package.json b/package.json index 0c9829af5a..5992ca0b99 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "ts-node": "^10.2.1", "typescript": "^4.2.3", "vite": "^2.6.0", - "yorkie": "^2.0.0" + "yorkie": "^2.0.0", + "glob": "^7.2.0" }, "gitHooks": { "pre-commit": "lint-staged", diff --git a/tests/changelogs.test.ts b/tests/changelogs.test.ts new file mode 100644 index 0000000000..e681a69129 --- /dev/null +++ b/tests/changelogs.test.ts @@ -0,0 +1,154 @@ +import {join, resolve} from 'path'; + +import {readFileSync} from 'fs-extra'; +import glob from 'glob'; + +const ROOT_PATH = resolve(__dirname, '..'); + +const HEADER_START_REGEX = /^## /; +const CHANGELOG_INTRO = `# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).`; + +readChangelogs().forEach(({packageChangelogPath, packageChangelog}) => { + describe(`changelog consistency for ${packageChangelogPath}`, () => { + it('begins with the "Keep a Changelog" intro section', () => { + const actualIntro = packageChangelog.substring(0, CHANGELOG_INTRO.length); + + expect(actualIntro).toBe(CHANGELOG_INTRO); + }); + + it('contains only known headers', () => { + const headerLines = packageChangelog + .split('\n') + .filter((line) => /^\s*#/.exec(line)); + const offendingHeaders = headerLines.filter( + (headerLine) => !headerIsAllowed(headerLine) + ); + + expect(offendingHeaders).toStrictEqual([]); + }); + + it('has exactly 1 empty line before headings', () => { + const notEnoughSpacingBeforeHeadings = /[^\n]+\n^#.*$/gm; + + expect(packageChangelog).not.toStrictEqual( + expect.stringMatching(notEnoughSpacingBeforeHeadings) + ); + }); + + it('has exactly 1 empty line after headings', () => { + const notEnoughSpacingAfterHeadings = /^#.*$\n[^\n]+/gm; + + expect(packageChangelog).not.toStrictEqual( + expect.stringMatching(notEnoughSpacingAfterHeadings) + ); + }); + + it('contains an Unreleased header with content, or a commented out Unreleased header with no content', () => { + // One of the following must be present + // - An Unreleased header, that is immediatly preceded by a level 3 heading ("Changed" etc) + // - An Unreleased header, that is immediatly preceded by a bullet ("- ...") + // - A commented out Unreleased header, that is immediatly preceded by a level 2 heading (Version info) + + const unrelasedHeaderWithContent = /^## Unreleased\n\n- /gm; + const unrelasedHeaderWithSubHeader = /^## Unreleased\n\n### /gm; + const commentedUnreleasedHeaderWithNoContent = + /^\n\n## /gm; + + expect([ + unrelasedHeaderWithContent.test(packageChangelog) || + unrelasedHeaderWithSubHeader.test(packageChangelog), + commentedUnreleasedHeaderWithNoContent.test(packageChangelog), + ]).toContain(true); + }); + + it('does not contain duplicate headers', () => { + const headerLines = packageChangelog + .split('\n') + .filter( + (line) => HEADER_START_REGEX.exec(line) || /## Unreleased/.exec(line) + ) + .sort(); + const uniqueHeaderLines = headerLines.filter( + (element, index, array) => array.indexOf(element) === index + ); + + expect(headerLines).toStrictEqual(uniqueHeaderLines); + }); + }); +}); + +const allowedHeaders = [ + '# Changelog', + '## Unreleased', + /^## \d+\.\d+\.\d-\w+\.\d+ - \d\d\d\d-\d\d-\d\d$/, // -alpha.x releases + /^## \d+\.\d+\.\d+ - \d\d\d\d-\d\d-\d\d$/, + '### Fixed', + '### Added', + '### Changed', + '### Deprecated', + '### Removed', + '### Security', + /^####/, +]; + +function headerIsAllowed(headerLine) { + return allowedHeaders.some((allowedHeader) => { + if (allowedHeader instanceof RegExp) { + return allowedHeader.test(headerLine); + } else { + return allowedHeader === headerLine; + } + }); +} + +function readChangelogs() { + const packagesPath = join(ROOT_PATH, 'packages'); + + return glob + .sync(join(packagesPath, '*/')) + .filter(hasPackageJSON) + .filter(hasChangelog) + .map((packageDir) => { + const packageChangelogPath = join(packageDir, 'CHANGELOG.md'); + const packageChangelog = safeReadSync(packageChangelogPath, { + encoding: 'utf8', + }).toString('utf-8'); + + return { + packageDir, + packageChangelogPath, + packageChangelog, + }; + }); +} + +function safeReadSync(path, options) { + try { + return readFileSync(path, options); + } catch { + return ''; + } +} + +function hasChangelog(packageDir) { + const changelogJSONPath = join(packageDir, 'CHANGELOG.md'); + const changelog = safeReadSync(changelogJSONPath, { + encoding: 'utf8', + }); + + return changelog.length > 0; +} + +function hasPackageJSON(packageDir) { + const packageJSONPath = join(packageDir, 'package.json'); + const packageJSON = safeReadSync(packageJSONPath, { + encoding: 'utf8', + }); + + return packageJSON.length > 0; +} From fce58643700d457af761cc575a6de3e3f0577e19 Mon Sep 17 00:00:00 2001 From: Matt Seccafien Date: Mon, 8 Nov 2021 19:32:33 +0100 Subject: [PATCH 2/3] Update tests/changelogs.test.ts Co-authored-by: Josh Larson --- tests/changelogs.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/changelogs.test.ts b/tests/changelogs.test.ts index e681a69129..94350d04a8 100644 --- a/tests/changelogs.test.ts +++ b/tests/changelogs.test.ts @@ -54,8 +54,8 @@ readChangelogs().forEach(({packageChangelogPath, packageChangelog}) => { // - An Unreleased header, that is immediatly preceded by a bullet ("- ...") // - A commented out Unreleased header, that is immediatly preceded by a level 2 heading (Version info) - const unrelasedHeaderWithContent = /^## Unreleased\n\n- /gm; - const unrelasedHeaderWithSubHeader = /^## Unreleased\n\n### /gm; + const unreleasedHeaderWithContent = /^## Unreleased\n\n- /gm; + const unreleasedHeaderWithSubHeader = /^## Unreleased\n\n### /gm; const commentedUnreleasedHeaderWithNoContent = /^\n\n## /gm; From 1ae093804d613eacbf4cf6cdb1165bb37268011d Mon Sep 17 00:00:00 2001 From: Matt Seccafien Date: Mon, 8 Nov 2021 19:32:38 +0100 Subject: [PATCH 3/3] Update tests/changelogs.test.ts Co-authored-by: Josh Larson --- tests/changelogs.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/changelogs.test.ts b/tests/changelogs.test.ts index 94350d04a8..ddb393a9dd 100644 --- a/tests/changelogs.test.ts +++ b/tests/changelogs.test.ts @@ -60,8 +60,8 @@ readChangelogs().forEach(({packageChangelogPath, packageChangelog}) => { /^\n\n## /gm; expect([ - unrelasedHeaderWithContent.test(packageChangelog) || - unrelasedHeaderWithSubHeader.test(packageChangelog), + unreleasedHeaderWithContent.test(packageChangelog) || + unreleasedHeaderWithSubHeader.test(packageChangelog), commentedUnreleasedHeaderWithNoContent.test(packageChangelog), ]).toContain(true); });