22set -eu
33
44usage () {
5- echo " Usage: $0 <version>"
5+ echo " Usage: $0 <version|--auto >"
66 echo " "
77 echo " Create and push a release tag to trigger the GitHub Actions release workflow."
88 echo " "
99 echo " Examples:"
10- echo " $0 0.1.0"
11- echo " $0 v0.1.0"
10+ echo " $0 1.1.0"
11+ echo " $0 v1.1.0"
12+ echo " $0 --auto"
1213 exit 1
1314}
1415
@@ -17,16 +18,169 @@ die() {
1718 exit 1
1819}
1920
20- # Require a version argument
21+ # Detect next version by analyzing commits since the last tag.
22+ # Classifies commits by prefix and determines the appropriate semver bump.
23+ detect_next_version () {
24+ # Find latest semver tag
25+ latest_tag=$( git tag -l ' v*' | sort -V | tail -1)
26+ if [ -z " $latest_tag " ]; then
27+ latest_tag=" v0.0.0"
28+ echo " No existing tags found. Starting from v0.0.0."
29+ commit_range=" HEAD"
30+ else
31+ commit_range=" ${latest_tag} ..HEAD"
32+ fi
33+
34+ # Get commits since last tag
35+ commits=$( git log " $commit_range " --oneline 2> /dev/null || true)
36+ if [ -z " $commits " ]; then
37+ echo " No commits since $latest_tag . Nothing to release."
38+ exit 0
39+ fi
40+
41+ commit_count=$( echo " $commits " | wc -l | tr -d ' ' )
42+
43+ # Classify commits by prefix and determine bump type
44+ bump=" patch"
45+ breaking_commits=" "
46+ feat_commits=" "
47+ other_commits=" "
48+
49+ # Use a temp file to handle BREAKING CHANGE body detection without subshell issues
50+ body_breaking_file=$( mktemp)
51+ trap ' rm -f "$body_breaking_file"' EXIT
52+
53+ # Check commit bodies for BREAKING CHANGE
54+ git log " $commit_range " --format=" %H" 2> /dev/null | while IFS= read -r hash ; do
55+ body=$( git log -1 --format=" %b" " $hash " )
56+ case " $body " in
57+ * " BREAKING CHANGE" * )
58+ subject=$( git log -1 --format=" %s" " $hash " )
59+ echo " $subject " >> " $body_breaking_file "
60+ ;;
61+ esac
62+ done
63+
64+ # Process each commit
65+ OLD_IFS=" $IFS "
66+ IFS='
67+ '
68+ for line in $commits ; do
69+ subject=$( echo " $line " | sed ' s/^[a-f0-9]* //' )
70+ case " $subject " in
71+ \[ breaking\] * |\[ BREAKING\] * )
72+ bump=" major"
73+ breaking_commits=" ${breaking_commits} - ${subject}
74+ "
75+ ;;
76+ \[ feat\] * |\[ Feat\] * )
77+ if [ " $bump " != " major" ]; then
78+ bump=" minor"
79+ fi
80+ feat_commits=" ${feat_commits} - ${subject}
81+ "
82+ ;;
83+ * )
84+ other_commits=" ${other_commits} - ${subject}
85+ "
86+ ;;
87+ esac
88+ done
89+ IFS=" $OLD_IFS "
90+
91+ # Incorporate BREAKING CHANGE from commit bodies
92+ if [ -s " $body_breaking_file " ]; then
93+ bump=" major"
94+ OLD_IFS=" $IFS "
95+ IFS='
96+ '
97+ for body_subject in $( cat " $body_breaking_file " ) ; do
98+ # Avoid duplicating commits already listed as [breaking]
99+ case " $breaking_commits " in
100+ * " $body_subject " * ) ;;
101+ * ) breaking_commits=" ${breaking_commits} - ${body_subject} (BREAKING CHANGE in body)
102+ " ;;
103+ esac
104+ done
105+ IFS=" $OLD_IFS "
106+ fi
107+
108+ # Parse current version
109+ major=$( echo " $latest_tag " | sed ' s/^v//' | cut -d. -f1)
110+ minor=$( echo " $latest_tag " | sed ' s/^v//' | cut -d. -f2)
111+ patch=$( echo " $latest_tag " | sed ' s/^v//' | cut -d. -f3)
112+
113+ # Compute next version
114+ case " $bump " in
115+ major)
116+ major=$(( major + 1 ))
117+ minor=0
118+ patch=0
119+ ;;
120+ minor)
121+ minor=$(( minor + 1 ))
122+ patch=0
123+ ;;
124+ patch)
125+ patch=$(( patch + 1 ))
126+ ;;
127+ esac
128+
129+ next_version=" v${major} .${minor} .${patch} "
130+
131+ # Print summary
132+ echo " "
133+ echo " Current version: $latest_tag "
134+ echo " Commits since $latest_tag : $commit_count "
135+ echo " "
136+
137+ if [ -n " $breaking_commits " ]; then
138+ echo " Breaking changes (major):"
139+ printf " %s" " $breaking_commits "
140+ echo " "
141+ fi
142+
143+ if [ -n " $feat_commits " ]; then
144+ echo " Features (minor):"
145+ printf " %s" " $feat_commits "
146+ echo " "
147+ fi
148+
149+ if [ -n " $other_commits " ]; then
150+ echo " Other:"
151+ printf " %s" " $other_commits "
152+ echo " "
153+ fi
154+
155+ echo " Next version: $next_version "
156+ echo " "
157+
158+ # Prompt for confirmation
159+ printf " Continue with release? [y/N] "
160+ read -r confirm
161+ case " $confirm " in
162+ [yY]|[yY][eE][sS]) ;;
163+ * ) echo " Aborted." ; exit 0 ;;
164+ esac
165+
166+ # Set version for the rest of the script
167+ version=" $next_version "
168+ }
169+
170+ # Require exactly one argument
21171[ $# -eq 1 ] || usage
22172
23- version=" $1 "
173+ if [ " $1 " = " --auto" ]; then
174+ detect_next_version
175+ else
176+ version=" $1 "
24177
25- # Normalize: ensure v prefix
26- case " $version " in
27- v* ) ;;
28- * ) version=" v${version} " ;;
29- esac
178+ # Normalize: ensure v prefix
179+ case " $version " in
180+ v* ) ;;
181+ * ) version=" v${version} " ;;
182+ esac
183+ fi
30184
31185# Validate semver format (vX.Y.Z)
32186echo " $version " | grep -qE ' ^v[0-9]+\.[0-9]+\.[0-9]+$' \
0 commit comments