Skip to content
Open
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
15 changes: 0 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,7 @@ on:
branches: [ main ]

jobs:
lint-commits:
# Note: To re-run `lint-commits` after fixing the PR title, close-and-reopen the PR.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22.x
- name: Check PR title
run: |
node "$GITHUB_WORKSPACE/.github/workflows/lintcommit.js"

build:
needs: lint-commits

runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down
178 changes: 0 additions & 178 deletions .github/workflows/lintcommit.js

This file was deleted.

8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ There is a convenience script for the above that you can run from the root of th
ops/ci-checks.sh
```

This script also validates your commit messages against the [Conventional Commits](https://www.conventionalcommits.org/) format.
If you have uncommitted changes, it will skip commit message validation with a warning - commit first, then re-run to validate.

You can also run the commit message check independently:
```
python ops/lintcommit.py
```

## Coding Standards
Consistency is important for maintainability. Please adhere to the house-style of the repo, unless there's a really
good reason to break pattern.
Expand Down
120 changes: 120 additions & 0 deletions ops/__tests__/test_lintcommit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in python the dir name should prob be tests, the dunder means something else in python.


from lintcommit import validate_message, validate_subject
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shouldn't need to do this if you import from ops.lintcommit import



class TestValidateSubject(unittest.TestCase):
# Valid subjects
def test_valid_feat(self) -> None:
self.assertIsNone(validate_subject("feat: add new feature"))

def test_valid_fix(self) -> None:
self.assertIsNone(validate_subject("fix: resolve issue"))

def test_valid_fix_with_scope(self) -> None:
self.assertIsNone(validate_subject("fix(sdk): resolve issue"))

def test_valid_build(self) -> None:
self.assertIsNone(validate_subject("build: update build process"))

def test_valid_chore(self) -> None:
self.assertIsNone(validate_subject("chore: update dependencies"))

def test_valid_ci(self) -> None:
self.assertIsNone(validate_subject("ci: configure CI/CD"))

def test_valid_deps(self) -> None:
self.assertIsNone(validate_subject("deps: bump aws-sdk group with 5 updates"))

def test_valid_docs(self) -> None:
self.assertIsNone(validate_subject("docs: update documentation"))

def test_valid_feat_with_scope(self) -> None:
self.assertIsNone(validate_subject("feat(sdk): add new feature"))

def test_valid_feat_scope_bar(self) -> None:
self.assertIsNone(validate_subject("feat(sdk): bar"))

def test_valid_feat_foo(self) -> None:
self.assertIsNone(validate_subject("feat: foo"))

def test_valid_fix_foo(self) -> None:
self.assertIsNone(validate_subject("fix: foo"))

# Invalid subjects
def test_invalid_type(self) -> None:
self.assertEqual(validate_subject("config: foo"), 'invalid type "config"')

def test_missing_colon(self) -> None:
self.assertEqual(validate_subject("invalid title"), "missing colon (:) char")

def test_period_at_end(self) -> None:
self.assertEqual(
validate_subject("feat: add thing."),
"subject must not end with a period",
)

def test_empty_subject(self) -> None:
self.assertEqual(validate_subject("feat: "), "empty subject")

def test_subject_too_long(self) -> None:
long_subject: str = "feat: " + "a" * 51
result: str | None = validate_subject(long_subject)
self.assertIsNotNone(result)
self.assertIn("invalid subject", result) # type: ignore[arg-type]

def test_type_with_whitespace(self) -> None:
self.assertEqual(
validate_subject("fe at: foo"), 'type contains whitespace: "fe at"'
)

def test_scope_not_closed(self) -> None:
self.assertEqual(
validate_subject("feat(sdk: foo"), "must be formatted like type(scope):"
)

def test_scope_too_long(self) -> None:
long_scope: str = "a" * 31
result: str | None = validate_subject(f"feat({long_scope}): foo")
self.assertIsNotNone(result)
self.assertIn("invalid scope", result) # type: ignore[arg-type]

def test_scope_uppercase(self) -> None:
result: str | None = validate_subject("feat(SDK): foo")
self.assertIsNotNone(result)
self.assertIn("invalid scope", result) # type: ignore[arg-type]


class TestValidateMessage(unittest.TestCase):
def test_valid_subject_only(self) -> None:
error, warnings = validate_message("feat: add thing")
self.assertIsNone(error)
self.assertEqual(warnings, [])

def test_valid_with_body(self) -> None:
error, warnings = validate_message("feat: add thing\n\nThis is the body.")
self.assertIsNone(error)
self.assertEqual(warnings, [])

def test_missing_blank_line(self) -> None:
_, warnings = validate_message("feat: add thing\nNo blank line.")
self.assertIn("missing blank line between subject and body", warnings)

def test_long_body_line(self) -> None:
_, warnings = validate_message("feat: add thing\n\n" + "x" * 80)
self.assertEqual(len(warnings), 1)
self.assertIn("exceeds 72 chars", warnings[0])

def test_empty_message(self) -> None:
error, _ = validate_message("")
self.assertEqual(error, "empty commit message")

def test_invalid_subject_in_message(self) -> None:
error, _ = validate_message("invalid title")
self.assertEqual(error, "missing colon (:) char")
3 changes: 3 additions & 0 deletions ops/ci-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ echo SUCCESS: typings
# static analysis
hatch fmt
echo SUCCESS: linting/fmt

# commit message validation
python ops/lintcommit.py
Loading
Loading