Skip to content

Commit

Permalink
Merge pull request #112 from JoshuaKGoldberg/more-commit-meanings
Browse files Browse the repository at this point in the history
feat: add understanding of more commit types
  • Loading branch information
JoshuaKGoldberg authored Oct 2, 2023
2 parents d746446 + 1d1acc2 commit c7e619b
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 56 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@

## Usage

This CLI script determines whether a semantic release should occur for a package based on Git history.
Specifically, it returns truthy only if a commit whose type _isn't_ `chore` or `docs` has come since the most recent release commit.
This function determines whether a semantic release should occur for a package based on Git history.
It returns true if a "meaningful" commit has come since the most recent release commit.

```shell
if npx should-semantic-release ; then npx release-it ; fi
Expand All @@ -53,6 +53,16 @@ Checking commit: chore: release v1.27.31
This is a release commit. Returning false.
```

### Commit Purposes

Based on a commit's conventional commit message type:

1. If the type is `feat` `fix`, or `perf`, it's considered "meaningful"
2. If the type is `docs`, `refactor`, `style`, or `test`, it's ignored
3. If the message looks like `v1.2.3`, `chore: release 1.2.3`, or similar, it's considered a "release"

See [`getCommitMeaning`](./src/getCommitMeaning.ts) for the exact logic used.

### Node API

Alternately, you can call this import asynchronous `shouldSemanticRelease` function into Node scripts:
Expand Down
46 changes: 46 additions & 0 deletions src/getCommitMeaning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";

import { getCommitMeaning } from "./getCommitMeaning.js";

describe("getCommitMeaning", () => {
it.each([
["chore", { type: undefined }],
["chore: deps", { type: "chore" }],
["chore(deps): bump", { type: "chore" }],
["docs", { type: undefined }],
["docs: bump", { type: "docs" }],
["docs: message", { type: "docs" }],
["feat", { type: undefined }],
["feat: bump", "meaningful"],
["feat: bump version to 1.2.3", "meaningful"],
["feat: message", "meaningful"],
["fix", { type: undefined }],
["fix: bump", "meaningful"],
["fix: bump version to 1.2.3", "meaningful"],
["fix: message", "meaningful"],
["perf", { type: undefined }],
["perf: bump", "meaningful"],
["perf: bump version to 1.2.3", "meaningful"],
["perf: message", "meaningful"],
["style", { type: undefined }],
["style: bump", { type: "style" }],
["style: bump version to 1.2.3", { type: "style" }],
["style: message", { type: "style" }],
["0.0.0", "release"],
["v0.0.0", "release"],
["1.2.3", "release"],
["v1.2.3", "release"],
["1.23.456", "release"],
["v1.23.456", "release"],
["12.345.6789", "release"],
["v12.345.6789", "release"],
["chore: release", "release"],
["chore: release 1.2.3", "release"],
["chore: release v1.2.3", "release"],
["chore(deps): release", "release"],
["chore(deps): release 1.2.3", "release"],
["chore(deps): release v1.2.3", "release"],
])("returns %j for %s", (input, expected) => {
expect(getCommitMeaning(input)).toEqual(expected);
});
});
28 changes: 28 additions & 0 deletions src/getCommitMeaning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import conventionalCommitsParser from "conventional-commits-parser";

const alwaysMeaningfulTypes = new Set(["feat", "fix", "perf"]);

const alwaysIgnoredTypes = new Set(["docs", "refactor", "style", "test"]);

const releaseCommitTester =
/^(?:chore(?:\(.*\))?:?)?\s*release|v?\d+\.\d+\.\d+/;

export function getCommitMeaning(message: string) {
// Some types are always meaningful or ignored, regardless of potentially release-like messages
const { type } = conventionalCommitsParser.sync(message);
if (type) {
if (alwaysMeaningfulTypes.has(type)) {
return "meaningful";
}
if (alwaysIgnoredTypes.has(type)) {
return { type };
}
}

// If we've hit a release commit, we know we don't need to release
if (releaseCommitTester.test(message)) {
return "release";
}

return { type: type ?? undefined };
}
32 changes: 0 additions & 32 deletions src/isReleaseCommit.test.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/isReleaseCommit.ts

This file was deleted.

30 changes: 13 additions & 17 deletions src/shouldSemanticRelease.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import conventionalCommitsParser from "conventional-commits-parser";

import { isReleaseCommit } from "./isReleaseCommit.js";
import { getCommitMeaning } from "./getCommitMeaning.js";
import { ShouldSemanticReleaseOptions } from "./types.js";
import { execOrThrow } from "./utils.js";

const ignoredTypes = new Set(["chore", "docs"]);

export async function shouldSemanticRelease({
verbose,
}: ShouldSemanticReleaseOptions) {
Expand All @@ -21,20 +17,20 @@ export async function shouldSemanticRelease({

for (const message of history) {
log(`Checking commit: ${message}`);
// If we've hit a release commit, we know we don't need to release
if (isReleaseCommit(message)) {
log(`Found a release commit. Returning false.`);
return false;
}
const meaning = getCommitMeaning(message);

// Otherwise, we should release if a non-ignored commit type is found
const { type } = conventionalCommitsParser.sync(message);
if (type && !ignoredTypes.has(type)) {
log(`Found a meaningful commit. Returning true.`);
return true;
}
switch (meaning) {
case "release":
log(`Found a release commit. Returning false.`);
return false;

case "meaningful":
log(`Found a meaningful commit. Returning true.`);
return true;

log(`Found type ${type}. Continuing.`);
default:
log(`Found type ${meaning.type}. Continuing.`);
}
}

// If we've seen every commit in the history and none match, don't release
Expand Down

0 comments on commit c7e619b

Please sign in to comment.