Skip to content

Commit 2a990fd

Browse files
cosmo0920edsiper
authored andcommitted
github: scripts: commit_linter: Extend a capability of prefix inferences
Signed-off-by: Hiroshi Hatake <hiroshi@chronosphere.io>
1 parent c88c545 commit 2a990fd

1 file changed

Lines changed: 68 additions & 29 deletions

File tree

.github/scripts/commit_prefix_check.py

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,64 @@
2525

2626

2727
# ------------------------------------------------
28-
# Identify expected prefix dynamically from file paths
28+
# Identify expected prefixes dynamically from file paths
2929
# ------------------------------------------------
3030
def infer_prefix_from_paths(paths):
31+
"""
32+
Returns:
33+
- prefixes: a set of allowed prefixes (including build:)
34+
- build_optional: True when commit subject does not need to be build:
35+
(i.e., when any real component — lib/tests/plugins/src — is touched)
36+
"""
3137
prefixes = set()
38+
component_prefixes = set()
39+
build_seen = False
40+
41+
for raw in paths:
42+
# Normalize path separators (Windows compatibility)
43+
p = raw.replace(os.sep, "/")
44+
basename = os.path.basename(p)
45+
46+
# ----- Any CMakeLists.txt → build: candidate -----
47+
if basename == "CMakeLists.txt":
48+
build_seen = True
49+
50+
# ----- lib/ → lib: -----
51+
if p.startswith("lib/"):
52+
component_prefixes.add("lib:")
53+
54+
# ----- tests/ → tests: -----
55+
if p.startswith("tests/"):
56+
component_prefixes.add("tests:")
3257

33-
for p in paths:
58+
# ----- plugins/<name>/ → <name>: -----
3459
if p.startswith("plugins/"):
3560
parts = p.split("/")
36-
prefix = parts[1]
37-
prefixes.add(f"{prefix}:")
38-
continue
61+
if len(parts) > 1:
62+
component_prefixes.add(f"{parts[1]}:")
3963

64+
# ----- src/ → flb_xxx.* → xxx: OR src/<dir>/ → <dir>: -----
4065
if p.startswith("src/"):
4166
filename = os.path.basename(p)
4267
if filename.startswith("flb_"):
4368
core = filename[4:].split(".")[0]
44-
prefixes.add(f"{core}:")
45-
continue
69+
component_prefixes.add(f"{core}:")
70+
else:
71+
parts = p.split("/")
72+
if len(parts) > 1:
73+
component_prefixes.add(f"{parts[1]}:")
4674

47-
directory = p.split("/")[1]
48-
prefixes.add(f"{directory}:")
49-
continue
75+
# prefixes = component prefixes + build: if needed
76+
prefixes |= component_prefixes
77+
if build_seen:
78+
prefixes.add("build:")
5079

51-
return prefixes
80+
# build_optional:
81+
# True if ANY real component (lib/tests/plugins/src) was modified.
82+
# False only when modifying build system files alone.
83+
build_optional = len(component_prefixes) > 0
84+
85+
return prefixes, build_optional
5286

5387

5488
# ------------------------------------------------
@@ -84,27 +118,27 @@ def detect_bad_squash(body):
84118

85119

86120
# ------------------------------------------------
87-
# Validate commit per test expectations
121+
# Validate commit based on expected behavior and test rules
88122
# ------------------------------------------------
89123
def validate_commit(commit):
90124
msg = commit.message.strip()
91125
first_line, *rest = msg.split("\n")
92126
body = "\n".join(rest)
93127

94-
# Subject must have prefix
128+
# Subject must start with a prefix
95129
subject_prefix_match = PREFIX_RE.match(first_line)
96130
if not subject_prefix_match:
97131
return False, f"Missing prefix in commit subject: '{first_line}'"
98132

99133
subject_prefix = subject_prefix_match.group()
100134

101-
# detect_bad_squash must run but
102-
# validate_commit IGNORE bad-squash reason if it was "multiple sign-offs"
135+
# Run squash detection (but ignore multi-signoff errors)
103136
bad_squash, reason = detect_bad_squash(body)
104137

105138
# If bad squash was caused by prefix lines in body → FAIL
106139
# If list of prefix lines in body → FAIL
107140
if bad_squash:
141+
# Prefix-like lines are always fatal
108142
if "subject-like prefix" in reason:
109143
return False, f"Bad squash detected: {reason}"
110144

@@ -113,7 +147,7 @@ def validate_commit(commit):
113147
# validate_commit ignores multi signoff warnings.
114148
pass
115149

116-
# Subject length
150+
# Subject length check
117151
if len(first_line) > 80:
118152
return False, f"Commit subject too long (>80 chars): '{first_line}'"
119153

@@ -122,30 +156,35 @@ def validate_commit(commit):
122156
if signoff_count == 0:
123157
return False, "Missing Signed-off-by line"
124158

125-
# Determine expected prefix
159+
# Determine expected prefixes + build option flag
126160
files = commit.stats.files.keys()
127-
expected = infer_prefix_from_paths(files)
161+
expected, build_optional = infer_prefix_from_paths(files)
128162

129-
# Docs/CI changes
163+
# When no prefix can be inferred (docs/tools), allow anything
130164
if len(expected) == 0:
131165
return True, ""
132166

133-
# *** TEST EXPECTATION ***
134-
# For mixed components, DO NOT return custom message.
135-
# Instead: same error shape as wrong-prefix case.
136-
if len(expected) > 1:
137-
# Always fail when multiple components are touched (even if prefix matches one)
167+
expected_lower = {p.lower() for p in expected}
168+
subj_lower = subject_prefix.lower()
169+
170+
# Subject prefix must be one of the expected ones
171+
if subj_lower not in expected_lower:
172+
expected_list = sorted(expected)
173+
expected_str = ", ".join(expected_list)
138174
return False, (
139175
f"Subject prefix '{subject_prefix}' does not match files changed.\n"
140-
f"Expected one of: {', '.join(sorted(expected))}"
176+
f"Expected one of: {expected_str}"
141177
)
142178

143-
# Normal prefix mismatch (case-insensitive comparison)
144-
only_expected = next(iter(expected))
145-
if subject_prefix.lower() != only_expected.lower():
179+
180+
return False, f"Commit subject too long (>80 chars): '{first_line}'"
181+
182+
# If build is NOT optional and build: exists among expected,
183+
# then subject MUST be build:
184+
if not build_optional and "build:" in expected_lower and subj_lower != "build:":
146185
return False, (
147186
f"Subject prefix '{subject_prefix}' does not match files changed.\n"
148-
f"Expected one of: {only_expected}"
187+
f"Expected one of: build:"
149188
)
150189

151190
return True, ""

0 commit comments

Comments
 (0)