diff --git a/action.yml b/action.yml index ea23a46..9b1ec16 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,10 @@ inputs: description: "A string which indicates the flags used by the `minor_pattern` regular expression. Supported flags: idgs" required: false default: "" + ignore_commits_pattern: + description: "A pattern to match commits that should be ignored when calculating the version. Commits matching this pattern will not trigger any version bump. Wrap with '/' to use a regular expression." + required: false + default: "" version_format: description: "Pattern to use when formatting output version" required: true diff --git a/src/ActionConfig.ts b/src/ActionConfig.ts index 3c476b7..bc8ff62 100644 --- a/src/ActionConfig.ts +++ b/src/ActionConfig.ts @@ -32,6 +32,8 @@ export class ActionConfig { public enablePrereleaseMode: boolean = false; /** If bump_each_commit is also set to true, setting this value will cause the version to increment only if the pattern specified is matched. */ public bumpEachCommitPatchPattern: string = ""; + /** A pattern to match commits that should be ignored when calculating the version. Commits matching this pattern will not trigger any version bump. Wrap with '/' to use a regular expression. */ + public ignoreCommitsPattern: string = ""; /** If enabled, diagnostic information will be added to the action output. */ public debug: boolean = false; /** Diagnostics to replay */ diff --git a/src/cli.ts b/src/cli.ts index bc6b2ca..ac7fc5e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -36,6 +36,11 @@ program "/feat:/", ) .option("--minor-flags ", "Flags for minor pattern regex", "") + .option( + "-i, --ignore-commits-pattern ", + "Pattern to match commits that should be ignored when calculating version", + "", + ) .option( "--version-format ", "Version format template", @@ -81,6 +86,7 @@ program config.majorFlags = options.majorFlags || ""; config.minorPattern = options.minorPattern; config.minorFlags = options.minorFlags || ""; + config.ignoreCommitsPattern = options.ignoreCommitsPattern || ""; config.bumpEachCommit = options.bumpEachCommit; config.bumpEachCommitPatchPattern = options.bumpEachCommitPatchPattern || ""; diff --git a/src/main.test.ts b/src/main.test.ts index 66023e0..efb4c79 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -1431,5 +1431,140 @@ testInterfaces.forEach((testInterface) => { }, timeout, ); + + test( + "Empty major pattern disables major version bumps", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + majorPattern: "", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("fix!: breaking fix"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("1.0.1+0"); + }, + timeout, + ); + + test( + "Empty major pattern still allows normal behavior to verify test", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("fix!: breaking fix"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("2.0.0+0"); + }, + timeout, + ); + + test( + "Empty minor pattern disables minor version bumps", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + minorPattern: "", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("feat: Feature Commit"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("1.0.1+0"); + }, + timeout, + ); + + test( + "Empty major and minor patterns result in only patch bumps", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + majorPattern: "", + minorPattern: "", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("feat!: Breaking Change"); + repo.makeCommit("feat: New Feature"); + repo.makeCommit("Regular Commit"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("1.0.1+2"); + }, + timeout, + ); + + test( + "Commits matching ignore pattern are excluded from version calculation", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + ignoreCommitsPattern: "/^chore:|^docs:|^style:/", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("chore: update dependencies"); + repo.makeCommit("docs: update readme"); + repo.makeCommit("style: fix formatting"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("1.0.0+0"); + expect(result.changed).toBe(false); + }, + timeout, + ); + + test( + "Non-ignored commits still trigger version bumps", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + ignoreCommitsPattern: "/^chore:|^docs:/", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("chore: update dependencies"); + repo.makeCommit("fix: bug fix"); + repo.makeCommit("docs: update readme"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("1.0.1+0"); + }, + timeout, + ); + + test( + "Ignored commits do not affect major/minor detection", + async () => { + const repo = createTestRepo(testRunner, { + tagPrefix: "", + ignoreCommitsPattern: "/^chore:/", + }); + + repo.makeCommit("Initial Commit"); + repo.exec("git tag 1.0.0"); + repo.makeCommit("chore: update dependencies"); + repo.makeCommit("feat: new feature"); + repo.makeCommit("chore: cleanup"); + const result = await repo.runAction(); + + expect(result.formattedVersion).toBe("1.1.0+0"); + }, + timeout, + ); }); }); diff --git a/src/main.ts b/src/main.ts index f554738..80f1450 100644 --- a/src/main.ts +++ b/src/main.ts @@ -84,6 +84,7 @@ export async function run() { userFormatType: core.getInput("user_format_type"), enablePrereleaseMode: toBool(core.getInput("enable_prerelease_mode")), bumpEachCommitPatchPattern: core.getInput("bump_each_commit_patch_pattern"), + ignoreCommitsPattern: core.getInput("ignore_commits_pattern"), debug: toBool(core.getInput("debug")), replay: "", }; diff --git a/src/providers/BumpAlwaysVersionClassifier.ts b/src/providers/BumpAlwaysVersionClassifier.ts index b0d6e06..089b057 100644 --- a/src/providers/BumpAlwaysVersionClassifier.ts +++ b/src/providers/BumpAlwaysVersionClassifier.ts @@ -38,15 +38,17 @@ export class BumpAlwaysVersionClassifier extends DefaultVersionClassifier { ); } + const filteredCommitSet = this.filterIgnoredCommits(commitSet); + let { major, minor, patch } = lastRelease; let type = VersionType.None; let increment = 0; - if (commitSet.commits.length === 0) { + if (filteredCommitSet.commits.length === 0) { return new VersionClassification(type, 0, false, major, minor, patch); } - for (let commit of commitSet.commits.reverse()) { + for (let commit of filteredCommitSet.commits.reverse()) { if (this.majorPattern(commit)) { type = VersionType.Major; } else if (this.minorPattern(commit)) { diff --git a/src/providers/DefaultVersionClassifier.ts b/src/providers/DefaultVersionClassifier.ts index 3d1194b..dfdbc8b 100644 --- a/src/providers/DefaultVersionClassifier.ts +++ b/src/providers/DefaultVersionClassifier.ts @@ -9,6 +9,7 @@ import { VersionType } from "./VersionType"; export class DefaultVersionClassifier implements VersionClassifier { protected majorPattern: (commit: CommitInfo) => boolean; protected minorPattern: (commit: CommitInfo) => boolean; + protected ignorePattern: ((commit: CommitInfo) => boolean) | null; protected enablePrereleaseMode: boolean; constructor(config: ActionConfig) { @@ -23,14 +24,31 @@ export class DefaultVersionClassifier implements VersionClassifier { config.minorFlags, searchBody, ); + this.ignorePattern = config.ignoreCommitsPattern + ? this.parsePattern(config.ignoreCommitsPattern, "", searchBody) + : null; this.enablePrereleaseMode = config.enablePrereleaseMode; } + protected filterIgnoredCommits(commitSet: CommitInfoSet): CommitInfoSet { + if (!this.ignorePattern) { + return commitSet; + } + const filteredCommits = commitSet.commits.filter( + (commit) => !this.ignorePattern!(commit), + ); + const changed = filteredCommits.length > 0 ? commitSet.changed : false; + return new CommitInfoSet(changed, filteredCommits); + } + protected parsePattern( pattern: string, flags: string, searchBody: boolean, ): (pattern: CommitInfo) => boolean { + if (pattern === "") { + return (_commit: CommitInfo) => false; + } if (/^\/.+\/[i]*$/.test(pattern)) { const regexEnd = pattern.lastIndexOf("/"); const parsedFlags = pattern.slice(pattern.lastIndexOf("/") + 1); @@ -149,7 +167,9 @@ export class DefaultVersionClassifier implements VersionClassifier { lastRelease: ReleaseInformation, commitSet: CommitInfoSet, ): Promise { - const { type, increment, changed } = this.resolveCommitType(commitSet); + const filteredCommitSet = this.filterIgnoredCommits(commitSet); + const { type, increment, changed } = + this.resolveCommitType(filteredCommitSet); const { major, minor, patch } = this.getNextVersion(lastRelease, type);