@@ -11,16 +11,114 @@ echo "📦 Local version: $LOCAL_VERSION"
1111NPM_VERSION=$( npm view " $PACKAGE_NAME " version 2> /dev/null || echo " 0.0.0" )
1212echo " 🌐 npm version: $NPM_VERSION "
1313
14- # Check if versions match - if so, bump the patch version
15- if [[ " $LOCAL_VERSION " == " $NPM_VERSION " ]]; then
16- echo " ⚠️ Local version matches npm. Incrementing patch version..."
14+ # Function to compare semver versions
15+ # Echoes: 0 if equal, 1 if first > second, 2 if first < second
16+ compare_versions () {
17+ local v1=$1 v2=$2
18+ IFS=' .' read -r v1_major v1_minor v1_patch <<< " $v1"
19+ IFS=' .' read -r v2_major v2_minor v2_patch <<< " $v2"
1720
18- # Parse version parts
21+ if [[ $v1_major -gt $v2_major ]]; then echo 1; return ; fi
22+ if [[ $v1_major -lt $v2_major ]]; then echo 2; return ; fi
23+ if [[ $v1_minor -gt $v2_minor ]]; then echo 1; return ; fi
24+ if [[ $v1_minor -lt $v2_minor ]]; then echo 2; return ; fi
25+ if [[ $v1_patch -gt $v2_patch ]]; then echo 1; return ; fi
26+ if [[ $v1_patch -lt $v2_patch ]]; then echo 2; return ; fi
27+ echo 0
28+ }
29+
30+ # Check version relationship
31+ VERSION_CMP=$( compare_versions " $LOCAL_VERSION " " $NPM_VERSION " )
32+
33+ if [[ $VERSION_CMP -eq 1 ]]; then
34+ # Local version is higher than npm - use it as-is (manually set)
35+ echo " ✅ Local version ($LOCAL_VERSION ) is higher than npm ($NPM_VERSION ). Using manually set version."
36+ elif [[ $VERSION_CMP -eq 0 ]]; then
37+ # Versions match - need to bump based on commits
38+ echo " ⚠️ Local version matches npm. Analyzing commits for semantic version bump..."
39+
40+ # Get the previous tag to analyze commits
41+ PREV_TAG=$( git describe --tags --abbrev=0 HEAD^ 2> /dev/null || echo " " )
42+
43+ if [[ -n " $PREV_TAG " ]]; then
44+ COMMIT_RANGE=" $PREV_TAG ..HEAD"
45+ else
46+ COMMIT_RANGE=" HEAD"
47+ fi
48+
49+ # Analyze commits to determine bump type
50+ HAS_BREAKING=false
51+ HAS_FEAT=false
52+ HAS_FIX=false
53+
54+ while IFS= read -r line; do
55+ [[ -z " $line " ]] && continue
56+
57+ # Check for breaking changes (BREAKING CHANGE in message or ! after type)
58+ if [[ " $line " =~ BREAKING[[:space:]]CHANGE ]] || [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (feat| fix| refactor| chore)! [:\( ] ]]; then
59+ HAS_BREAKING=true
60+ fi
61+
62+ # Check for features
63+ if [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (feat| feature)[:\( ] ]]; then
64+ HAS_FEAT=true
65+ fi
66+
67+ # Check for fixes
68+ if [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (fix| bugfix)[:\( ] ]]; then
69+ HAS_FIX=true
70+ fi
71+ done < <( git log $COMMIT_RANGE --oneline)
72+
73+ # Parse current version
1974 IFS=' .' read -r MAJOR MINOR PATCH <<< " $LOCAL_VERSION"
75+
76+ # Determine new version based on commit types
77+ if [[ " $HAS_BREAKING " == true ]]; then
78+ NEW_MAJOR=$(( MAJOR + 1 ))
79+ NEW_VERSION=" $NEW_MAJOR .0.0"
80+ BUMP_TYPE=" major (breaking change)"
81+ elif [[ " $HAS_FEAT " == true ]]; then
82+ NEW_MINOR=$(( MINOR + 1 ))
83+ NEW_VERSION=" $MAJOR .$NEW_MINOR .0"
84+ BUMP_TYPE=" minor (new feature)"
85+ elif [[ " $HAS_FIX " == true ]]; then
86+ NEW_PATCH=$(( PATCH + 1 ))
87+ NEW_VERSION=" $MAJOR .$MINOR .$NEW_PATCH "
88+ BUMP_TYPE=" patch (bug fix)"
89+ else
90+ # Default to patch if no recognized commit types
91+ NEW_PATCH=$(( PATCH + 1 ))
92+ NEW_VERSION=" $MAJOR .$MINOR .$NEW_PATCH "
93+ BUMP_TYPE=" patch (default)"
94+ fi
95+
96+ echo " 📝 Bumping version ($BUMP_TYPE ): $LOCAL_VERSION → $NEW_VERSION "
97+
98+ # Update package.json with new version
99+ node -e "
100+ const fs = require('fs');
101+ const pkg = require('./package.json');
102+ pkg.version = '$NEW_VERSION ';
103+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
104+ "
105+
106+ # Amend the last commit with the version bump
107+ git add package.json
108+ git commit --amend --no-edit
109+ git push --force-with-lease
110+
111+ LOCAL_VERSION=" $NEW_VERSION "
112+ echo " ✅ Version bumped and commit amended"
113+ else
114+ # Local version is lower than npm - this shouldn't happen, bump to npm + patch
115+ echo " ⚠️ Local version ($LOCAL_VERSION ) is lower than npm ($NPM_VERSION ). Bumping from npm version..."
116+
117+ IFS=' .' read -r MAJOR MINOR PATCH <<< " $NPM_VERSION"
20118 NEW_PATCH=$(( PATCH + 1 ))
21119 NEW_VERSION=" $MAJOR .$MINOR .$NEW_PATCH "
22120
23- echo " 📝 Bumping version: $LOCAL_VERSION → $NEW_VERSION "
121+ echo " 📝 Bumping version: $NPM_VERSION → $NEW_VERSION "
24122
25123 # Update package.json with new version
26124 node -e "
@@ -37,8 +135,6 @@ if [[ "$LOCAL_VERSION" == "$NPM_VERSION" ]]; then
37135
38136 LOCAL_VERSION=" $NEW_VERSION "
39137 echo " ✅ Version bumped and commit amended"
40- else
41- echo " ✅ Local version ($LOCAL_VERSION ) differs from npm ($NPM_VERSION ). Proceeding..."
42138fi
43139
44140TAG=" v$LOCAL_VERSION "
@@ -65,6 +161,127 @@ if [[ "$TAG_EXISTS" == false ]]; then
65161 git push origin " $TAG "
66162fi
67163
164+ # Generate changelog from commits since last release
165+ echo " 📋 Generating changelog..."
166+
167+ # Get the previous tag (most recent tag before the current one)
168+ PREV_TAG=$( git describe --tags --abbrev=0 HEAD^ 2> /dev/null || echo " " )
169+
170+ if [[ -n " $PREV_TAG " ]]; then
171+ COMMIT_RANGE=" $PREV_TAG ..HEAD"
172+ echo " Commits from $PREV_TAG to HEAD"
173+ else
174+ COMMIT_RANGE=" HEAD"
175+ echo " All commits (no previous tag found)"
176+ fi
177+
178+ # Get commits and group by type
179+ declare -a FEAT_COMMITS
180+ declare -a FIX_COMMITS
181+ declare -a REFACTOR_COMMITS
182+ declare -a DOCS_COMMITS
183+ declare -a STYLE_COMMITS
184+ declare -a TEST_COMMITS
185+ declare -a CHORE_COMMITS
186+ declare -a OTHER_COMMITS
187+
188+ while IFS= read -r line; do
189+ [[ -z " $line " ]] && continue
190+
191+ # Extract the type and message
192+ if [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (feat| feature)[:\( ] ]]; then
193+ FEAT_COMMITS+=(" ${line#* } " )
194+ elif [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (fix| bugfix)[:\( ] ]]; then
195+ FIX_COMMITS+=(" ${line#* } " )
196+ elif [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (refactor)[:\( ] ]]; then
197+ REFACTOR_COMMITS+=(" ${line#* } " )
198+ elif [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (docs| doc)[:\( ] ]]; then
199+ DOCS_COMMITS+=(" ${line#* } " )
200+ elif [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (style)[:\( ] ]]; then
201+ STYLE_COMMITS+=(" ${line#* } " )
202+ elif [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (test| tests)[:\( ] ]]; then
203+ TEST_COMMITS+=(" ${line#* } " )
204+ elif [[ " $line " =~ ^[a-f0-9]+[[:space:]]+ (chore| build| ci)[:\( ] ]]; then
205+ CHORE_COMMITS+=(" ${line#* } " )
206+ else
207+ OTHER_COMMITS+=(" ${line#* } " )
208+ fi
209+ done < <( git log $COMMIT_RANGE --oneline)
210+
211+ # Build changelog
212+ CHANGELOG=" "
213+
214+ if [[ ${# FEAT_COMMITS[@]} -gt 0 ]]; then
215+ CHANGELOG+=" ## Features\n"
216+ for commit in " ${FEAT_COMMITS[@]} " ; do
217+ CHANGELOG+=" - $commit \n"
218+ done
219+ CHANGELOG+=" \n"
220+ fi
221+
222+ if [[ ${# FIX_COMMITS[@]} -gt 0 ]]; then
223+ CHANGELOG+=" ## Bug Fixes\n"
224+ for commit in " ${FIX_COMMITS[@]} " ; do
225+ CHANGELOG+=" - $commit \n"
226+ done
227+ CHANGELOG+=" \n"
228+ fi
229+
230+ if [[ ${# REFACTOR_COMMITS[@]} -gt 0 ]]; then
231+ CHANGELOG+=" ## Refactoring\n"
232+ for commit in " ${REFACTOR_COMMITS[@]} " ; do
233+ CHANGELOG+=" - $commit \n"
234+ done
235+ CHANGELOG+=" \n"
236+ fi
237+
238+ if [[ ${# DOCS_COMMITS[@]} -gt 0 ]]; then
239+ CHANGELOG+=" ## Documentation\n"
240+ for commit in " ${DOCS_COMMITS[@]} " ; do
241+ CHANGELOG+=" - $commit \n"
242+ done
243+ CHANGELOG+=" \n"
244+ fi
245+
246+ if [[ ${# STYLE_COMMITS[@]} -gt 0 ]]; then
247+ CHANGELOG+=" ## Styling\n"
248+ for commit in " ${STYLE_COMMITS[@]} " ; do
249+ CHANGELOG+=" - $commit \n"
250+ done
251+ CHANGELOG+=" \n"
252+ fi
253+
254+ if [[ ${# TEST_COMMITS[@]} -gt 0 ]]; then
255+ CHANGELOG+=" ## Tests\n"
256+ for commit in " ${TEST_COMMITS[@]} " ; do
257+ CHANGELOG+=" - $commit \n"
258+ done
259+ CHANGELOG+=" \n"
260+ fi
261+
262+ if [[ ${# CHORE_COMMITS[@]} -gt 0 ]]; then
263+ CHANGELOG+=" ## Chores\n"
264+ for commit in " ${CHORE_COMMITS[@]} " ; do
265+ CHANGELOG+=" - $commit \n"
266+ done
267+ CHANGELOG+=" \n"
268+ fi
269+
270+ if [[ ${# OTHER_COMMITS[@]} -gt 0 ]]; then
271+ CHANGELOG+=" ## Other Changes\n"
272+ for commit in " ${OTHER_COMMITS[@]} " ; do
273+ CHANGELOG+=" - $commit \n"
274+ done
275+ CHANGELOG+=" \n"
276+ fi
277+
278+ # Fallback if no commits found
279+ if [[ -z " $CHANGELOG " ]]; then
280+ CHANGELOG=" No notable changes in this release."
281+ fi
282+
283+ echo " Changelog generated!"
284+
68285# Create GitHub release (triggers the publish workflow)
69286echo " 🚀 Creating GitHub release..."
70287
@@ -73,9 +290,9 @@ REPO_NAME="api-ape"
73290
74291# Try gh CLI first, fall back to curl
75292if command -v gh & > /dev/null; then
76- gh release create " $TAG " \
293+ echo -e " $CHANGELOG " | gh release create " $TAG " \
77294 --title " $TAG " \
78- --generate- notes
295+ --notes-file -
79296else
80297 # Use GitHub API with curl
81298 if [[ -z " $GITHUB_TOKEN " ]]; then
85302 exit 1
86303 fi
87304
305+ # Escape the changelog for JSON
306+ CHANGELOG_ESCAPED=$( echo -e " $CHANGELOG " | python3 -c ' import json,sys; print(json.dumps(sys.stdin.read()))' )
307+
88308 # Create release via GitHub API
89309 RESPONSE=$( curl -s -X POST \
90310 -H " Authorization: token $GITHUB_TOKEN " \
93313 -d " {
94314 \" tag_name\" : \" $TAG \" ,
95315 \" name\" : \" $TAG \" ,
96- \" generate_release_notes \" : true
316+ \" body \" : $CHANGELOG_ESCAPED
97317 }" )
98318
99319 # Check if release was created
0 commit comments