|
| 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