A GitHub Action for checking commit message formatting, branch naming, committer name, email, commit signoff, and more.
Important
This v2 release introduces several 🚨breaking changes. Please review the Breaking Changes section carefully before upgrading.
- Removed support for
commit-signoff,merge-base, andimperativeinputs — now configured viacommit-check.tomlorcchk.toml. - Deprecated
.commit-check.ymlin favor ofcommit-check.tomlorcchk.toml. - Changed default values of
author-nameandauthor-emailinputs tofalseto align with the default behavior in commit-check. - Upgraded core dependency
commit-checkto v2.0.0.
- Usage
- Action, pre-commit hook, or GitHub App — which to use?
- Optional Inputs
- GitHub Action Job Summary
- GitHub Pull Request Comments
- Advanced Configuration
- Fork Pull Requests
- Badging Your Repository
- Versioning
Create a new GitHub Actions workflow in your project, e.g. at .github/workflows/commit-check.yml
name: Commit Check
on:
pull_request:
branches: 'main'
jobs:
commit-check:
runs-on: ubuntu-latest
permissions: # use permissions because use of pr-comments
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
# Required. With the default fetch-depth: 1 the clone holds only GitHub's
# merge commit: the PR's own commits cannot be listed, author checks are
# skipped, and the action warns and falls back to checking HEAD alone.
fetch-depth: 0
- uses: commit-check/commit-check-action@v2
with:
message: true
branch: true
author-name: false
author-email: false
job-summary: true
pr-comments: trueWarning
Without fetch-depth: 0 the action still runs, but it cannot see the pull
request's commits. It posts ::warning title=commit-check::Could not list the pull request's commits (is actions/checkout using fetch-depth: 0?); only HEAD was checked and checks only the synthetic merge commit — whose subject
Merge <sha> into <sha> passes the default rules — so a shallow clone makes
every PR look green. Author checks are skipped (⊘) for the same reason. On
pull_request_target, also check out refs/pull/<number>/merge.
Note
This action supports running on Linux, macOS, and Windows (ubuntu-latest, macos-latest, windows-latest).
The action is a composite step and uses what the runner already has:
- Python 3.10 or newer on
PATH(python3, orpythonon Windows). Nosetup-pythonstep is needed on GitHub-hosted runners. Everything the action installs goes under$RUNNER_TEMP, never into your checkout. ghCLI — used to verify the build-provenance attestation of thecommit-checkwheel before installing it. Present on GitHub-hosted images; install it on self-hosted runners or the attestation step fails. Only thecommit-checkwheel is attested; PyGithub and the transitive dependencies are pinned byrequirements.txtbut not verified.- Network access to PyPI and
api.github.com— the pinned wheels are downloaded once per run and the attestation is fetched from GitHub. gitonPATH, and a checkout withfetch-depth: 0(see above).
There is currently no input to skip attestation verification.
All three run the same commit-check engine against the same
commit-check.toml / cchk.toml; they differ in where they run and what they
can see.
| GitHub Action (this repo) | pre-commit hook | Commit Check GitHub App | |
|---|---|---|---|
| Where it runs | In your workflow, on the runner, after the push | On the contributor's machine, at git commit / git push |
Hosted by commit-check; installed on the repository, no workflow file |
| What it checks | Every PR commit's message, plus the PR title, branch and author checks you enable; renders a job summary, annotations, a PR comment and the result output |
Message (commit-msg stage), branch, author; tag, force-push and files (pre-push) — one commit at a time, before it exists |
Every commit of a push or pull request: message, branch, author (the PR title only in squash mode); reported as one Commit Check check run per commit |
| When to pick it | You want enforcement in CI that a contributor cannot skip, per-rule outputs for later steps, or you run on GitHub Enterprise Server / need CCHK_* overrides |
You want the fastest feedback and to stop bad commits before they are pushed; pair it with the Action, since hooks are opt-in | You want zero YAML and no Actions minutes, or feedback on fork pull requests without the Action's read-only-token limits |
Most teams pair the pre-commit hook (fast, local) with the Action (enforced): the hook catches a bad message before it is pushed, and the Action is why CI fails when a contributor did not install the hook.
Apache
discovery-unicamp
Texas Instruments
OpenCADC
Extrawest
Chainlift
Mila
RLinf
Collective
cpp-linter
and many more.
- Description: check git commit message following Conventional Commits.
- Default:
true
- Description: check git branch name following Conventional Branch.
- Default:
true
- Description: check committer author name.
- Default:
false
- Description: check committer author email.
- Default:
false
- Description: report failures (job summary, PR comment, and annotations downgraded to warnings) but always exit 0, so the job never fails.
- Default:
false
- Description: display job summary to the workflow run.
- Default:
true
- Description: post results to the pull request comments.
- Default:
false
Note
pr-comments is disabled by default.
PR comments are skipped for pull requests from forked repositories, whose
GITHUB_TOKEN is read-only. Everything else still works there: the check
status, the annotations and the job summary. See
Fork pull requests.
Dependabot pull requests are not forks, but GitHub gives their
pull_request runs a read-only GITHUB_TOKEN by default. The permissions
key is honoured for them, so the pull-requests: write grant in the
usage example is enough; without it the action logs a
::warning:: on the 403 and leaves the report in the job summary. Note that
Actions secrets are not available in Dependabot-triggered runs. Adding
dependabot[bot] to ignore_authors skips the checks for those PRs
altogether.
Note: write-access to pull-requests requires the pull-requests: write permission.
See usage example.
- Description: check pull request title following Conventional Commits.
- Default:
false
Tip
This is especially useful for teams using Squash & Merge, where the PR title becomes the final commit message in the main branch. When enabled, the action validates the PR title against your Conventional Commits configuration, giving early feedback at PR time rather than after merge.
pr-title works alongside message — you can enable both to validate the PR
title and individual commits, or just one depending on your workflow.
This setting only applies to pull_request and pull_request_target events;
it is silently ignored on push events.
Important
By default, pull_request does not trigger on title changes.
To validate the PR title immediately when updated, add edited to your
workflow's event types:
on:
pull_request:
types: [opened, synchronize, reopened, edited]Without edited, only the initial title (at PR creation) is validated.
The Optional Inputs above cover the most common settings.
For everything else (e.g., subject-capitalized, require-signed-off-by,
ai-attribution, custom allow-commit-types, etc.), you have two approaches:
Set any CCHK_* environment variable in your workflow step — no config file required:
- uses: commit-check/commit-check-action@v2
env:
CCHK_SUBJECT_CAPITALIZED: "true"
CCHK_REQUIRE_SIGNED_OFF_BY: "true"
CCHK_AI_ATTRIBUTION: "forbid"
CCHK_ALLOW_COMMIT_TYPES: "feat,fix,docs,chore"All available environment variables follow the naming convention:
CCHK_ + uppercase option name with underscores instead of hyphens. See the
full mapping
in the commit-check documentation.
Add a commit-check.toml or cchk.toml to the root of your repository.
Refer to the configuration guide
for all available options.
Note
Configuration priority: CLI args > environment variables > config file > defaults. The action itself doesn't set any CLI flags beyond those in Optional Inputs, so env vars and config files are the recommended way to customize.
The config's top-level warn reports a rule without failing the run — see
Warning Job Summary for what that looks like:
warn = ["branch", "CC003"]Structured check results as JSON, available to downstream steps via
fromJSON:
- uses: commit-check/commit-check-action@v2
id: commit-check
with:
dry-run: true # (1)
- name: Inspect results
run: |
echo "Status: ${{ fromJSON(steps.commit-check.outputs.result).status }}"
echo "Scopes: ${{ toJSON(fromJSON(steps.commit-check.outputs.result).scopes) }}"- Without
dry-run, a failing check ends the job before any later step runs. Usedry-run(orcontinue-on-error) when a downstream step is meant to read the result and decide for itself.
The top-level status is one of:
status |
Meaning | Exit code |
|---|---|---|
pass |
every check passed | 0 |
warn |
nothing failed, but a rule listed under the config's warn found something |
0 |
skip |
every check declined to run (for example the author is in ignore_authors) |
0 |
fail |
at least one check failed | 1 (0 with dry-run) |
Only fail is ever non-zero; warn exists so a downstream step can react to a
bent-but-not-broken policy without the run turning red:
- if: fromJSON(steps.commit-check.outputs.result).status == 'warn'
run: echo "passed with warnings"Each entry in scopes has a label (PR title, Commit 2/3, Branch, ...),
a status like the ones above, a sha (the full hash of the commit a
Commit N/M or Commit message scope checked; empty for the others) and the
check outcomes (rule_id, check, status, value, error, suggest,
fix, docs_url) exactly as produced by commit-check --format json, so
downstream jobs can build their own reports or gate on individual rules.
By default, commit-check-action results are shown on the job summary page of the workflow. The report below is reproduced as the action renders it, except that its title is a heading in the real thing — it is bold here so it stays out of this page's table of contents — and the footer names the version that actually ran.
Passing runs stay to one line, with the detail folded away:
Commit Check
✅ All 3 checks passed
Show all 3 checks
Commit message ✔ PR title (feat: add login page) ✔ Commit 1/2 (d87faca) (feat: add login page) Branch ✔ Branch (feature/add-login)commit-check <version> · Rules reference
Failures open with a count, then a table of only the scopes that failed — every rule ID links to its documentation, and every commit to itself — with the full tree still one click away:
Commit Check
❌ 2 of 4 checks failed
Scope Checked value Failed checks Commit 2/2 (5584f46) bad msgCC001 message Branch Feature/Add-LoginCC201 branch Show all 4 checks
Commit message ✔ PR title (feat: add login page) ✔ Commit 1/2 (d87faca) (feat: add login page) ✖ Commit 2/2 (5584f46) (1 failure) CC001 message value: bad msg The commit message should follow Conventional Commits. Suggest: Use <type>(<scope>): <description> Branch ✖ Branch (1 failure) CC201 branch value: Feature/Add-Login The branch should follow Conventional Branch. Suggest: Rename the branch to "feature/Add-Login" (git branch -m feature/Add-Login) Fix: feature/Add-Logincommit-check <version> · Rules reference
A scope is one thing that was checked — a commit message, the branch, the author — not one rule evaluation, so the total matches the ✔/✖ lines you can count and does not grow with the number of rules in your config.
A commit scope names its commit by short hash, and the table row links to it,
so a reviewer can jump from a failed row straight to the offending commit.
Fix: is the corrected text commit-check proposes whenever the correction is
mechanical (a capitalised subject, a dropped WIP marker, a missing sign-off
trailer); when the suggestion is nothing more than "use the fix", only Fix:
is shown.
The step log prints the same tree, then one annotation per finding — shown in the run summary and on the Files changed tab — whose message carries the commit, the checked value, the suggestion and the fix on separate lines:
::error title=CC001 message::Commit 2/2 (5584f46): The commit message should follow Conventional Commits.%0Avalue: bad msg%0ASuggest: Use <type>(<scope>): <description>
::error title=CC201 branch::Branch: The branch should follow Conventional Branch.%0Avalue: Feature/Add-Login%0ASuggest: Rename the branch to "feature/Add-Login" (git branch -m feature/Add-Login)%0AFix: feature/Add-Login
✖ commit-check: 2 of 4 checks failed
The verdict is a plain line rather than another ::error, so the run's error
count equals the number of findings.
Some runs validate nothing at all — most commonly when the commit author is
listed in ignore_authors, which is how Dependabot and other bots are usually
exempted. Those runs report ⊘, never ✔:
⊘ All 3 checks skipped — nothing was validated
Show all 3 checks
Commit message ⊘ PR title (skipped) ⊘ Commit 1/1 (skipped) Branch ⊘ Branch (skipped)commit-check <version> · Rules reference
A skipped scope carries no checked value, because nothing was examined. When
only some scopes skip, the verdict counts them separately —
✅ **3 of 5 checks passed**, 2 skipped — so the headline never claims a pass
that did not happen. Failures still take precedence over skips.
This needs commit-check 2.13.4 or newer, which reports "status": "skip" in
its JSON. Against an older engine every check is pass or fail as before,
and the report is unchanged.
A rule listed under the config's top-level warn
still runs and is reported in full — its own table row, its own entry in the
details block — but it never fails the workflow. It counts toward "passed":
Commit Check
✅ 3 of 4 checks passed, 1 warning
Scope Checked value Warnings Branch jsmith/fix-xCC201 branch Show all 4 checks
Commit message ✔ PR title (feat: add login page) ✔ Commit 1/2 (feat: add login page) Branch ⚠ Branch (1 warning) CC201 branch value: jsmith/fix-x The branch should follow Conventional Branch. Suggest: Use <type>/<description> with allowed typescommit-check <version> · Rules reference
A warned scope is marked ⚠, never ✖, and a real failure elsewhere still
fails the run — the verdict then reads ❌ **N of M checks failed**, K warnings and both tables appear. In the step log, a warning becomes a
::warning annotation rather than ::error, so it never counts toward the
run's error count. The result output reports the run as
"status": "warn", with exit code 0.
This needs commit-check 2.17.0 or newer, which reports "status": "warn" in
its JSON. Against an older engine, or a config with no warn list, no check
can ever be a warning, and the report is unchanged.
With pr-comments: true the same report is posted as a pull request comment.
It is the same Markdown: the job summary and the comment are both rendered by
render_report, so the two surfaces cannot disagree. See
Success Job Summary and
Failure Job Summary above for what it looks like.
What differs is the lifecycle rather than the content:
- The comment is edited in place on later runs rather than added to, so a pull request carries one Commit Check comment however many times CI runs. It stays after the checks pass, showing the ✅ report rather than disappearing.
- Comments are identified by a hidden
<!-- commit-check-action -->marker, so reformatting the visible text does not orphan the previous one. If several marked comments somehow exist, the newest is kept and the rest deleted. - A comment from a version predating the marker is adopted rather than duplicated — but only when a bot posted it, since the older signal was just a title prefix that a person could type by hand.
When a pull request is opened from a forked repository, the GITHUB_TOKEN used by the
pull_request event has read-only permissions by design (GitHub security policy).
This means pr-comments: true cannot write a comment back to the PR.
By default, commit-check-action handles this gracefully:
- PR comment writing is skipped with a
::warning::message in the logs - A notice is added to the Job Summary explaining why and how to fix it
- The commit checks themselves still run normally
For most projects, this is sufficient — a fork contributor already gets the red check, the per-finding annotations on their diff and the full report in the job summary. If you want feedback on the pull request itself, the Commit Check GitHub App posts a check run per commit with no workflow file (free on public repositories), or you can run this action on
pull_request_target. Both are covered in Fork pull requests.
You can add a badge to your repository to show your contributors/users that you use commit-check!
Markdown
[](https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml)
reStructuredText
.. image:: https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml/badge.svg
:target: https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml
:alt: Commit Check
Versioning follows Semantic Versioning.
To provide feedback (requesting a feature or reporting a bug), please post to issues or start a discussion.