Skip to content

Commit 7ee1a1f

Browse files
nibzardclaude
andcommitted
fix(ci): accept pull request bodies with CRLF line endings
The contribution validator matched `^## <heading>\n`, so a body sent with CRLF line endings reported every required section as missing. Correct submissions were rejected based on the client that composed them. Match `\r?\n` instead, and add tests for the validator to the lint job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4fe0850 commit 7ee1a1f

3 files changed

Lines changed: 71 additions & 1 deletion

File tree

.github/workflows/lint.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
uses: actions/checkout@v4
2222
with:
2323
fetch-depth: 0
24+
- name: Test the validator
25+
run: python3 -m unittest discover -s scripts -p "test_*.py"
2426
- name: Validate contribution policy
2527
run: python3 scripts/validate_contribution.py
2628

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ABOUTME: Tests for the contribution policy validator used by the lint workflow.
2+
# ABOUTME: Run with `python3 -m unittest discover -s scripts -p "test_*.py"`.
3+
4+
from __future__ import annotations
5+
6+
import unittest
7+
8+
from validate_contribution import REQUIRED_BODY_SECTIONS, extract_body_sections, normalize_section
9+
10+
11+
BODY = """## Summary
12+
13+
Adds an item.
14+
15+
## Section
16+
17+
Dev Tools
18+
19+
## Why this belongs
20+
21+
It controls a browser.
22+
23+
## Primary documented web-agent use case
24+
25+
https://example.com/docs
26+
27+
## Public reference
28+
29+
https://example.com
30+
31+
## Affiliation disclosure
32+
33+
I maintain the project.
34+
35+
## Checklist
36+
37+
- [x] This PR adds exactly one item.
38+
"""
39+
40+
41+
class ExtractBodySectionsTest(unittest.TestCase):
42+
def test_reads_every_required_section(self) -> None:
43+
sections = extract_body_sections(BODY)
44+
for heading in REQUIRED_BODY_SECTIONS:
45+
self.assertTrue(sections.get(heading), f'missing "{heading}"')
46+
47+
def test_reads_every_required_section_with_carriage_returns(self) -> None:
48+
# Some clients send the pull request body with CRLF line endings.
49+
sections = extract_body_sections(BODY.replace("\n", "\r\n"))
50+
for heading in REQUIRED_BODY_SECTIONS:
51+
self.assertTrue(sections.get(heading), f'missing "{heading}"')
52+
53+
def test_section_name_has_no_carriage_return(self) -> None:
54+
sections = extract_body_sections(BODY.replace("\n", "\r\n"))
55+
self.assertEqual(normalize_section(sections["Section"]), "Dev Tools")
56+
57+
def test_absent_section_is_not_reported(self) -> None:
58+
sections = extract_body_sections(BODY.replace("## Public reference", "## Other"))
59+
self.assertIsNone(sections.get("Public reference"))
60+
61+
def test_empty_section_is_not_reported(self) -> None:
62+
sections = extract_body_sections(BODY.replace("https://example.com\n\n## Affiliation", "\n## Affiliation"))
63+
self.assertFalse(sections.get("Public reference"))
64+
65+
66+
if __name__ == "__main__":
67+
unittest.main()

scripts/validate_contribution.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ def added_item_lines(base_sha: str) -> list[tuple[str, str]]:
7575
def extract_body_sections(body: str) -> dict[str, str]:
7676
sections: dict[str, str] = {}
7777
for heading in REQUIRED_BODY_SECTIONS:
78+
# Some clients send the pull request body with CRLF line endings.
7879
pattern = re.compile(
79-
rf"(?ms)^## {re.escape(heading)}\n+(.*?)(?=^## |\Z)"
80+
rf"(?ms)^## {re.escape(heading)}\r?\n+(.*?)(?=^## |\Z)"
8081
)
8182
match = pattern.search(body)
8283
if match:

0 commit comments

Comments
 (0)