From 6e7c5ec2bd1f9b466b02d6225d2136820982b5bb Mon Sep 17 00:00:00 2001 From: KT Date: Tue, 7 Jul 2026 20:47:33 +0800 Subject: [PATCH 1/2] docs: widen pr-description non-ascii check to full ascii range The section 3.7 preview grep only matched CJK plus full-width punctuation, so em-dash / curly quotes / ellipsis passed as clean while the CI (commit_lint._is_ascii) rejects any non-ascii. Since the squash commit body is the PR description, such a char fails the post-merge commit lint on main (as seen on #97). Switch to grep -nP "[^\x00-\x7F]", and note in the 3.1.1 punctuation row that the rule is ascii-only, not merely no-full-width. Co-authored-by: Claude (claude-opus-4-8) --- AGENTS.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2107550..33bf353 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. @@ -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)"`; From e2b4efcb44635b2da5a7d0a4a2173e94a480e831 Mon Sep 17 00:00:00 2001 From: KT Date: Tue, 7 Jul 2026 20:55:30 +0800 Subject: [PATCH 2/2] ci: enforce ascii-only pr body before merge The PR body becomes the squash commit body, but nothing checked it before merge: the pr-title job only linted the title, and commit-lint only saw branch commits. A non-ascii PR body therefore only failed the post-merge commit lint on main, too late to fix cleanly (as on #97). Add check_pr_body (ascii-only, reusing commit_lint._is_ascii) and a Validate PR body step in the pull_request job, so a non-ascii body fails the PR check before merge and points at the offending line. Co-authored-by: Claude (claude-opus-4-8) --- .github/workflows/commits.yml | 5 +++++ scripts/check_pr_body.py | 24 ++++++++++++++++++++++++ scripts/commit_lint.py | 12 ++++++++++++ tests/test_commit_lint.py | 20 ++++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 scripts/check_pr_body.py diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml index b41ebb1..2ff353c 100644 --- a/.github/workflows/commits.yml +++ b/.github/workflows/commits.yml @@ -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 diff --git a/scripts/check_pr_body.py b/scripts/check_pr_body.py new file mode 100644 index 0000000..0bd0841 --- /dev/null +++ b/scripts/check_pr_body.py @@ -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()) diff --git a/scripts/commit_lint.py b/scripts/commit_lint.py index 9c890cc..e240e84 100644 --- a/scripts/commit_lint.py +++ b/scripts/commit_lint.py @@ -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() diff --git a/tests/test_commit_lint.py b/tests/test_commit_lint.py index d709691..595a228 100644 --- a/tests/test_commit_lint.py +++ b/tests/test_commit_lint.py @@ -7,6 +7,7 @@ from scripts.commit_lint import ( CommitLintConfig, check_commit_message, + check_pr_body, check_pr_title, ) @@ -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")