Skip to content

Commit c1dd240

Browse files
committed
chore(ci): enforce conventional commits as per the MA201 spec
1 parent 75503e0 commit c1dd240

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Check Conventional Commit
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited, synchronize, reopened]
6+
7+
jobs:
8+
check-conventional-commit:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
13+
14+
- name: Validate PR title and body against conventional commit spec
15+
env:
16+
PR_TITLE: ${{ github.event.pull_request.title }}
17+
PR_BODY: ${{ github.event.pull_request.body }}
18+
run: |
19+
# We create the squash commit for PR as follows:
20+
# PR title as subject, blank line, then PR body.
21+
commit_msg="${PR_TITLE}"
22+
if [[ -n "${PR_BODY}" ]]; then
23+
commit_msg="${PR_TITLE}
24+
25+
${PR_BODY}"
26+
fi
27+
28+
./utilities/check-conventional-commit.sh "$commit_msg"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# check-conventional-commit.sh
5+
#
6+
# Validate a commit message against the MAAS MA201 - Conventional commits spec.
7+
#
8+
# Usage:
9+
# ./check-conventional-commit.sh "<commit message>"
10+
11+
ALLOWED_TYPES_RE='feat|fix|refactor|perf|test|chore|docs'
12+
ALLOWED_SCOPES_RE='bootresources|dhcp|dns|network|power|proxy|security|storage|tftp|deps|ci'
13+
14+
die() {
15+
echo "ERROR: $*" >&2
16+
exit 1
17+
}
18+
19+
usage() {
20+
cat >&2 <<'EOF'
21+
Usage:
22+
check-conventional-commit.sh "<commit message>"
23+
EOF
24+
exit 1
25+
}
26+
27+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
28+
usage
29+
fi
30+
31+
if [[ $# -ne 1 ]]; then
32+
usage
33+
fi
34+
35+
MSG="$1"
36+
37+
if [[ -z "$MSG" ]]; then
38+
die "Empty commit message"
39+
fi
40+
41+
subject="$(echo "$MSG" | head -n 1)"
42+
43+
# Validate header syntax:
44+
# <type>[(<scope>)][!]: <description>
45+
header_re="^(${ALLOWED_TYPES_RE})(\((${ALLOWED_SCOPES_RE})\))?(!)?:[[:space:]]+.+$"
46+
if ! [[ "$subject" =~ $header_re ]]; then
47+
die "Commit subject does not match required format: <type>[(<scope>)][!]: <description>
48+
Allowed types : ${ALLOWED_TYPES_RE//|/, }
49+
Allowed scopes : ${ALLOWED_SCOPES_RE//|/, }
50+
Got: $subject"
51+
fi
52+
53+
type="${BASH_REMATCH[1]}"
54+
scope="${BASH_REMATCH[3]:-}"
55+
bang="${BASH_REMATCH[4]:-}"
56+
57+
# The 'ci' scope is only allowed with the 'chore' type.
58+
if [[ "$scope" == "ci" && "$type" != "chore" ]]; then
59+
die "Scope 'ci' can only be used with type 'chore'. Got: ${type}(ci)"
60+
fi
61+
62+
# If '!' present, require "BREAKING CHANGE: <description>" footer
63+
if [[ -n "$bang" ]]; then
64+
if ! printf '%s\n' "$MSG" | grep -Eq '^BREAKING CHANGE:[[:space:]]+.+$'; then
65+
die "Header contains '!' (breaking change) but no 'BREAKING CHANGE: <description>' footer was found."
66+
fi
67+
fi
68+
69+
# fix commits must include a bug reference (case-insensitive).
70+
# Accepted keywords: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
71+
# An optional colon may follow the keyword.
72+
# Example valid footers:
73+
# Resolves LP: #1234567
74+
# fixes: LP: #1234567
75+
# Closed LP: #1234567
76+
BUG_REF_KEYWORDS="close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved"
77+
BUG_REF_RE="^(${BUG_REF_KEYWORDS}):?[[:space:]]+LP:[[:space:]]?[0-9]+$"
78+
79+
if [[ "$type" == "fix" ]]; then
80+
if ! printf '%s\n' "$MSG" | grep -Eiq "$BUG_REF_RE"; then
81+
die "Type is 'fix' but no bug reference footer was found. Add the reference to the Launchpad bug:
82+
Resolves LP:<bug-number>"
83+
fi
84+
fi
85+
86+
echo "OK: Conventional Commit compliant."
87+

0 commit comments

Comments
 (0)