Fail pull requests that add too many comments.
Coding agents love to narrate every line. This action counts the code and comment lines a pull request adds with tokei, and fails the check when comments take up more than their share.
# .github/workflows/comment-ratio.yml
name: Comment ratio
on:
pull_request:
permissions:
contents: read
pull-requests: write # to post the report comment
jobs:
comments:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: IgnaceMaes/comment-ratio@v1
with:
max-comment-ratio: 0.05 # at most 1 in 20 added lines may be a commentThat's it. On every pull request the action:
- Downloads a pinned tokei binary (cached on the runner).
- Lists the files the pull request changes and counts code and comment lines in each file before and after the change.
- Sums the per-file increases into "code added" and "comments added".
- Fails the job when
comments added / (code added + comments added)is abovemax-comment-ratio.
Every run writes a job summary and keeps one comment on the pull request up to date:
38.2% of the added lines are comments (149 of 390); the limit is 5%.
Code Comments Added +241 +149 Removed −42 −3 Net +199 +146 Comment ratio 38.2% · Limit 5%
4 files analyzed
File Language Code Δ Comments Δ Ratio src/scheduler/queue.tsTypeScript +153 +131 46.1% ⚠️ src/scheduler/worker.tsTypeScript +88 +15 14.6% ⚠️ src/index.tsTypeScript 0 +3 100% ⚠️ src/legacy/poll.tsTypeScript −42 −3 – The ratio is the share of comment lines among all lines added. Lines are counted per changed file before and after the change; positive deltas are summed. Comparing
4f2c1a9…b81e0d3, counted with tokei 12.1.2.
Files that exceed the limit on their own also get a warning annotation in the Files changed tab.
| Input | Default | Description |
|---|---|---|
max-comment-ratio |
0.05 |
Maximum share of added lines that may be comments, as a fraction between 0 and 1 (5% works too). The job fails when the ratio is above it. |
min-lines-added |
50 |
Skip the check when fewer lines (code plus comments) were added. Keeps a three-line fix with one comment from failing on a technicality. |
include |
all files | Newline-separated globs. When set, only matching paths are analyzed. |
exclude |
none | Newline-separated globs to ignore (generated code, vendored dependencies, fixtures). |
languages |
all languages | Comma- or newline-separated tokei language names to analyze, e.g. TypeScript, Rust. |
exclude-languages |
Markdown, Plain Text, ReStructuredText, AsciiDoc, Org, Djot |
Languages to ignore. tokei counts prose as comments, so docs are excluded by default. Pass none to include everything. |
fail-on-threshold |
true |
Set to false to report without failing the job. |
comment |
true |
Post and keep updating a sticky comment on the pull request. |
github-token |
${{ github.token }} |
Token used to comment and to resolve the merge base through the API. |
tokei-version |
12.1.2 |
tokei release to download, or system to use a tokei already on PATH. See tokei versions. |
base / head |
from the event | Commit-ish pair to compare. Required for events that carry no range (e.g. workflow_dispatch). |
| Output | Example | Description |
|---|---|---|
code-added |
241 |
Code lines added (sum of positive per-file deltas). |
comments-added |
149 |
Comment lines added. |
comment-ratio |
0.3821 |
Share of added lines that are comments, as a fraction with four decimals. |
status |
pass, fail, skip |
Outcome of the check. skip means below min-lines-added or no countable files. |
passed |
true / false |
false only when the limit was exceeded (regardless of fail-on-threshold). |
report |
Markdown | The full report, for use in other steps. |
tokei classifies every line of a file as code, comment or blank, and it understands
block comments, docstrings, nested comments and embedded languages (JavaScript inside HTML,
code fences inside Markdown). Diffs alone can't do that: a + line in the middle of a
/* ... */ block looks like code to git diff.
So instead of reading the diff, the action reads the file contents on both sides of it:
git diff --raw base...head
│
┌──────────────┴──────────────┐
▼ ▼
base blobs ─▶ tokei head blobs ─▶ tokei
{ code, comments } { code, comments }
└──────────────┬──────────────┘
▼
per file: Δcode = head.code − base.code
Δcomments = head.comments − base.comments
totals: code added = Σ max(Δcode, 0)
comments added = Σ max(Δcomments, 0)
ratio = comments added / (code added + comments added)
Only changed files are materialized, so the run takes seconds even on large repositories. Renames are followed, deletions count as removals, and symlinks and submodules are skipped.
The base of the comparison is the merge base of the pull request, so a branch that lags
behind main is not blamed for changes it didn't make. The action fetches the commits it needs on
its own; the default shallow actions/checkout is fine.
The ratio is "comment lines as a share of all lines added", so 0.05 means one comment line
per twenty lines added. Some reference points:
| Ratio | What it looks like |
|---|---|
0.05 |
The default. A short "why" comment where it earns its place; the code speaks. |
0.15 |
A doc comment on most public functions plus the occasional explanation. |
0.40 |
Every other statement narrated. Typical unedited coding-agent output. |
A pull request that only adds comments has a ratio of 1 and fails; one that only removes comments has a ratio of 0 and passes. Both are on purpose.
- uses: IgnaceMaes/comment-ratio@v1
with:
include: |
src/**
packages/*/src/**
exclude: |
**/*.test.ts
**/*.generated.*
**/__snapshots__/**- uses: IgnaceMaes/comment-ratio@v1
with:
languages: TypeScript, TSX, RustUseful while a team is easing into the rule. The comment and job summary still appear, and
outputs.passed still reflects the verdict.
- uses: IgnaceMaes/comment-ratio@v1
with:
fail-on-threshold: false- uses: IgnaceMaes/comment-ratio@v1
id: comments
with:
fail-on-threshold: false
- if: steps.comments.outputs.status == 'fail'
run: echo "::notice::Comment ratio is ${{ steps.comments.outputs.comment-ratio }}."For push events the action compares before...after of the push. For events without a range,
pass one explicitly:
on:
workflow_dispatch:
inputs:
base: { required: true }
head: { required: true }
jobs:
comments:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: IgnaceMaes/comment-ratio@v1
with:
base: ${{ inputs.base }}
head: ${{ inputs.head }}
comment: falsetokei publishes prebuilt binaries for 12.1.2 only, so that is the default. Newer releases
add languages and fixes but must be built from source. To use one, install it in a previous
step and point the action at it:
- uses: taiki-e/install-action@v2
with:
tool: tokei@15.0.0
- uses: IgnaceMaes/comment-ratio@v1
with:
tokei-version: systemThe action works with tokei 12 through 15 (the JSON schema is the same).
Why are Markdown and other docs excluded by default?
tokei counts every line of prose as a comment. A README edit would otherwise push the ratio up
and fail a perfectly fine pull request. Set exclude-languages: none to include them anyway.
Does it work for pull requests from forks?
Yes. The check runs with the read-only token forks receive; only the comment is skipped (with a
warning in the log). Use pull_request_target if you need the comment on fork pull requests,
and understand its security implications first.
Does moving code around count as "added"? A pure rename is detected by git and contributes nothing. Moving a function from one file to another counts the destination file's increase, so moved code is judged on its comments the same as new code.
Which lines are "comments"? Whatever tokei says: line comments, block comments, doc comments and docstrings. Commented-out code is a comment too; this action doesn't try to tell the difference.
Can it enforce a minimum instead? Not today. The action was built to catch over-commented changes, so the limit is a maximum.
Can I run it on macOS or Windows runners? Yes. Prebuilt tokei binaries exist for Linux (x64, arm64), macOS (Apple Silicon runs the x64 build through Rosetta) and Windows (x64).
Bug reports and pull requests are welcome. See CONTRIBUTING.md for the development setup and release process.