Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }}
run: PYTHONPATH=. python3 scripts/check_pr_title.py

- name: Validate PR body
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: PYTHONPATH=. python3 scripts/check_pr_body.py

commit-messages:
name: commit messages
runs-on: ubuntu-latest
Expand Down
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ No other languages anywhere in the message — not just the subject; body and fo
|---|---|
| subject | English, lowercase start, ≤ 72 chars, no period |
| body | **All English**; when citing a non-English plan / discussion, **translate** it, don't paste |
| punctuation | No full-width punctuation (`:`,`,`,`。`,`「」`,`""` …), no `§`-numbering, no non-English path names; the latin part of a §N.M anchor is fine |
| punctuation | **ASCII-only** — not just no full-width punctuation (`:`,`,`,`。`,`「」`,`""` …) but also no em-dash `—`, curly quotes, or ellipsis `…` (all non-ASCII, all rejected by CI); no `§`-numbering, no non-English path names; the latin part of a §N.M anchor is fine |
| trailer | `Co-authored-by: ...` is ASCII by format |

**Why:** Conventional-Commits tooling (commitlint / semantic-release / changelog generators) parses ASCII grammar and mis-lints on non-English text; cross-language reviewers and a public commit history both need English.
Expand Down Expand Up @@ -285,10 +285,11 @@ Filling rules:
- internal branch names / commit-hash references (no reviewer context).

**Preview-verification (required):**
1. After drafting, **grep for full-width / non-English chars first**:
1. After drafting, **grep for any non-ASCII char first** (the CI lints the whole message as ASCII-only via `commit_lint._is_ascii`, and the squash commit body is this PR description — so it must be ASCII too):
```bash
grep -cP "[\x{4E00}-\x{9FFF}]|[\x{3000}-\x{303F}]|[\x{FF00}-\x{FFEF}]" /tmp/pr_description.md
# must be 0
grep -nP "[^\x00-\x7F]" /tmp/pr_description.md
# must print nothing (0 matches). Catches em-dash/curly-quotes/ellipsis,
# not just CJK + full-width — a CJK-only pattern gives a false pass.
```
2. show the full text for preview;
3. only after the user edits/confirms, run `gh pr create --title "..." --body "$(cat /tmp/pr_description.md)"`;
Expand Down
24 changes: 24 additions & 0 deletions scripts/check_pr_body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

import os
import sys

from scripts.commit_lint import check_pr_body


def main() -> int:
body = os.environ.get("PR_BODY", "")
result = check_pr_body(body)
if result.ok:
return 0

for lineno, line in enumerate(body.splitlines(), start=1):
if any(ord(ch) > 0x7F for ch in line):
print(f"Invalid PR body line {lineno}: {line}", file=sys.stderr)
for error in result.errors:
print(f"- {error}", file=sys.stderr)
return 1


if __name__ == "__main__":
raise SystemExit(main())
12 changes: 12 additions & 0 deletions scripts/commit_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ def check_pr_title(title: str, config: CommitLintConfig | None = None) -> LintRe
return _check_message(title, subject_limit=(config or CommitLintConfig()).pr_title_subject_limit)


def check_pr_body(body: str) -> LintResult:
"""The PR body becomes the squash commit body, so it must be ASCII-only.

Only the ASCII rule applies (the body is free-form Markdown, not a
Conventional-Commits header), so the header/subject checks are skipped.
"""
errors: list[str] = []
if not _is_ascii(body):
errors.append("must be ASCII-only English")
return LintResult(errors)


def _check_message(message: str, *, subject_limit: int) -> LintResult:
errors: list[str] = []
text = message.strip()
Expand Down
20 changes: 20 additions & 0 deletions tests/test_commit_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from scripts.commit_lint import (
CommitLintConfig,
check_commit_message,
check_pr_body,
check_pr_title,
)

Expand Down Expand Up @@ -73,6 +74,25 @@ def test_pr_title_rejects_subject_over_pr_limit() -> None:
assert "subject must be 90 characters or fewer" in result.errors


def test_pr_body_accepts_ascii_markdown() -> None:
result = check_pr_body("## Summary\n\nPlain ASCII body - fine.\n")

assert result.ok


def test_pr_body_rejects_em_dash() -> None:
# The em-dash (U+2014) is the char that slipped past the CJK-only grep and
# failed the post-merge commit lint once the PR body became the squash body.
result = check_pr_body("drops the hint only — empty submits still ignored")

assert not result.ok
assert "must be ASCII-only English" in result.errors


def test_pr_body_allows_empty() -> None:
assert check_pr_body("").ok


def test_commit_msg_hook_runs_commitlint_before_python_checker(monkeypatch, tmp_path: Path) -> None:
message_file = tmp_path / "COMMIT_EDITMSG"
message_file.write_text("docs: add launch notes\n")
Expand Down
Loading