-
Notifications
You must be signed in to change notification settings - Fork 42
137 lines (117 loc) · 4.91 KB
/
Copy pathcommit.yml
File metadata and controls
137 lines (117 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# Copyright (C) 2020-2021 TotalCross Global Mobile Platform Ltda
# Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda
#
# SPDX-License-Identifier: LGPL-2.1-only
name: Commit Message Check
on:
push:
branches:
- master
pull_request:
jobs:
check-commit-message:
name: Validate commit messages
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate commit messages in range
env:
EVENT_NAME: ${{ github.event_name }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
python3 <<'PY'
import os
import re
import subprocess
import sys
TITLE_FORMAT = re.compile(
r'^(fix|feat|refactor|perf|style|test|docs|build|ci|chore|revert)'
r'(!\([A-Za-z0-9_-]+(,[a-z0-9_-]+)?\)|'
r'\([A-Za-z0-9_-]+(,[a-z0-9_-]+)?\)!?): [a-z0-9 ].*$'
)
TITLE_CAPITALIZATION = re.compile(r'^[^A-Z]')
TITLE_WORD_COUNT = re.compile(r'^[^ ]+([ \t]+[^ ]+){2,}$', re.S)
TITLE_LENGTH = re.compile(r'^([^\n]{1,80})(\n.*)?$', re.S)
BLANK_LINE = re.compile(r'^[^\n]+(\n\n.+)?$', re.S)
MARKDOWN_LINK = re.compile(r'\[([^\]]*)\]\([^)]+\)')
def run(*args):
return subprocess.check_output(args, text=True).strip()
def commit_range():
event_name = os.environ["EVENT_NAME"]
if event_name == "pull_request":
head = os.environ["PR_HEAD_SHA"]
base = run("git", "merge-base", os.environ["PR_BASE_SHA"], head)
return base, head
before = os.environ["PUSH_BEFORE"]
after = os.environ["PUSH_AFTER"]
if before == "0000000000000000000000000000000000000000":
return "", after
return before, after
def list_commits(base, head):
if base:
output = run("git", "rev-list", f"{base}..{head}")
else:
output = run("git", "rev-list", head)
return [line for line in output.splitlines() if line]
def read_message(commit):
return subprocess.check_output(
["git", "show", "-s", "--format=%B", commit],
text=True,
).rstrip("\n")
def body_line_length(line):
return len(MARKDOWN_LINK.sub(r"\1", line))
def validate(commit, message):
title = message.split("\n", 1)[0]
failures = []
if not TITLE_FORMAT.match(title):
failures.append("Invalid commit title format.")
if not TITLE_CAPITALIZATION.match(title):
failures.append("Commit title must not start with an uppercase letter.")
if not TITLE_WORD_COUNT.match(title):
failures.append("Commit title must contain at least 3 words.")
if not TITLE_LENGTH.match(message):
failures.append("Commit title must be 80 characters or less.")
if title.endswith("."):
failures.append("Commit title must not end with a period.")
if not BLANK_LINE.match(message):
failures.append(
"If a commit body is present, it must be separated from the title by a blank line."
)
if any(body_line_length(line) > 80 for line in message.split("\n")[1:]):
failures.append(
"Each line in the commit body must be 80 characters or less, "
"excluding Markdown link URLs."
)
return title, failures
base, head = commit_range()
commits = list_commits(base, head)
range_label = f"{base}..{head}" if base else head
print(f"Validating commit messages in range: {range_label}")
print(f"Event: {os.environ['EVENT_NAME']}")
if not commits:
print("No commits to validate.")
sys.exit(0)
invalid = []
for commit in reversed(commits):
message = read_message(commit)
title, failures = validate(commit, message)
if failures:
invalid.append((commit, title, failures))
if invalid:
print(f"Commit message validation failed for {len(invalid)} commit(s).\n")
print("Commits outside the expected format:")
for commit, title, failures in invalid:
print(f"- {commit} {title}")
for failure in failures:
print(f" - {failure}")
print(f"::error title={commit[:10]} {title}::{failure}")
print()
sys.exit(1)
print(f"Validated {len(commits)} commit message(s).")
PY