Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

commitlint

A single-binary Conventional Commits linter with zero third-party dependencies — small enough to audit in one sitting, built to be trusted in CI.

go install github.com/DivergentCodes/commitlint@latest

Why this exists

The widely used commitlint is excellent but is a Node project: it pulls a dependency tree and needs npm in CI. This is a Go reimplementation of the parts that matter for gating commits:

  • One static binary, zero third-party dependencies — nothing to audit transitively, nothing to pin, trivial to vendor.
  • No runtime — no Node, no npm, no package.json.
  • Sources a message from args, stdin, a file, a git range, or a GitHub PR event, so the same binary covers commit-msg hooks and CI.

Non-goals: it does not validate commit footers or BREAKING CHANGE: trailers (only the ! breaking marker in the subject), and it does not do commitizen-style interactive prompting. It lints; it doesn't author.

Usage

commitlint lint "feat: add gateway"            # one message from args
git log -1 --format=%B | commitlint lint       # stdin
commitlint lint --file .git/COMMIT_EDITMSG     # commit-msg hook
commitlint lint --range origin/main..HEAD      # every commit in a range
commitlint lint --github-pr-title              # PR title from $GITHUB_EVENT_PATH

Exit codes: 0 conforms (or any violation in --mode warn), 1 violations in block mode, 2 usage/config errors.

--range lints each commit in the range and skips merge commits (they're not authored conventional messages and shouldn't fail the range).

Example output

A conforming message:

$ commitlint lint "feat(gateway): add MCP discovery"
✓ 1 message(s) conform to Conventional Commits

A violation (block mode, exit 1):

$ commitlint lint "Add gateway"
✗ message: "Add gateway"
    format: subject must be `type(scope)?: description`, got "Add gateway"
✗ 1 of 1 message(s) do not conform

The same in --mode warn prints the finding but exits 0.

JSON output

--json emits the same run as a structured document, for CI annotations, dashboards, or jq. Exit codes are unchanged.

$ commitlint lint --json "Add gateway"
{
  "conforms": false,
  "checked": 1,
  "failed": 1,
  "blocking": true,
  "results": [
    {
      "label": "message",
      "subject": "Add gateway",
      "problems": [
        {
          "rule": "format",
          "message": "subject must be `type(scope)?: description`, got \"Add gateway\""
        }
      ]
    }
  ]
}

blocking is what drove the exit code: it is false in --mode warn even when failed is non-zero, so a consumer can distinguish "found problems" from "failed the run". problems is always an array, never null.

commitlint lint --json --range origin/main..HEAD | jq -r '.results[].problems[].rule'

Modes

  • --mode block (default): violations fail the run (exit 1).
  • --mode warn: violations are printed but never fail — for advisory linting of individual PR commits when only the squash-merge title is load-bearing.

Rules

  • Subject matches type(scope)?!?: description:
    • type is lowercase and in the allowed set (see config).
    • (scope) is optional; when present it must be non-empty, and it can be required or restricted to an allowlist.
    • ! before the colon marks a breaking change and is accepted.
    • description is non-empty and has no trailing period.
  • Subject length ≤ the max (default 72).
  • Body: if present, the line after the subject must be blank (subject and body separated by an empty line).
  • Git's autogenerated Revert "..." subjects are rejected by default; allow them with --allow-revert-prefix.

Git comments

Commit messages are linted as git would record them: # comment lines and everything below the commit --verbose scissors marker are stripped first. This matters for the commit-msg hook, where git's template puts its comment block directly under the subject — without stripping, a conforming message would fail the "second line must be blank" rule.

Pass --no-strict to lint the raw buffer instead, comments and all.

Configuration

Precedence: flags > environment > config file > defaults.

Flag Env .commitlint.json key Default
--types COMMITLINT_TYPES types conventional set
--scopes COMMITLINT_SCOPES scopes any
--require-scope COMMITLINT_REQUIRE_SCOPE require_scope false
--max-subject-length COMMITLINT_MAX_SUBJECT_LENGTH max_subject_length 72
--allow-revert-prefix COMMITLINT_ALLOW_REVERT_PREFIX allow_revert_prefix false
--no-strict COMMITLINT_NO_STRICT no_strict false
--mode COMMITLINT_MODE mode block
--config .commitlint.json if present

The default type set is the conventional one: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.

Example .commitlint.json:

{
  "types": ["feat", "fix", "docs", "chore", "ci", "refactor", "test", "build"],
  "scopes": ["api", "cli", "gateway"],
  "require_scope": false,
  "max_subject_length": 72,
  "mode": "block"
}

GitHub Actions

Use DivergentCodes/commitlint-action, a thin composite action over this binary.

As a git hook

# .git/hooks/commit-msg  (chmod +x)
#!/bin/sh
exec commitlint lint --file "$1"

Releases

Versions are published as GitHub tagged releases with autogenerated changelogs in the release notes (no committed changelog file). Pin a released version in CI:

go install github.com/DivergentCodes/[email protected]

Each release also carries prebuilt binaries for linux, darwin, and windows (amd64 and arm64, except windows) plus a checksums.txt, for environments without a Go toolchain:

curl -sSfL -O https://github.com/DivergentCodes/commitlint/releases/download/v1.0.1/commitlint_v1.0.1_linux_amd64.tar.gz
tar -xzf commitlint_v1.0.1_linux_amd64.tar.gz
./commitlint_v1.0.1_linux_amd64/commitlint version

Releases are automatic on merge to main. .github/workflows/release.yml derives the next version from the merged commit's conventional type, pushes the tag, runs the tests, cross-compiles, verifies the binary reports the tagged version, and publishes:

Merged commit Bump Example
feat!: / BREAKING CHANGE major v1.2.3v2.0.0
feat: minor v1.2.3v1.3.0
fix: / perf: patch v1.2.3v1.2.4
docs: / ci: / chore: / refactor: none no release

Docs- and CI-only merges therefore do not cut releases. Pushing a tag by hand still works, for re-cuts or out-of-band releases:

git tag v1.0.1 && git push origin v1.0.1

License

MIT

About

Linting commit messages and PR titles for conventional commits

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages