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
25 changes: 25 additions & 0 deletions .github/workflows/check-pr-basic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: 🧱 Check PR Basics
on:
pull_request:
types: [opened, edited, synchronize, reopened]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
check-pr-basics:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Skip check for dependabot
if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' }}
run: echo "Skipping PR basics check for dependabot." && exit 0
- uses: thehanimo/pr-title-checker@v1.4.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
configuration_path: pull_request_title.json
41 changes: 41 additions & 0 deletions .github/workflows/test-pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
name: 🧪 Test Validation

on:
pull_request:
types: [opened, edited, synchronize, reopened]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test-pr-title-regex:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Run PR title validation tests
run: |
echo "Running PR title validation tests..."
python3 test_pr_title.py -v

- name: Test results summary
run: |
echo "✅ All PR title validation tests passed!"
echo ""
echo "The regex correctly validates PR titles with these rules:"
echo "1. ✅ Type must be lowercase (feat, fix, docs, etc.)"
echo "2. ✅ Single space after colon (not double spaces)"
echo "3. ✅ First letter after colon must be lowercase"
echo "4. ✅ Title length must be 8-70 characters total"
echo "5. ✅ Cannot end with period"
echo "6. ✅ Cannot contain double spaces anywhere"
echo "7. ✅ Cannot contain double dashes (--)"
4 changes: 2 additions & 2 deletions pull_request_title.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"color": "#B60205"
},
"CHECKS": {
"regexp": "^(\\[.+?\\] )?(feat|hotfix|fix|docs|style|refactor|perf|test|chore|build|ci|revert): \\S.{10,70}$",
"regexp": "^(\\[[^\\]]+\\] )?(feat|hotfix|fix|docs|style|refactor|perf|test|chore|build|ci|revert): [a-z](?!.* )(?!.*--).{7,68}[^. ]$",
"ignoreLabels": []
},
"MESSAGES": {
"success": "Pull request title is OK",
"failure": "Pull request title is invalid. Regex: /^(\\[.+?\\] )?(feat|hotfix|fix|docs|style|refactor|perf|test|chore|build|ci|revert): \\S.{10,70}$/",
"failure": "Pull request title is invalid. Requirements: 1) Use lowercase type 2) Single space after colon 3) Lowercase first letter 4) 8-70 chars total 5) No ending period 6) No double spaces 7) No double dashes. Regex: /^(\\[[^\\]]+\\] )?(feat|hotfix|fix|docs|style|refactor|perf|test|chore|build|ci|revert): [a-z](?!.* )(?!.*--).{7,68}[^. ]$/",
"notice": ""
}
}
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# No external dependencies required for the PR title validation test
# The test script only uses Python standard library modules:
# - re (regular expressions)
# - json (JSON parsing)
# - sys (system functions)
# - pathlib (path handling)
101 changes: 101 additions & 0 deletions test_pr_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Test script for PR title validation regex using unittest framework.
Tests all the good and bad cases according to the requirements.
"""

import re
import json
import unittest
from pathlib import Path


class PRTitleValidationTest(unittest.TestCase):
"""Test cases for PR title validation regex"""

@classmethod
def setUpClass(cls):
"""Load the regex pattern from configuration file"""
config_path = Path(__file__).parent / "pull_request_title.json"
with open(config_path, 'r') as f:
config = json.load(f)
cls.regex_pattern = config['CHECKS']['regexp']
cls.pattern = re.compile(cls.regex_pattern)

# Good cases - should match the regex
cls.good_cases = [
"feat: add new authentication system",
"fix: resolve memory leak in parser",
"docs: update installation guide",
"refactor: simplify user management logic",
"test: add unit tests for api endpoints",
"chore: update dependencies to latest versions",
"[URGENT] fix: critical security vulnerability patch",
"[FEATURE] feat: implement advanced search functionality",
"perf: optimize database query performance significantly",
"style: improve code formatting and readability standards"
]

# Bad cases - should NOT match the regex
cls.bad_cases = [
# Double spaces after colon
"feat: enhance",
"refactor: director don't output result",

# Uppercase type or first letter
"Feat: blabla",
"Fix: something wrong",
"DOCS: update readme",

# Too short titles
"fix: bug",
"feat: add",
"docs: fix",

# Too long titles
"feat: this is a really really really really really really really really really really really really long message, do something something something something something something",

# Ending with period
"fix: bug something.",
"feat: add new feature.",
"docs: update documentation.",

# Double dashes
"fix: lirian--fix-some-thing",
"feat: implement--new--feature",
"refactor: clean--up--old--code",

# Additional edge cases
"invalidtype: some message",
": missing type",
"feat:", # missing message
"feat:add", # missing space
]

def test_good_cases_should_pass(self):
"""Test that all good cases match the regex"""
for title in self.good_cases:
with self.subTest(title=title):
self.assertIsNotNone(
self.pattern.match(title),
f"Good case should pass but failed: '{title}'"
)

def test_bad_cases_should_fail(self):
"""Test that all bad cases are rejected by the regex"""
for title in self.bad_cases:
with self.subTest(title=title):
self.assertIsNone(
self.pattern.match(title),
f"Bad case should fail but passed: '{title}'"
)

def test_regex_pattern_loaded(self):
"""Test that regex pattern is loaded correctly"""
self.assertIsNotNone(self.regex_pattern)
self.assertIsInstance(self.regex_pattern, str)
self.assertGreater(len(self.regex_pattern), 0)


if __name__ == "__main__":
unittest.main()