1+ #! /usr/bin/env bash
2+
3+ # Ensure we're running in bash
4+ if [ -z " $BASH_VERSION " ]; then
5+ echo " This script requires bash. Please run with: bash $0 " >&2
6+ exit 1
7+ fi
8+
9+ # Ensure we're using bash 4.0 or later for associative arrays
10+ if (( BASH_VERSINFO[0 ] < 4 )) ; then
11+ echo " This script requires bash version 4 or later" >&2
12+ echo " Current bash version: $BASH_VERSION " >&2
13+ exit 1
14+ fi
15+
16+ # Set default values for required environment variables if not in GitHub Actions
17+ if [ -z " $GITHUB_ACTIONS " ]; then
18+ : " ${GITHUB_SERVER_URL:= https:// github.com} "
19+ : " ${GITHUB_REPOSITORY:= gerome-elassaad/ codinit-app} "
20+ : " ${GITHUB_OUTPUT:=/ tmp/ github_output} "
21+ touch " $GITHUB_OUTPUT "
22+
23+ # Running locally
24+ echo " Running locally - checking for upstream remote..."
25+ MAIN_REMOTE=" origin"
26+ if git remote -v | grep -q " upstream" ; then
27+ MAIN_REMOTE=" upstream"
28+ fi
29+ MAIN_BRANCH=" main" # or "master" depending on your repository
30+
31+ # Ensure we have latest tags
32+ git fetch ${MAIN_REMOTE} --tags
33+
34+ # Use the remote reference for git log
35+ GITLOG_REF=" ${MAIN_REMOTE} /${MAIN_BRANCH} "
36+ else
37+ # Running in GitHub Actions
38+ GITLOG_REF=" HEAD"
39+ fi
40+
41+ # Get the latest tag
42+ LATEST_TAG=$( git describe --tags --abbrev=0 2> /dev/null || echo " " )
43+
44+ # Start changelog file
45+ echo " # 🚀 Release v${NEW_VERSION} " > changelog.md
46+ echo " " >> changelog.md
47+ echo " ## What's Changed 🌟" >> changelog.md
48+ echo " " >> changelog.md
49+
50+ if [ -z " $LATEST_TAG " ]; then
51+ echo " ### 🎉 First Release" >> changelog.md
52+ echo " " >> changelog.md
53+ echo " Exciting times! This marks our first release. Thanks to everyone who contributed! 🙌" >> changelog.md
54+ echo " " >> changelog.md
55+ COMPARE_BASE=" $( git rev-list --max-parents=0 HEAD) "
56+ else
57+ echo " ### 🔄 Changes since $LATEST_TAG " >> changelog.md
58+ echo " " >> changelog.md
59+ COMPARE_BASE=" $LATEST_TAG "
60+ fi
61+
62+ # Function to extract conventional commit type and associated emoji
63+ get_commit_type () {
64+ local msg=" $1 "
65+ if [[ $msg =~ ^feat(\( .+\) )? :| ^feature(\( .+\) )? : ]]; then echo " ✨ Features"
66+ elif [[ $msg =~ ^fix(\( .+\) )? : ]]; then echo " 🐛 Bug Fixes"
67+ elif [[ $msg =~ ^docs(\( .+\) )? : ]]; then echo " 📚 Documentation"
68+ elif [[ $msg =~ ^style(\( .+\) )? : ]]; then echo " 💎 Styles"
69+ elif [[ $msg =~ ^refactor(\( .+\) )? : ]]; then echo " ♻️ Code Refactoring"
70+ elif [[ $msg =~ ^perf(\( .+\) )? : ]]; then echo " ⚡ Performance Improvements"
71+ elif [[ $msg =~ ^test(\( .+\) )? : ]]; then echo " 🧪 Tests"
72+ elif [[ $msg =~ ^build(\( .+\) )? : ]]; then echo " 🛠️ Build System"
73+ elif [[ $msg =~ ^ci(\( .+\) )? : ]]; then echo " ⚙️ CI"
74+ elif [[ $msg =~ ^chore(\( .+\) )? : ]]; then echo " " # Skip chore commits
75+ else echo " 🔍 Other Changes" # Default category with emoji
76+ fi
77+ }
78+
79+ # Initialize associative arrays
80+ declare -A CATEGORIES
81+ declare -A COMMITS_BY_CATEGORY
82+ declare -A ALL_AUTHORS
83+ declare -A NEW_CONTRIBUTORS
84+
85+ # Get all historical authors before the compare base
86+ while IFS= read -r author; do
87+ ALL_AUTHORS[" $author " ]=1
88+ done < <( git log " ${COMPARE_BASE} " --pretty=format:" %ae" | sort -u)
89+
90+ # Process all commits since last tag
91+ while IFS= read -r commit_line; do
92+ if [[ ! $commit_line =~ ^[a-f0-9]+\| ]]; then
93+ echo " WARNING: Skipping invalid commit line format: $commit_line " >&2
94+ continue
95+ fi
96+
97+ HASH=$( echo " $commit_line " | cut -d' |' -f1)
98+ COMMIT_MSG=$( echo " $commit_line " | cut -d' |' -f2)
99+ BODY=$( echo " $commit_line " | cut -d' |' -f3)
100+ # Skip if hash doesn't match the expected format
101+ if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then
102+ continue
103+ fi
104+
105+ HASH=$( echo " $commit_line " | cut -d' |' -f1)
106+ COMMIT_MSG=$( echo " $commit_line " | cut -d' |' -f2)
107+ BODY=$( echo " $commit_line " | cut -d' |' -f3)
108+
109+
110+ # Validate hash format
111+ if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then
112+ echo " WARNING: Invalid commit hash format: $HASH " >&2
113+ continue
114+ fi
115+
116+ # Check if it's a merge commit
117+ if [[ $COMMIT_MSG =~ Merge\ pull\ request\ # ([0-9]+) ]]; then
118+ # echo "Processing as merge commit" >&2
119+ PR_NUM= " ${BASH_REMATCH[1]} "
120+
121+ # Extract the PR title from the merge commit body
122+ PR_TITLE= $( echo " $BODY " | grep -v " ^Merge pull request" | head -n 1)
123+
124+ # Only process if it follows conventional commit format
125+ CATEGORY= $( get_commit_type " $PR_TITLE " )
126+
127+ if [ -n " $CATEGORY " ]; then # Only process if it's a conventional commit
128+ # Get PR author's GitHub username
129+ GITHUB_USERNAME=$( gh pr view " $PR_NUM " --json author --jq ' .author.login' )
130+
131+ if [ -n " $GITHUB_USERNAME " ]; then
132+ # Check if this is a first-time contributor
133+ AUTHOR_EMAIL=$( git show -s --format=' %ae' " $HASH " )
134+ if [ -z " ${ALL_AUTHORS[$AUTHOR_EMAIL]} " ]; then
135+ NEW_CONTRIBUTORS[" $GITHUB_USERNAME " ]=1
136+ ALL_AUTHORS[" $AUTHOR_EMAIL " ]=1
137+ fi
138+
139+ CATEGORIES[" $CATEGORY " ]=1
140+ COMMITS_BY_CATEGORY[" $CATEGORY " ]+=" * ${PR_TITLE#*: } ([#$PR_NUM ](${GITHUB_SERVER_URL} /${GITHUB_REPOSITORY} /pull/$PR_NUM )) by @$GITHUB_USERNAME " $' \n '
141+ else
142+ COMMITS_BY_CATEGORY[" $CATEGORY " ]+=" * ${PR_TITLE#*: } ([#$PR_NUM ](${GITHUB_SERVER_URL} /${GITHUB_REPOSITORY} /pull/$PR_NUM ))" $' \n '
143+ fi
144+ fi
145+ # Check if it's a squash merge by looking for (#NUMBER) pattern
146+ elif [[ $COMMIT_MSG =~ \( # ([0-9]+)\) ]]; then
147+ # echo "Processing as squash commit" >&2
148+ PR_NUM= " ${BASH_REMATCH[1]} "
149+
150+ # Only process if it follows conventional commit format
151+ CATEGORY= $( get_commit_type " $COMMIT_MSG " )
152+
153+ if [ -n " $CATEGORY " ]; then # Only process if it's a conventional commit
154+ # Get PR author's GitHub username
155+ GITHUB_USERNAME=$( gh pr view " $PR_NUM " --json author --jq ' .author.login' )
156+
157+ if [ -n " $GITHUB_USERNAME " ]; then
158+ # Check if this is a first-time contributor
159+ AUTHOR_EMAIL=$( git show -s --format=' %ae' " $HASH " )
160+ if [ -z " ${ALL_AUTHORS[$AUTHOR_EMAIL]} " ]; then
161+ NEW_CONTRIBUTORS[" $GITHUB_USERNAME " ]=1
162+ ALL_AUTHORS[" $AUTHOR_EMAIL " ]=1
163+ fi
164+
165+ CATEGORIES[" $CATEGORY " ]=1
166+ COMMIT_TITLE=${COMMIT_MSG%% (#* } # Remove the PR number suffix
167+ COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
168+ COMMITS_BY_CATEGORY[" $CATEGORY " ]+=" * $COMMIT_TITLE ([#$PR_NUM ](${GITHUB_SERVER_URL} /${GITHUB_REPOSITORY} /pull/$PR_NUM )) by @$GITHUB_USERNAME " $' \n '
169+ else
170+ COMMIT_TITLE=${COMMIT_MSG%% (#* } # Remove the PR number suffix
171+ COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
172+ COMMITS_BY_CATEGORY[" $CATEGORY " ]+=" * $COMMIT_TITLE ([#$PR_NUM ](${GITHUB_SERVER_URL} /${GITHUB_REPOSITORY} /pull/$PR_NUM ))" $' \n '
173+ fi
174+ fi
175+
176+ else
177+ # echo "Processing as regular commit" >&2
178+ # Process conventional commits without PR numbers
179+ CATEGORY= $( get_commit_type " $COMMIT_MSG " )
180+
181+ if [ -n " $CATEGORY " ]; then # Only process if it's a conventional commit
182+ # Get commit author info
183+ AUTHOR_EMAIL=$( git show -s --format=' %ae' " $HASH " )
184+
185+ # Try to get GitHub username using gh api
186+ if [ -n " $GITHUB_ACTIONS " ] || command -v gh > /dev/null 2>&1 ; then
187+ GITHUB_USERNAME=$( gh api " /repos/${GITHUB_REPOSITORY} /commits/${HASH} " --jq ' .author.login' 2> /dev/null)
188+ fi
189+
190+ if [ -n " $GITHUB_USERNAME " ]; then
191+ # If we got GitHub username, use it
192+ if [ -z " ${ALL_AUTHORS[$AUTHOR_EMAIL]} " ]; then
193+ NEW_CONTRIBUTORS[" $GITHUB_USERNAME " ]=1
194+ ALL_AUTHORS[" $AUTHOR_EMAIL " ]=1
195+ fi
196+
197+ CATEGORIES[" $CATEGORY " ]=1
198+ COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
199+ COMMITS_BY_CATEGORY[" $CATEGORY " ]+=" * $COMMIT_TITLE (${HASH: 0: 7} ) by @$GITHUB_USERNAME " $' \n '
200+ else
201+ # Fallback to git author name if no GitHub username found
202+ AUTHOR_NAME=$( git show -s --format=' %an' " $HASH " )
203+
204+ if [ -z " ${ALL_AUTHORS[$AUTHOR_EMAIL]} " ]; then
205+ NEW_CONTRIBUTORS[" $AUTHOR_NAME " ]=1
206+ ALL_AUTHORS[" $AUTHOR_EMAIL " ]=1
207+ fi
208+
209+ CATEGORIES[" $CATEGORY " ]=1
210+ COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
211+ COMMITS_BY_CATEGORY[" $CATEGORY " ]+=" * $COMMIT_TITLE (${HASH: 0: 7} ) by $AUTHOR_NAME " $' \n '
212+ fi
213+ fi
214+ fi
215+
216+ done < < (git log " ${COMPARE_BASE} ..${GITLOG_REF} " --pretty=format:" %H|%s|%b" --reverse --first-parent)
217+
218+ # Write categorized commits to changelog with their emojis
219+ for category in " ✨ Features" " 🐛 Bug Fixes" " 📚 Documentation" " 💎 Styles" " ♻️ Code Refactoring" " ⚡ Performance Improvements" " 🧪 Tests" " 🛠️ Build System" " ⚙️ CI" " 🔍 Other Changes" ; do
220+ if [ -n " ${COMMITS_BY_CATEGORY[$category]} " ]; then
221+ echo " ### $category " >> changelog.md
222+ echo " " >> changelog.md
223+ echo " ${COMMITS_BY_CATEGORY[$category]} " >> changelog.md
224+ echo " " >> changelog.md
225+ fi
226+ done
227+
228+ # Add first-time contributors section if there are any
229+ if [ ${# NEW_CONTRIBUTORS[@]} -gt 0 ]; then
230+ echo " ## ✨ First-time Contributors" >> changelog.md
231+ echo " " >> changelog.md
232+ echo " A huge thank you to our amazing new contributors! Your first contribution marks the start of an exciting journey! 🌟" >> changelog.md
233+ echo " " >> changelog.md
234+ # Use readarray to sort the keys
235+ readarray -t sorted_contributors < <( printf ' %s\n' " ${! NEW_CONTRIBUTORS[@]} " | sort)
236+ for github_username in " ${sorted_contributors[@]} " ; do
237+ echo " * 🌟 [@$github_username ](https://github.com/$github_username )" >> changelog.md
238+ done
239+ echo " " >> changelog.md
240+ fi
241+
242+ # Add compare link if not first release
243+ if [ -n " $LATEST_TAG " ]; then
244+ echo " ## 📈 Stats" >> changelog.md
245+ echo " " >> changelog.md
246+ echo " **Full Changelog**: [\` $LATEST_TAG ..v${NEW_VERSION} \` ](${GITHUB_SERVER_URL} /${GITHUB_REPOSITORY} /compare/$LATEST_TAG ...v${NEW_VERSION} )" >> changelog.md
247+ fi
248+
249+ # Output the changelog content
250+ CHANGELOG_CONTENT= $( cat changelog.md)
251+ {
252+ echo " content<<EOF"
253+ echo " $CHANGELOG_CONTENT "
254+ echo " EOF"
255+ } >> " $GITHUB_OUTPUT "
256+
257+ # Also print to stdout for local testing
258+ echo " Generated changelog:"
259+ echo " ==================="
260+ cat changelog.md
261+ echo " ==================="
0 commit comments