|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | # check-commits.sh — Conventional Commits enforcement. |
| 3 | +# |
| 4 | +# The repos squash-merge PRs, so ONLY the final squashed subject (which |
| 5 | +# defaults to the PR title) becomes a commit on main. Checking every |
| 6 | +# intermediate commit floods the review with findings for messages that |
| 7 | +# will be discarded. We therefore check a single subject: |
| 8 | +# 1. PR_TITLE (env, set by orchestrate from the PR metadata) if available |
| 9 | +# 2. else the most recent non-merge commit subject (HEAD) |
3 | 10 | set -euo pipefail |
4 | 11 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
5 | 12 | source "$SCRIPT_DIR/lib/json-emit.sh" |
6 | 13 |
|
7 | 14 | LANGUAGE="${LANGUAGE:-python}" |
8 | | -# Resolve BASE_SHA from GITHUB_BASE_REF merge-base when not explicitly set — |
9 | | -# HEAD~10 is a poor fallback (only reachable when the PR has ≥10 commits). |
10 | | -if [ -z "${BASE_SHA:-}" ]; then |
11 | | - base_ref="${GITHUB_BASE_REF:-main}" |
12 | | - # Try origin/<ref>, then <ref>, then finally HEAD~10 as last resort |
13 | | - if git rev-parse --verify "origin/${base_ref}" >/dev/null 2>&1; then |
14 | | - BASE_SHA=$(git merge-base HEAD "origin/${base_ref}" 2>/dev/null || echo "HEAD~10") |
15 | | - elif git rev-parse --verify "$base_ref" >/dev/null 2>&1; then |
16 | | - BASE_SHA=$(git merge-base HEAD "$base_ref" 2>/dev/null || echo "HEAD~10") |
17 | | - else |
18 | | - BASE_SHA="HEAD~10" |
19 | | - fi |
20 | | -fi |
21 | 15 | HEAD_SHA="${HEAD_SHA:-HEAD}" |
| 16 | +PR_TITLE="${PR_TITLE:-}" |
22 | 17 |
|
23 | 18 | STARTED=$(now_iso) |
24 | 19 | findings=$(mktemp); trap 'rm -f "$findings"' EXIT |
25 | 20 |
|
26 | | -# List commit subjects |
27 | | -commits=$(git log "${BASE_SHA}..${HEAD_SHA}" --format='%H %s' 2>/dev/null || echo "") |
28 | 21 | prefix_regex='^(feat|fix|refactor|chore|docs|test|ci|build|perf|style|revert)(\([a-z0-9_/-]+\))?!?:[[:space:]]+.+' |
29 | 22 |
|
30 | | -echo "$commits" | while IFS= read -r line; do |
31 | | - [ -z "$line" ] && continue |
32 | | - sha=$(echo "$line" | cut -d' ' -f1) |
33 | | - subject=$(echo "$line" | cut -d' ' -f2-) |
34 | | - # Skip merge commits (identified by 2+ parents, robust regardless of message shape) |
35 | | - parents=$(git rev-list --parents -n 1 "$sha" 2>/dev/null | awk '{print NF-1}') |
36 | | - if [ "${parents:-0}" -gt 1 ]; then continue; fi |
37 | | - # Also skip legacy "Merge branch/pull/etc" defaults |
38 | | - if echo "$subject" | grep -qiE '^Merge '; then continue; fi |
| 23 | +# Pick the single subject that will land on main after squash-merge. |
| 24 | +if [ -n "$PR_TITLE" ]; then |
| 25 | + subject="$PR_TITLE" |
| 26 | + location="PR_TITLE" |
| 27 | +else |
| 28 | + # Most recent non-merge commit subject. |
| 29 | + subject=$({ git log "$HEAD_SHA" --no-merges -1 --format='%s' 2>/dev/null || true; }) |
| 30 | + location="COMMIT:$({ git rev-parse "$HEAD_SHA" 2>/dev/null || echo HEAD; })" |
| 31 | +fi |
| 32 | + |
| 33 | +if [ -n "$subject" ] && ! echo "$subject" | grep -qiE '^Merge '; then |
39 | 34 | if ! echo "$subject" | grep -qE "$prefix_regex"; then |
40 | | - emit_finding "COM-01" "FLAG" "COMMIT:$sha" 1 \ |
41 | | - "Commit '$subject' does not follow Conventional Commits" \ |
42 | | - "Prefix with feat:/fix:/chore:/docs:/test:/refactor:/ci:/build:/perf:/style:/revert:" >> "$findings" |
| 35 | + emit_finding "COM-01" "FLAG" "$location" 1 \ |
| 36 | + "Squash-merge subject '$subject' does not follow Conventional Commits" \ |
| 37 | + "The PR title becomes the squashed commit — prefix with feat:/fix:/chore:/docs:/test:/refactor:/ci:/build:/perf:/style:/revert:" >> "$findings" |
43 | 38 | fi |
44 | | -done |
| 39 | +fi |
45 | 40 |
|
46 | 41 | status=$(status_from_findings < "$findings") |
47 | 42 | emit_report "commits" "$LANGUAGE" "$status" "$STARTED" < "$findings" |
0 commit comments