Skip to content

Commit e3ac54a

Browse files
Merge branch 'master' into pr/PilotGaea-support
2 parents ca86b36 + 8aaa8be commit e3ac54a

2 files changed

Lines changed: 186 additions & 83 deletions

File tree

.github/workflows/test_branch_conventions.yml

Lines changed: 5 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -44,91 +44,13 @@ jobs:
4444
run: |
4545
Tools/scripts/test_new_boards.py --master-branch "upstream/${{ github.event.pull_request.base.ref }}"
4646
47-
- name: Check for merge commits in PR branch
47+
- name: Install markdownlint-cli2
4848
shell: bash
4949
run: |
50-
# Find merge-base between base branch and (rebased) PR branch
51-
echo "Merge target is origin/${{ github.event.pull_request.base.ref }}"
52-
echo "github.event.pull_request.base.ref=${{ github.event.pull_request.base.ref }} "
53-
54-
# Look for merge commits in PR branch only
55-
MERGE_COMMITS=$(git log upstream/${{ github.event.pull_request.base.ref }}..HEAD --merges --oneline)
56-
echo "Merge commits:"
57-
echo "$MERGE_COMMITS"
58-
59-
if [[ -n "$MERGE_COMMITS" ]]; then
60-
echo "❌ Merge commits detected in the PR branch (not allowed):"
61-
echo "Please see https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html"
62-
exit 1
63-
else
64-
echo "✅ No merge commits found in the PR branch."
65-
fi
66-
67-
# Look for fixup! commits in PR branch only
68-
COMMITS=$(git log upstream/${{ github.event.pull_request.base.ref }}..HEAD --oneline)
69-
echo "Commits:"
70-
echo "$COMMITS"
71-
72-
if [[ "$COMMITS" == *"fixup!"* ]]; then
73-
echo "❌ fixup commits detected in the PR branch (not allowed):"
74-
echo "$COMMITS"
75-
echo "Please see https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html"
76-
exit 1
77-
else
78-
echo "✅ No fixup commits found in the PR branch."
79-
fi
80-
81-
# require a well-formed subsystem tag before ":" in each commit message:
82-
while IFS= read x; do
83-
# strip leading hash from oneline format
84-
subject="${x#* }"
85-
# extract everything before the first colon
86-
prefix="${subject%%:*}"
87-
if [[ "$prefix" == "$subject" ]]; then
88-
echo "❌ Commit message ($x) missing subsystem tag on front. Re-word your commit to reflect what subsystem it changes. E.g. 'AP_Compass: Added driver for XYZZY' (https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html)"
89-
exit 1
90-
fi
91-
# spaces and quotes are allowed to support Revert commits e.g. 'Revert "AP_Periph: ...'
92-
if ! [[ "$prefix" =~ ^[A-Za-z0-9._/\ \"-]+$ ]]; then
93-
echo "❌ Commit message ($x) has malformed subsystem tag '$prefix'. The subsystem prefix must contain only letters, digits, dots, underscores, slashes, hyphens, spaces, and quotes. E.g. 'AP_Compass: Added driver for XYZZY' (https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html)"
94-
exit 1
95-
fi
96-
done <<< $COMMITS
97-
echo "✅ Commit messages have well-formed subsystem tags."
98-
99-
- name: Lint changed markdown files
100-
shell: bash
101-
run: |
102-
# Get list of added or modified markdown files in the PR
103-
CHANGED_MD=$(git diff --name-only --diff-filter=AM upstream/${{ github.event.pull_request.base.ref }}...HEAD -- '*.md')
104-
105-
if [[ -z "$CHANGED_MD" ]]; then
106-
echo "✅ No markdown files changed."
107-
exit 0
108-
fi
109-
110-
echo "Changed markdown files:"
111-
echo "$CHANGED_MD"
112-
113-
# Install markdownlint-cli2 (older version compatible with container's Node.js)
11450
apt-get update && apt-get install -y npm
11551
npm install -g markdownlint-cli2@0.4.0
11652
117-
# Lint the changed files
118-
if markdownlint-cli2 $CHANGED_MD; then
119-
echo "✅ Markdown files pass linting."
120-
else
121-
echo "❌ Markdown linting errors found."
122-
exit 1
123-
fi
124-
125-
# check that no commit subject line exceeds 160 characters:
126-
while IFS= read -r line; do
127-
if [[ ${#line} -gt 160 ]]; then
128-
echo "❌ Commit subject line exceeds 160 characters:"
129-
echo "$line"
130-
echo "Please keep the commit subject line to 160 characters or fewer."
131-
exit 1
132-
fi
133-
done <<< "$COMMITS"
134-
echo "✅ All commit subject lines are within 160 characters."
53+
- name: Check branch conventions
54+
shell: bash
55+
run: |
56+
Tools/scripts/check_branch_conventions.py --base-branch "upstream/${{ github.event.pull_request.base.ref }}"
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
'''
6+
Check PR branch commit conventions and markdown linting.
7+
8+
Validates:
9+
- no merge commits
10+
- no fixup! commits
11+
- commit messages have a well-formed subsystem prefix before ':'
12+
- commit subject lines are <= 160 characters
13+
- changed markdown files pass markdownlint-cli2
14+
15+
AP_FLAKE8_CLEAN
16+
'''
17+
18+
import argparse
19+
import os
20+
import re
21+
import subprocess
22+
import sys
23+
24+
import build_script_base
25+
26+
DOCS_URL = "https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html"
27+
MAX_SUBJECT_LEN = 160
28+
# spaces and quotes allowed to support Revert commits e.g. 'Revert "AP_Periph: ...'
29+
PREFIX_RE = re.compile(r'^[-A-Za-z0-9._/" ]+$')
30+
31+
# Enable colour when attached to a terminal or running under GitHub Actions
32+
_colour = sys.stdout.isatty() or os.environ.get('GITHUB_ACTIONS') == 'true'
33+
_GREEN = '\033[32m' if _colour else ''
34+
_RED = '\033[31m' if _colour else ''
35+
_YELLOW = '\033[33m' if _colour else ''
36+
_RESET = '\033[0m' if _colour else ''
37+
38+
PASS = f"{_GREEN}{_RESET}"
39+
FAIL = f"{_RED}{_RESET}"
40+
SKIP = f"{_YELLOW}~{_RESET}"
41+
42+
43+
class CheckBranchConventions(build_script_base.BuildScriptBase):
44+
45+
DEFAULT_UPSTREAM = "origin/master"
46+
47+
def __init__(self, base_branch: str | None = None) -> None:
48+
super().__init__()
49+
self.base_branch = base_branch
50+
51+
def progress_prefix(self) -> str:
52+
return "CBC"
53+
54+
def run_git(self, args, show_output=True, source_dir=None):
55+
cmd_list = ["git"] + list(args)
56+
return self.run_program(
57+
"SCB-GIT", cmd_list,
58+
show_output=show_output, show_command=False, cwd=source_dir,
59+
)
60+
61+
def check_merge_commits(self) -> bool:
62+
merge_commits = self.run_git(
63+
["log", f"{self.base_branch}..HEAD", "--merges", "--oneline"],
64+
show_output=False,
65+
).strip()
66+
if merge_commits:
67+
print(f"{FAIL} Merge commits are not allowed:")
68+
for line in merge_commits.splitlines():
69+
print(f" {line}")
70+
print(f" See: {DOCS_URL}")
71+
return False
72+
print(f"{PASS} No merge commits.")
73+
return True
74+
75+
def check_fixup_commits(self, commits: str) -> bool:
76+
bad = [line for line in commits.splitlines() if "fixup!" in line]
77+
if bad:
78+
print(f"{FAIL} fixup! commits are not allowed:")
79+
for line in bad:
80+
print(f" {line}")
81+
print(f" See: {DOCS_URL}")
82+
return False
83+
print(f"{PASS} No fixup! commits.")
84+
return True
85+
86+
def check_commit_messages(self, commits: str) -> bool:
87+
ok = True
88+
for line in commits.splitlines():
89+
if not line.strip():
90+
continue
91+
# strip leading hash from --oneline format
92+
subject = line.split(" ", 1)[1] if " " in line else line
93+
if ":" not in subject:
94+
print(f"{FAIL} Missing subsystem prefix: {line}")
95+
print(f" Reword to e.g. 'AP_Compass: {subject}'")
96+
print(f" See: {DOCS_URL}")
97+
ok = False
98+
continue
99+
prefix = subject.split(":")[0]
100+
if not PREFIX_RE.match(prefix):
101+
print(f"{FAIL} Malformed subsystem prefix '{prefix}': {line}")
102+
print(" Prefix must contain only letters, digits, '.', '_', '/', '-', spaces, quotes.")
103+
print(f" See: {DOCS_URL}")
104+
ok = False
105+
if ok:
106+
print(f"{PASS} All commit messages have well-formed subsystem tags.")
107+
return ok
108+
109+
def check_commit_lengths(self, commits: str) -> bool:
110+
ok = True
111+
for line in commits.splitlines():
112+
if not line.strip():
113+
continue
114+
if len(line) > MAX_SUBJECT_LEN:
115+
print(f"{FAIL} Subject too long ({len(line)} chars, limit {MAX_SUBJECT_LEN}): {line}")
116+
ok = False
117+
if ok:
118+
print(f"{PASS} All commit subject lines within {MAX_SUBJECT_LEN} characters.")
119+
return ok
120+
121+
def check_markdown(self) -> bool:
122+
changed_md = self.run_git(
123+
["diff", "--name-only", "--diff-filter=AM",
124+
f"{self.base_branch}...HEAD", "--", "*.md"],
125+
show_output=False,
126+
).strip()
127+
if not changed_md:
128+
print(f"{PASS} No markdown files changed.")
129+
return True
130+
131+
try:
132+
result = subprocess.run(["markdownlint-cli2"] + changed_md.splitlines())
133+
except FileNotFoundError:
134+
print(f"{SKIP} markdownlint-cli2 not installed.")
135+
return True
136+
if result.returncode != 0:
137+
print(f"{FAIL} Markdown linting errors found (see above).")
138+
return False
139+
140+
print(f"{PASS} Markdown files pass linting.")
141+
return True
142+
143+
def run(self) -> None:
144+
if self.base_branch is None:
145+
current = self.find_current_git_branch_or_sha1()
146+
self.base_branch = self.find_git_branch_merge_base(current, self.DEFAULT_UPSTREAM)
147+
self.progress(f"Using merge base with {self.DEFAULT_UPSTREAM}: {self.base_branch}")
148+
149+
commits = self.run_git(
150+
["log", f"{self.base_branch}..HEAD", "--oneline"],
151+
show_output=False,
152+
).strip()
153+
154+
n = len(commits.splitlines()) if commits else 0
155+
print(f"\nChecking {n} commit(s) since {self.base_branch}...\n")
156+
157+
results = [
158+
self.check_merge_commits(),
159+
self.check_fixup_commits(commits),
160+
self.check_commit_messages(commits),
161+
self.check_commit_lengths(commits),
162+
self.check_markdown(),
163+
]
164+
165+
failures = results.count(False)
166+
print(f"\n{'All checks passed.' if not failures else f'{failures} check(s) failed.'}")
167+
sys.exit(0 if all(results) else 1)
168+
169+
170+
if __name__ == "__main__":
171+
parser = argparse.ArgumentParser(
172+
description="Check PR branch commit conventions and markdown linting",
173+
)
174+
parser.add_argument(
175+
"--base-branch",
176+
default=None,
177+
help="Upstream base branch or commit to compare against "
178+
"(default: merge base of HEAD with origin/master)",
179+
)
180+
args = parser.parse_args()
181+
CheckBranchConventions(args.base_branch).run()

0 commit comments

Comments
 (0)