Skip to content

Commit 88b74e0

Browse files
committed
refactor(ci): replace JS PR title linter with Python commit linter
1 parent c16cd40 commit 88b74e0

6 files changed

Lines changed: 338 additions & 193 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,7 @@ on:
1212
branches: [ main ]
1313

1414
jobs:
15-
lint-commits:
16-
# Note: To re-run `lint-commits` after fixing the PR title, close-and-reopen the PR.
17-
runs-on: ubuntu-latest
18-
steps:
19-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20-
- name: Use Node.js
21-
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
22-
with:
23-
node-version: 22.x
24-
- name: Check PR title
25-
run: |
26-
node "$GITHUB_WORKSPACE/.github/workflows/lintcommit.js"
27-
2815
build:
29-
needs: lint-commits
30-
3116
runs-on: ubuntu-latest
3217
strategy:
3318
fail-fast: false

.github/workflows/lintcommit.js

Lines changed: 0 additions & 178 deletions
This file was deleted.

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ There is a convenience script for the above that you can run from the root of th
3030
ops/ci-checks.sh
3131
```
3232

33+
This script also validates your commit messages against the [Conventional Commits](https://www.conventionalcommits.org/) format.
34+
If you have uncommitted changes, it will skip commit message validation with a warning - commit first, then re-run to validate.
35+
36+
You can also run the commit message check independently:
37+
```
38+
python ops/lintcommit.py
39+
```
40+
3341
## Coding Standards
3442
Consistency is important for maintainability. Please adhere to the house-style of the repo, unless there's a really
3543
good reason to break pattern.

ops/__tests__/test_lintcommit.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
import unittest
6+
7+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
8+
9+
from lintcommit import validate_message, validate_subject
10+
11+
12+
class TestValidateSubject(unittest.TestCase):
13+
# Valid subjects
14+
def test_valid_feat(self) -> None:
15+
self.assertIsNone(validate_subject("feat: add new feature"))
16+
17+
def test_valid_fix(self) -> None:
18+
self.assertIsNone(validate_subject("fix: resolve issue"))
19+
20+
def test_valid_fix_with_scope(self) -> None:
21+
self.assertIsNone(validate_subject("fix(sdk): resolve issue"))
22+
23+
def test_valid_build(self) -> None:
24+
self.assertIsNone(validate_subject("build: update build process"))
25+
26+
def test_valid_chore(self) -> None:
27+
self.assertIsNone(validate_subject("chore: update dependencies"))
28+
29+
def test_valid_ci(self) -> None:
30+
self.assertIsNone(validate_subject("ci: configure CI/CD"))
31+
32+
def test_valid_deps(self) -> None:
33+
self.assertIsNone(validate_subject("deps: bump aws-sdk group with 5 updates"))
34+
35+
def test_valid_docs(self) -> None:
36+
self.assertIsNone(validate_subject("docs: update documentation"))
37+
38+
def test_valid_feat_with_scope(self) -> None:
39+
self.assertIsNone(validate_subject("feat(sdk): add new feature"))
40+
41+
def test_valid_feat_scope_bar(self) -> None:
42+
self.assertIsNone(validate_subject("feat(sdk): bar"))
43+
44+
def test_valid_feat_foo(self) -> None:
45+
self.assertIsNone(validate_subject("feat: foo"))
46+
47+
def test_valid_fix_foo(self) -> None:
48+
self.assertIsNone(validate_subject("fix: foo"))
49+
50+
# Invalid subjects
51+
def test_invalid_type(self) -> None:
52+
self.assertEqual(validate_subject("config: foo"), 'invalid type "config"')
53+
54+
def test_missing_colon(self) -> None:
55+
self.assertEqual(validate_subject("invalid title"), "missing colon (:) char")
56+
57+
def test_period_at_end(self) -> None:
58+
self.assertEqual(
59+
validate_subject("feat: add thing."),
60+
"subject must not end with a period",
61+
)
62+
63+
def test_empty_subject(self) -> None:
64+
self.assertEqual(validate_subject("feat: "), "empty subject")
65+
66+
def test_subject_too_long(self) -> None:
67+
long_subject: str = "feat: " + "a" * 51
68+
result: str | None = validate_subject(long_subject)
69+
self.assertIsNotNone(result)
70+
self.assertIn("invalid subject", result) # type: ignore[arg-type]
71+
72+
def test_type_with_whitespace(self) -> None:
73+
self.assertEqual(
74+
validate_subject("fe at: foo"), 'type contains whitespace: "fe at"'
75+
)
76+
77+
def test_scope_not_closed(self) -> None:
78+
self.assertEqual(
79+
validate_subject("feat(sdk: foo"), "must be formatted like type(scope):"
80+
)
81+
82+
def test_scope_too_long(self) -> None:
83+
long_scope: str = "a" * 31
84+
result: str | None = validate_subject(f"feat({long_scope}): foo")
85+
self.assertIsNotNone(result)
86+
self.assertIn("invalid scope", result) # type: ignore[arg-type]
87+
88+
def test_scope_uppercase(self) -> None:
89+
result: str | None = validate_subject("feat(SDK): foo")
90+
self.assertIsNotNone(result)
91+
self.assertIn("invalid scope", result) # type: ignore[arg-type]
92+
93+
94+
class TestValidateMessage(unittest.TestCase):
95+
def test_valid_subject_only(self) -> None:
96+
error, warnings = validate_message("feat: add thing")
97+
self.assertIsNone(error)
98+
self.assertEqual(warnings, [])
99+
100+
def test_valid_with_body(self) -> None:
101+
error, warnings = validate_message("feat: add thing\n\nThis is the body.")
102+
self.assertIsNone(error)
103+
self.assertEqual(warnings, [])
104+
105+
def test_missing_blank_line(self) -> None:
106+
_, warnings = validate_message("feat: add thing\nNo blank line.")
107+
self.assertIn("missing blank line between subject and body", warnings)
108+
109+
def test_long_body_line(self) -> None:
110+
_, warnings = validate_message("feat: add thing\n\n" + "x" * 80)
111+
self.assertEqual(len(warnings), 1)
112+
self.assertIn("exceeds 72 chars", warnings[0])
113+
114+
def test_empty_message(self) -> None:
115+
error, _ = validate_message("")
116+
self.assertEqual(error, "empty commit message")
117+
118+
def test_invalid_subject_in_message(self) -> None:
119+
error, _ = validate_message("invalid title")
120+
self.assertEqual(error, "missing colon (:) char")

ops/ci-checks.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ echo SUCCESS: typings
1313
# static analysis
1414
hatch fmt
1515
echo SUCCESS: linting/fmt
16+
17+
# commit message validation
18+
python ops/lintcommit.py

0 commit comments

Comments
 (0)