diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7f7ecbaf836a5..422fc7927afb6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -45,5 +45,9 @@ jobs: cd nuttx commits="${{ github.event.pull_request.base.sha }}..HEAD" git log --oneline $commits - echo "../nuttx/tools/checkpatch.sh -c -u -m -g $commits" - ../nuttx/tools/checkpatch.sh -c -u -m -g $commits + breaking_opts="" + if echo '${{ toJSON(github.event.pull_request.labels) }}' | jq -e 'any(.[].name; test("breaking change"; "i"))' >/dev/null 2>&1; then + breaking_opts="-b" + fi + echo "../nuttx/tools/checkpatch.sh -c -u -m -g $breaking_opts $commits" + ../nuttx/tools/checkpatch.sh -c -u -m -g $breaking_opts $commits diff --git a/tools/checkpatch.sh b/tools/checkpatch.sh index cce2e6f2d2981..a84dfc379e61f 100755 --- a/tools/checkpatch.sh +++ b/tools/checkpatch.sh @@ -32,6 +32,7 @@ range=0 spell=0 encoding=0 message=0 +breaking_change=0 # CMake cmake_warning_once=0 @@ -57,7 +58,10 @@ usage() { echo "-r range check only (coupled with -p or -g)" echo "-p (default)" echo "-m Check commit message (coupled with -g)" + echo "-b Enforce breaking change format when checking commit message (requires -m -g; use when PR has breaking change label)" echo "-g " + echo " Use --stdin as the only argument with -m -g to read commit message from stdin (message-only check, no patch/diff)." + echo " Use --stdin with -p to read patch content from stdin." echo "-f " echo "-x format supported files (only .py, requires: pip install black)" echo "- read standard input mainly used by git pre-commit hook as below:" @@ -284,15 +288,17 @@ check_patch() { check_msg() { signedoffby_found=0 num_lines=0 + # Commit subject line length limit (50/72 are common; NuttX uses 80) max_line_len=80 min_num_lines=5 + breaking_change_found=0 first=$(head -n1 <<< "$msg") # check for Merge line and remove from parsed string if [[ $first == *Merge* ]]; then msg="$(echo "$msg" | tail -n +2)" - first=$(head -n2 <<< "$msg") + first=$(head -n1 <<< "$msg") fi while IFS= read -r REPLY; do @@ -311,7 +317,15 @@ check_msg() { fail=1 fi + if [[ $REPLY =~ ^BREAKING\ CHANGE: ]]; then + breaking_change_found=1 + fi + if [[ $REPLY =~ ^Signed-off-by ]]; then + if [ $breaking_change != 0 ] && [ $breaking_change_found == 0 ]; then + echo "❌ BREAKING CHANGE: must appear in the commit body before Signed-off-by (see CONTRIBUTING.md 1.13)" + fail=1 + fi signedoffby_found=1 fi @@ -328,6 +342,23 @@ check_msg() { fail=1 fi + second=$(echo "$msg" | sed -n '2p') + if [ $num_lines -ge 2 ] && ! [[ "$second" =~ ^[[:space:]]*$ ]]; then + echo "❌ Commit subject must be followed by a blank line" + fail=1 + fi + + if [ $breaking_change != 0 ]; then + if [[ "${first:0:1}" != "!" ]]; then + echo "❌ Breaking change commit subject must start with '!' (e.g. '!subsystem: description')" + fail=1 + fi + if [ $breaking_change_found == 0 ]; then + echo "❌ Breaking change commit must contain 'BREAKING CHANGE:' in the body before Signed-off-by (see CONTRIBUTING.md 1.13)" + fail=1 + fi + fi + if ! [ $signedoffby_found == 1 ]; then echo "❌ Missing Signed-off-by" fail=1 @@ -383,12 +414,18 @@ while [ ! -z "$1" ]; do -m ) message=1 ;; + -b ) + breaking_change=1 + ;; -g ) check=check_commit ;; -h ) usage 0 ;; + --stdin ) + break + ;; -p ) check=check_patch ;; @@ -406,7 +443,28 @@ while [ ! -z "$1" ]; do done for arg in $@; do - $check $arg + if [ "$arg" = "--stdin" ]; then + case "$check" in + check_commit) + msg=$(cat) + check_msg <<< "$msg" + ;; + check_patch) + tmp=$(mktemp) + trap "rm -f $tmp" EXIT + cat > "$tmp" + check_patch "$tmp" + rm -f "$tmp" + trap - EXIT + ;; + check_file|format_file) + echo "❌ --stdin is only supported with -g (commit message) or -p (patch)" + fail=1 + ;; + esac + else + $check $arg + fi done if [ $fail == 1 ]; then