From d20e8a4e2c55100cfa1bd6e0f8ed84a1c738c335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 5 Apr 2025 20:36:06 -0700 Subject: [PATCH] ci(linter): replace commitlint with a Python script --- .github/workflows/linters.yml | 13 ++- .github/workflows/scripts/commit-checker.py | 93 +++++++++++++++++++++ 2 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/scripts/commit-checker.py diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 1568b86f..c9e2ffce 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -39,19 +39,16 @@ jobs: run: gofmt -d -e . commitlint: + if: github.event_name == 'pull_request' name: Commit Linter runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - node-version: "lts/*" - - name: Install commitlint - run: | - npm install --save-dev @commitlint/config-conventional @commitlint/cli - echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js + python-version: '3.13' - name: Validate PR commits - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + run: python3 .github/workflows/scripts/commit-checker.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/scripts/commit-checker.py b/.github/workflows/scripts/commit-checker.py new file mode 100644 index 00000000..de209548 --- /dev/null +++ b/.github/workflows/scripts/commit-checker.py @@ -0,0 +1,93 @@ +import subprocess +import re +import sys +import argparse +from typing import Match + +# Conventional commit pattern +CONVENTIONAL_COMMIT_PATTERN: str = r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}" + + +def get_commit_message(commit_hash: str) -> str: + """Get the commit message for a given commit hash.""" + try: + result: subprocess.CompletedProcess = subprocess.run( + ["git", "show", "-s", "--format=%B", commit_hash], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error retrieving commit message: {e}") + sys.exit(1) + + +def check_commit_message( + message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN +) -> bool: + """Check if commit message follows conventional commit format.""" + first_line: str = message.split("\n")[0] + match: Match[str] | None = re.match(pattern, first_line) + return bool(match) + + +def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]: + """Check all commits in a range for compliance.""" + try: + result: subprocess.CompletedProcess = subprocess.run( + ["git", "log", "--format=%H", f"{base_ref}..{head_ref}"], + capture_output=True, + text=True, + check=True, + ) + commit_hashes: list[str] = result.stdout.strip().split("\n") + + # Filter out empty lines + commit_hashes = [hash for hash in commit_hashes if hash] + + non_compliant: list[dict[str, str]] = [] + for commit_hash in commit_hashes: + message: str = get_commit_message(commit_hash) + if not check_commit_message(message): + non_compliant.append( + {"hash": commit_hash, "message": message.split("\n")[0]} + ) + + return non_compliant + except subprocess.CalledProcessError as e: + print(f"Error checking commit range: {e}") + sys.exit(1) + + +def main() -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="Check conventional commit compliance" + ) + parser.add_argument( + "--base", required=True, help="Base ref (starting commit, exclusive)" + ) + parser.add_argument( + "--head", required=True, help="Head ref (ending commit, inclusive)" + ) + args: argparse.Namespace = parser.parse_args() + + non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head) + + if non_compliant: + print("The following commits do not follow the conventional commit format:") + for commit in non_compliant: + print(f"- {commit['hash'][:8]}: {commit['message']}") + print("\nPlease ensure your commit messages follow the format:") + print("type(scope): subject") + print( + "\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test" + ) + sys.exit(1) + else: + print("All commits follow the conventional commit format!") + sys.exit(0) + + +if __name__ == "__main__": + main()