diff --git a/lib/changelog.d.ts b/lib/changelog.d.ts new file mode 100644 index 0000000..c10da79 --- /dev/null +++ b/lib/changelog.d.ts @@ -0,0 +1,52 @@ +import { IConventionalCommit } from "@dev-build-deploy/commit-it"; +import { SemVer } from "./semver"; +type semVerBumpTypes = "major" | "minor" | "patch" | "none"; +/** + * Exclude configuration + * @interface IExclude + * @member bump Exclude commits from the changelog based on the bump type + * @member types Exclude commits from the changelog based on the type + * @member scopes Exclude commits from the changelog based on the scope + */ +interface IExclude { + bump?: semVerBumpTypes[]; + types?: string[]; + scopes?: string[]; +} +/** + * Release configuration + * @interface IReleaseConfiguration + * @member changelog Changelog configuration + * @member changelog.exclude Exclude commits from the changelog + * @member changelog.categories Categories to use in the changelog + * @member changelog.categories.title Title of the category + * @member changelog.categories.bump Bump type for the category + * @member changelog.categories.types Types to include in the category + * @member changelog.categories.scopes Scopes to include in the category + * @member changelog.categories.exclude Exclude commits from the category + */ +interface IReleaseConfiguration { + changelog?: { + exclude?: IExclude; + categories?: { + title: string; + bump?: semVerBumpTypes[]; + types?: string[]; + scopes?: string[]; + exclude?: IExclude; + }[]; + }; +} +/** + * Get the configuration for the Changelog + * @returns + */ +export declare function getConfiguration(): IReleaseConfiguration; +/** + * Generate the changelog based on the provided version and commits + * @param version SemVer version of the Release + * @param commits Conventional Commits part of the Changelog + * @returns Changelog in Markdown format + */ +export declare function generateChangelog(version: SemVer, commits: IConventionalCommit[]): string; +export {}; diff --git a/lib/index.js b/lib/index.js index 0f6faba..f8017df 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10572,11 +10572,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.run = exports.determineBumpType = void 0; +exports.run = exports.filterConventionalCommits = exports.determineBumpType = void 0; const core = __importStar(__nccwpck_require__(2186)); const github = __importStar(__nccwpck_require__(5438)); const semver_1 = __nccwpck_require__(8593); const commit_it_1 = __nccwpck_require__(9403); +const changelog_1 = __nccwpck_require__(8598); /** * Retrieve GitHub Releases, sorted by SemVer * @returns List of releases @@ -10610,10 +10611,9 @@ function determineBumpType(commits) { const typeCount = { feat: 0, fix: 0 }; for (const commit of commits) { try { - const conventionalCommit = (0, commit_it_1.getConventionalCommit)(commit); - if (conventionalCommit.breaking) + if (commit.breaking) return "major"; - typeCount[conventionalCommit.type]++; + typeCount[commit.type]++; } catch (error) { if (!(error instanceof commit_it_1.ConventionalCommitError)) @@ -10627,9 +10627,29 @@ function determineBumpType(commits) { return; } exports.determineBumpType = determineBumpType; -function createRelease(version) { +/** + * Filters the provided commits to only include Conventional Commits + * @param commits The commits to filter + * @returns List of Conventional Commits + * @internal + */ +function filterConventionalCommits(commits) { + return commits + .map(c => { + try { + return (0, commit_it_1.getConventionalCommit)(c); + } + catch (error) { + if (!(error instanceof commit_it_1.ConventionalCommitError)) + throw error; + } + }) + .filter(c => c !== undefined); +} +exports.filterConventionalCommits = filterConventionalCommits; +function createRelease(version, commits) { const octokit = github.getOctokit(core.getInput("token")); - return octokit.rest.repos.createRelease(Object.assign({ name: version.toString(), body: "ReleaseMe", draft: false, prerelease: version.preRelease !== undefined, make_latest: version.preRelease === undefined ? "true" : "false", generate_release_notes: false, discussion_category_name: undefined, tag_name: version.toString(), target_commitish: github.context.ref }, github.context.repo)); + return octokit.rest.repos.createRelease(Object.assign({ name: version.toString(), body: (0, changelog_1.generateChangelog)(version, commits), draft: false, prerelease: version.preRelease !== undefined, make_latest: version.preRelease === undefined ? "true" : "false", generate_release_notes: false, discussion_category_name: undefined, tag_name: version.toString(), target_commitish: github.context.ref }, github.context.repo)); } /** * Determines whether the current branch is the default branch. @@ -10663,7 +10683,8 @@ function run() { core.info(`ℹ️ Latest release: ${latestRelease.tag_name}`); const delta = yield getChangesSinceRelease(latestRelease.tag_name); core.info(`ℹ️ Changes since latest release: ${delta.length} commits`); - const bump = determineBumpType(delta); + const commits = filterConventionalCommits(delta); + const bump = determineBumpType(commits); if (bump === undefined) { core.info("⚠️ No bump required, skipping..."); core.endGroup(); @@ -10673,7 +10694,7 @@ function run() { core.startGroup("📝 Creating GitHub Release"); const newVersion = new semver_1.SemVer(latestRelease.tag_name).bump(bump); core.info(`Next version will be: ${newVersion}`); - yield createRelease(newVersion); + yield createRelease(newVersion, commits); core.setOutput("new-version", newVersion); core.endGroup(); } @@ -10685,6 +10706,115 @@ function run() { exports.run = run; +/***/ }), + +/***/ 8598: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +/* +SPDX-FileCopyrightText: 2023 Kevin de Jong + +SPDX-License-Identifier: GPL-3.0-or-later +*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.generateChangelog = exports.getConfiguration = void 0; +const action_1 = __nccwpck_require__(7672); +const thisModule = __importStar(__nccwpck_require__(8598)); +/** + * Get the configuration for the Changelog + * @returns + */ +function getConfiguration() { + // TODO: Read configuration from file (.github/release.y[a]ml) + return { + changelog: { + categories: [ + { title: "💥 Breaking Changes", bump: ["major"] }, + { title: "✨ New Features", bump: ["minor"] }, + { title: "🐛 Bug Fixes", bump: ["patch"] }, + ], + }, + }; +} +exports.getConfiguration = getConfiguration; +/** + * First character to upper case + * @param value + * @returns + */ +function firstCharToUpperCase(value) { + return value.charAt(0).toUpperCase() + value.slice(1); +} +/** + * Generate the changelog based on the provided version and commits + * @param version SemVer version of the Release + * @param commits Conventional Commits part of the Changelog + * @returns Changelog in Markdown format + */ +function generateChangelog(version, commits) { + var _a, _b; + const config = thisModule.getConfiguration(); + const changelog = `## What's Changed\n\n${(_b = (_a = config.changelog) === null || _a === void 0 ? void 0 : _a.categories) === null || _b === void 0 ? void 0 : _b.map(category => { + const categoryCommits = commits.filter(commit => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; + if (category.bump && !category.bump.includes((_a = (0, action_1.determineBumpType)([commit])) !== null && _a !== void 0 ? _a : "none")) + return false; + if (((_b = category.exclude) === null || _b === void 0 ? void 0 : _b.bump) && + category.exclude.bump.includes((_c = (0, action_1.determineBumpType)([commit])) !== null && _c !== void 0 ? _c : "none")) + return false; + if (((_e = (_d = config.changelog) === null || _d === void 0 ? void 0 : _d.exclude) === null || _e === void 0 ? void 0 : _e.bump) && + config.changelog.exclude.bump.includes((_f = (0, action_1.determineBumpType)([commit])) !== null && _f !== void 0 ? _f : "none")) + return false; + if (category.types && !category.types.includes(commit.type)) + return false; + if (((_g = category.exclude) === null || _g === void 0 ? void 0 : _g.types) && category.exclude.types.includes(commit.type)) + return false; + if (((_j = (_h = config.changelog) === null || _h === void 0 ? void 0 : _h.exclude) === null || _j === void 0 ? void 0 : _j.types) && config.changelog.exclude.types.includes(commit.type)) + return false; + if (category.scopes && !category.scopes.includes((_k = commit.scope) !== null && _k !== void 0 ? _k : "none")) + return false; + if (((_l = category.exclude) === null || _l === void 0 ? void 0 : _l.scopes) && category.exclude.scopes.includes((_m = commit.scope) !== null && _m !== void 0 ? _m : "none")) + return false; + if (((_p = (_o = config.changelog) === null || _o === void 0 ? void 0 : _o.exclude) === null || _p === void 0 ? void 0 : _p.scopes) && config.changelog.exclude.scopes.includes((_q = commit.scope) !== null && _q !== void 0 ? _q : "none")) + return false; + return true; + }); + if (categoryCommits.length === 0) + return ""; + return `## ${category.title}\n\n${categoryCommits + .map(commit => `- ${firstCharToUpperCase(commit.description)}`) + .join("\n")}\n\n`; + }).join("\n")}`; + return changelog; +} +exports.generateChangelog = generateChangelog; + + /***/ }), /***/ 8593: diff --git a/src/action.ts b/src/action.ts index a681c62..6843ccf 100644 --- a/src/action.ts +++ b/src/action.ts @@ -7,7 +7,15 @@ SPDX-License-Identifier: GPL-3.0-or-later import * as core from "@actions/core"; import * as github from "@actions/github"; import { SemVer, sortSemVer } from "./semver"; -import { ConventionalCommitError, ICommit, getCommit, getConventionalCommit } from "@dev-build-deploy/commit-it"; +import { + ConventionalCommitError, + ICommit, + IConventionalCommit, + getCommit, + getConventionalCommit, + isConventionalCommit, +} from "@dev-build-deploy/commit-it"; +import { generateChangelog } from "./changelog"; /** * Retrieve GitHub Releases, sorted by SemVer @@ -42,14 +50,13 @@ async function getChangesSinceRelease(tag: string): Promise { * @returns The bump type ("major", "minor", "patch" or undefined) * @internal */ -export function determineBumpType(commits: ICommit[]): "major" | "minor" | "patch" | undefined { +export function determineBumpType(commits: IConventionalCommit[]): "major" | "minor" | "patch" | undefined { const typeCount: { [key: string]: number } = { feat: 0, fix: 0 }; for (const commit of commits) { try { - const conventionalCommit = getConventionalCommit(commit); - if (conventionalCommit.breaking) return "major"; - typeCount[conventionalCommit.type]++; + if (commit.breaking) return "major"; + typeCount[commit.type]++; } catch (error) { if (!(error instanceof ConventionalCommitError)) throw error; } @@ -61,12 +68,30 @@ export function determineBumpType(commits: ICommit[]): "major" | "minor" | "patc return; } -function createRelease(version: SemVer) { +/** + * Filters the provided commits to only include Conventional Commits + * @param commits The commits to filter + * @returns List of Conventional Commits + * @internal + */ +export function filterConventionalCommits(commits: ICommit[]): IConventionalCommit[] { + return commits + .map(c => { + try { + return getConventionalCommit(c); + } catch (error) { + if (!(error instanceof ConventionalCommitError)) throw error; + } + }) + .filter(c => c !== undefined) as IConventionalCommit[]; +} + +function createRelease(version: SemVer, commits: IConventionalCommit[]) { const octokit = github.getOctokit(core.getInput("token")); return octokit.rest.repos.createRelease({ name: version.toString(), - body: "ReleaseMe", + body: generateChangelog(version, commits), draft: false, prerelease: version.preRelease !== undefined, make_latest: version.preRelease === undefined ? "true" : "false", @@ -113,7 +138,9 @@ export async function run(): Promise { const delta = await getChangesSinceRelease(latestRelease.tag_name); core.info(`ℹ️ Changes since latest release: ${delta.length} commits`); - const bump = determineBumpType(delta); + const commits = filterConventionalCommits(delta); + const bump = determineBumpType(commits); + if (bump === undefined) { core.info("⚠️ No bump required, skipping..."); core.endGroup(); @@ -124,10 +151,10 @@ export async function run(): Promise { core.startGroup("📝 Creating GitHub Release"); const newVersion = new SemVer(latestRelease.tag_name).bump(bump); core.info(`Next version will be: ${newVersion}`); - await createRelease(newVersion); + await createRelease(newVersion, commits); core.setOutput("new-version", newVersion); core.endGroup(); } catch (ex) { core.setFailed((ex as Error).message); } -} \ No newline at end of file +} diff --git a/src/changelog.ts b/src/changelog.ts new file mode 100644 index 0000000..46c7764 --- /dev/null +++ b/src/changelog.ts @@ -0,0 +1,124 @@ +/* +SPDX-FileCopyrightText: 2023 Kevin de Jong + +SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import { IConventionalCommit } from "@dev-build-deploy/commit-it"; +import { SemVer } from "./semver"; +import { determineBumpType } from "./action"; +import * as thisModule from "./changelog"; + +type semVerBumpTypes = "major" | "minor" | "patch" | "none"; + +/** + * Exclude configuration + * @interface IExclude + * @member bump Exclude commits from the changelog based on the bump type + * @member types Exclude commits from the changelog based on the type + * @member scopes Exclude commits from the changelog based on the scope + */ +interface IExclude { + bump?: semVerBumpTypes[]; + types?: string[]; + scopes?: string[]; +} + +/** + * Release configuration + * @interface IReleaseConfiguration + * @member changelog Changelog configuration + * @member changelog.exclude Exclude commits from the changelog + * @member changelog.categories Categories to use in the changelog + * @member changelog.categories.title Title of the category + * @member changelog.categories.bump Bump type for the category + * @member changelog.categories.types Types to include in the category + * @member changelog.categories.scopes Scopes to include in the category + * @member changelog.categories.exclude Exclude commits from the category + */ +interface IReleaseConfiguration { + changelog?: { + exclude?: IExclude; + categories?: { + title: string; + bump?: semVerBumpTypes[]; + types?: string[]; + scopes?: string[]; + exclude?: IExclude; + }[]; + }; +} + +/** + * Get the configuration for the Changelog + * @returns + */ +export function getConfiguration(): IReleaseConfiguration { + // TODO: Read configuration from file (.github/release.y[a]ml) + return { + changelog: { + categories: [ + { title: "💥 Breaking Changes", bump: ["major"] }, + { title: "✨ New Features", bump: ["minor"] }, + { title: "🐛 Bug Fixes", bump: ["patch"] }, + ], + }, + }; +} + +/** + * First character to upper case + * @param value + * @returns + */ +function firstCharToUpperCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +/** + * Generate the changelog based on the provided version and commits + * @param version SemVer version of the Release + * @param commits Conventional Commits part of the Changelog + * @returns Changelog in Markdown format + */ +export function generateChangelog(version: SemVer, commits: IConventionalCommit[]) { + const config = thisModule.getConfiguration(); + + const changelog = `## What's Changed\n\n${config.changelog?.categories + ?.map(category => { + const categoryCommits = commits.filter(commit => { + if (category.bump && !category.bump.includes((determineBumpType([commit]) as semVerBumpTypes) ?? "none")) + return false; + if ( + category.exclude?.bump && + category.exclude.bump.includes((determineBumpType([commit]) as semVerBumpTypes) ?? "none") + ) + return false; + if ( + config.changelog?.exclude?.bump && + config.changelog.exclude.bump.includes((determineBumpType([commit]) as semVerBumpTypes) ?? "none") + ) + return false; + + if (category.types && !category.types.includes(commit.type)) return false; + if (category.exclude?.types && category.exclude.types.includes(commit.type)) return false; + if (config.changelog?.exclude?.types && config.changelog.exclude.types.includes(commit.type)) return false; + + if (category.scopes && !category.scopes.includes(commit.scope ?? "none")) return false; + if (category.exclude?.scopes && category.exclude.scopes.includes(commit.scope ?? "none")) return false; + if (config.changelog?.exclude?.scopes && config.changelog.exclude.scopes.includes(commit.scope ?? "none")) + return false; + + return true; + }); + + if (categoryCommits.length === 0) return ""; + + return `## ${category.title}\n\n${categoryCommits + .map(commit => `- ${firstCharToUpperCase(commit.description)}`) + .join("\n")}\n\n`; + }) + .join("\n")}`; + + return changelog; +} diff --git a/test/action.test.ts b/test/action.test.ts index cb8ebc9..c996f82 100644 --- a/test/action.test.ts +++ b/test/action.test.ts @@ -13,7 +13,7 @@ describe("Determine bump type", () => { { hash: "0a0b0c0d", subject: "fix: prevent bug" }, { hash: "0a0b0c0d", subject: "chore!: breaking change" }, ]; - expect(action.determineBumpType(commits)).toBe("major"); + expect(action.determineBumpType(action.filterConventionalCommits(commits))).toBe("major"); }); test("Breaking Change (footer)", () => { @@ -22,7 +22,7 @@ describe("Determine bump type", () => { { hash: "0a0b0c0d", subject: "fix: prevent bug", footer: { "BREAKING CHANGE": "This is a breaking change" } }, { hash: "0a0b0c0d", subject: "chore: breaking change" }, ]; - expect(action.determineBumpType(commits)).toBe("major"); + expect(action.determineBumpType(action.filterConventionalCommits(commits))).toBe("major"); }); test("Minor change", () => { @@ -31,7 +31,7 @@ describe("Determine bump type", () => { { hash: "0a0b0c0d", subject: "fix: prevent bug" }, { hash: "0a0b0c0d", subject: "chore: breaking change" }, ]; - expect(action.determineBumpType(commits)).toBe("minor"); + expect(action.determineBumpType(action.filterConventionalCommits(commits))).toBe("minor"); }); test("Patch change", () => { @@ -40,7 +40,7 @@ describe("Determine bump type", () => { { hash: "0a0b0c0d", subject: "fix: prevent bug" }, { hash: "0a0b0c0d", subject: "chore: breaking change" }, ]; - expect(action.determineBumpType(commits)).toBe("patch"); + expect(action.determineBumpType(action.filterConventionalCommits(commits))).toBe("patch"); }); test("No change", () => { @@ -49,6 +49,6 @@ describe("Determine bump type", () => { { hash: "0a0b0c0d", subject: "perf: introduce async handling of events" }, { hash: "0a0b0c0d", subject: "chore: breaking change" }, ]; - expect(action.determineBumpType(commits)).toBe(undefined); + expect(action.determineBumpType(action.filterConventionalCommits(commits))).toBe(undefined); }); }); diff --git a/test/changelog.test.ts b/test/changelog.test.ts new file mode 100644 index 0000000..ef848be --- /dev/null +++ b/test/changelog.test.ts @@ -0,0 +1,165 @@ +/* +SPDX-FileCopyrightText: 2023 Kevin de Jong + +SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import * as changelog from "../src/changelog"; +import { SemVer } from "../src/semver"; + +describe("Generate Changelog", () => { + const commits = [ + { hash: "0a0b0c0d", subject: "feat: add new feature", type: "feat", description: "add new feature" }, + { + hash: "0a0b0c0d", + subject: "feat!: add new breaking feature", + type: "feat", + description: "add new breaking feature", + breaking: true, + }, + { hash: "0a0b0c0d", subject: "fix: address a bug", type: "fix", description: "address a bug" }, + { + hash: "0a0b0c0d", + subject: "fix(core): address failure in core logic", + type: "fix", + description: "address failure in core logic", + scope: "core", + }, + { + hash: "0a0b0c0d", + subject: "chore!: break the api", + type: "chore", + description: "break the api", + breaking: true, + }, + { hash: "0a0b0c0d", subject: "docs: improve documentation", type: "docs", description: "improve documentation" }, + { hash: "0a0b0c0d", subject: "perf: improve performance", type: "perf", description: "improve performance" }, + ]; + + test("Breaking changes only", () => { + jest.spyOn(changelog, "getConfiguration").mockImplementation(() => { + return { + changelog: { + categories: [{ title: "💥 Breaking Changes", bump: ["major"] }], + }, + }; + }); + + const result = changelog.generateChangelog(new SemVer("0.0.1"), commits); + + expect(result.includes("Add new feature")).toBe(false); + expect(result.includes("Add new breaking feature")).toBe(true); + expect(result.includes("Address a bug")).toBe(false); + expect(result.includes("Address failure in core logic")).toBe(false); + expect(result.includes("Break the api")).toBe(true); + expect(result.includes("Improve documentation")).toBe(false); + expect(result.includes("Improve performance")).toBe(false); + }); + + test("Breaking changes of type feat only", () => { + jest.spyOn(changelog, "getConfiguration").mockImplementation(() => { + return { + changelog: { + categories: [{ title: "💥 Breaking Changes", bump: ["major"], types: ["feat"] }], + }, + }; + }); + + const result = changelog.generateChangelog(new SemVer("0.0.1"), commits); + + expect(result.includes("Add new feature")).toBe(false); + expect(result.includes("Add new breaking feature")).toBe(true); + expect(result.includes("Address a bug")).toBe(false); + expect(result.includes("Address failure in core logic")).toBe(false); + expect(result.includes("Break the api")).toBe(false); + expect(result.includes("Improve documentation")).toBe(false); + expect(result.includes("Improve performance")).toBe(false); + }); + + test("Bug fix with scope", () => { + jest.spyOn(changelog, "getConfiguration").mockImplementation(() => { + return { + changelog: { + categories: [{ title: "🐛 Bug Fixes", bump: ["patch"], scopes: ["core"] }], + }, + }; + }); + + const result = changelog.generateChangelog(new SemVer("0.0.1"), commits); + + expect(result.includes("Add new feature")).toBe(false); + expect(result.includes("Add new breaking feature")).toBe(false); + expect(result.includes("Address a bug")).toBe(false); + expect(result.includes("Address failure in core logic")).toBe(true); + expect(result.includes("Break the api")).toBe(false); + expect(result.includes("Improve documentation")).toBe(false); + expect(result.includes("Improve performance")).toBe(false); + }); + + test("Bug fix without scope", () => { + jest.spyOn(changelog, "getConfiguration").mockImplementation(() => { + return { + changelog: { + categories: [{ title: "🐛 Bug Fixes", bump: ["patch"], exclude: { scopes: ["core"] } }], + }, + }; + }); + + const result = changelog.generateChangelog(new SemVer("0.0.1"), commits); + + expect(result.includes("Add new feature")).toBe(false); + expect(result.includes("Add new breaking feature")).toBe(false); + expect(result.includes("Address a bug")).toBe(true); + expect(result.includes("Address failure in core logic")).toBe(false); + expect(result.includes("Break the api")).toBe(false); + expect(result.includes("Improve documentation")).toBe(false); + expect(result.includes("Improve performance")).toBe(false); + }); + + test("Documentation and bug fixes", () => { + jest.spyOn(changelog, "getConfiguration").mockImplementation(() => { + return { + changelog: { + categories: [ + { title: "🐛 Bug Fixes", bump: ["patch"], exclude: { scopes: ["core"] } }, + { title: "📚 Documentation", types: ["docs"] }, + ], + }, + }; + }); + + const result = changelog.generateChangelog(new SemVer("0.0.1"), commits); + + expect(result.includes("Add new feature")).toBe(false); + expect(result.includes("Add new breaking feature")).toBe(false); + expect(result.includes("Address a bug")).toBe(true); + expect(result.includes("Address failure in core logic")).toBe(false); + expect(result.includes("Break the api")).toBe(false); + expect(result.includes("Improve documentation")).toBe(true); + expect(result.includes("Improve performance")).toBe(false); + }); + + test("Exclude all bug fixes", () => { + jest.spyOn(changelog, "getConfiguration").mockImplementation(() => { + return { + changelog: { + exclude: { types: ["fix"] }, + categories: [ + { title: "🐛 Bug Fixes", bump: ["patch"], exclude: { scopes: ["core"] } }, + { title: "📚 Documentation", types: ["docs"] }, + ], + }, + }; + }); + + const result = changelog.generateChangelog(new SemVer("0.0.1"), commits); + + expect(result.includes("Add new feature")).toBe(false); + expect(result.includes("Add new breaking feature")).toBe(false); + expect(result.includes("Address a bug")).toBe(false); + expect(result.includes("Address failure in core logic")).toBe(false); + expect(result.includes("Break the api")).toBe(false); + expect(result.includes("Improve documentation")).toBe(true); + expect(result.includes("Improve performance")).toBe(false); + }); +});