Skip to content
Open
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
35 changes: 35 additions & 0 deletions .github/workflows/test_branch_conventions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Test for Malformed Commits in PR Branch

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

concurrency:
group: ci-${{github.workflow}}-${{ github.ref }}
cancel-in-progress: true

jobs:
check-merge-commits:
if: github.repository == 'ArduPilot/ardupilot_wiki'
runs-on: ubuntu-slim

steps:
- name: Checkout PR branch
uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
submodules: 'false'
fetch-depth: 0

- name: Fetch base branch
run: |
git remote -v
git remote add upstream ${{ github.event.pull_request.base.repo.clone_url }}
git fetch upstream ${{ github.event.pull_request.base.ref }}
git show upstream/${{ github.event.pull_request.base.ref }}

- name: Check branch conventions
shell: bash
run: |
scripts/check_branch_conventions.py --base-branch "upstream/${{ github.event.pull_request.base.ref }}"
152 changes: 152 additions & 0 deletions scripts/check_branch_conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env python3

'''
Check PR branch commit conventions

Note: this is a sideways copy of ArduPilot's
Tools/scripts/check_branch_conventions.py which has been modified to
remove checks that don't belong. And, by the time you read this,
probably adjusted to add wiki-specific checks!

Validates:
- no merge commits
- no fixup! commits
- commit subject lines are <= 160 characters

AP_FLAKE8_CLEAN

'''

from __future__ import annotations

import argparse
import os
import sys

import build_script_base

DOCS_URL = "https://ardupilot.org/dev/docs/submitting-patches-back-to-master.html"
MAX_SUBJECT_LEN = 160

# Enable colour when attached to a terminal or running under GitHub Actions
_colour = sys.stdout.isatty() or os.environ.get('GITHUB_ACTIONS') == 'true'
_GREEN = '\033[32m' if _colour else ''
_RED = '\033[31m' if _colour else ''
_YELLOW = '\033[33m' if _colour else ''
_RESET = '\033[0m' if _colour else ''

PASS = f"{_GREEN}✓{_RESET}"
FAIL = f"{_RED}✗{_RESET}"
SKIP = f"{_YELLOW}~{_RESET}"


class CheckBranchConventions(build_script_base.BuildScriptBase):

DEFAULT_UPSTREAM = "origin/master"

def __init__(self, base_branch: str | None = None) -> None:
super().__init__()
self.base_branch = base_branch

def progress_prefix(self) -> str:
return "CBC"

def run_git(self, args, show_output=True, source_dir=None):
cmd_list = ["git"] + list(args)
return self.run_program(
"SCB-GIT", cmd_list,
show_output=show_output, show_command=False, cwd=source_dir,
)

def check_merge_commits(self) -> bool:
merge_commits = self.run_git(
["log", f"{self.base_branch}..HEAD", "--merges", "--oneline"],
show_output=False,
).strip()
if merge_commits:
print(f"{FAIL} Merge commits are not allowed:")
for line in merge_commits.splitlines():
print(f" {line}")
print(f" See: {DOCS_URL}")
return False
print(f"{PASS} No merge commits.")
return True

def check_fixup_commits(self, commits: str) -> bool:
bad = [line for line in commits.splitlines() if "fixup!" in line]
if bad:
print(f"{FAIL} fixup! commits are not allowed:")
for line in bad:
print(f" {line}")
print(f" See: {DOCS_URL}")
return False
print(f"{PASS} No fixup! commits.")
return True

def check_commit_lengths(self, commits: str) -> bool:
ok = True
for line in commits.splitlines():
if not line.strip():
continue
if len(line) > MAX_SUBJECT_LEN:
print(f"{FAIL} Subject too long ({len(line)} chars, limit {MAX_SUBJECT_LEN}): {line}")
ok = False
if ok:
print(f"{PASS} All commit subject lines within {MAX_SUBJECT_LEN} characters.")
return ok

def check_author_emails(self) -> bool:
emails = self.run_git(
["log", f"{self.base_branch}..HEAD", "--format=%ae"],
show_output=False,
).strip()
bad = []
for email in emails.splitlines():
if "example.com" in email:
bad.append(email)
if bad:
print(f"{FAIL} Author email(s) with example.com are not allowed:")
for email in bad:
print(f" {email}")
return False
print(f"{PASS} No unacceptable author emails.")
return True

def run(self) -> None:
if self.base_branch is None:
current = self.find_current_git_branch_or_sha1()
self.base_branch = self.find_git_branch_merge_base(current, self.DEFAULT_UPSTREAM)
self.progress(f"Using merge base with {self.DEFAULT_UPSTREAM}: {self.base_branch}")

commits = self.run_git(
["log", f"{self.base_branch}..HEAD", "--oneline"],
show_output=False,
).strip()

n = len(commits.splitlines()) if commits else 0
print(f"\nChecking {n} commit(s) since {self.base_branch}...\n")

results = [
self.check_merge_commits(),
self.check_fixup_commits(commits),
self.check_commit_lengths(commits),
self.check_author_emails(),
]

failures = results.count(False)
print(f"\n{'All checks passed.' if not failures else f'{failures} check(s) failed.'}")
sys.exit(0 if all(results) else 1)


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Check PR branch commit conventions and markdown linting",
)
parser.add_argument(
"--base-branch",
default=None,
help="Upstream base branch or commit to compare against "
"(default: merge base of HEAD with origin/master)",
)
args = parser.parse_args()
CheckBranchConventions(args.base_branch).run()
Loading