1+ name : Create Release
2+
3+ on :
4+ workflow_dispatch :
5+ inputs :
6+ version :
7+ description : ' Release version (e.g., v1.2.3)'
8+ required : true
9+ type : string
10+ previous_version :
11+ description : ' Previous version for changelog (leave empty for auto-detect)'
12+ required : false
13+ type : string
14+
15+ jobs :
16+ create-release :
17+ runs-on : ubuntu-latest
18+ permissions :
19+ contents : write
20+
21+ steps :
22+ - name : Checkout repository
23+ uses : actions/checkout@v4
24+ with :
25+ fetch-depth : 0 # Get full history for changelog generation
26+ token : ${{ secrets.GITHUB_TOKEN }}
27+
28+ - name : Validate version format
29+ run : |
30+ VERSION="${{ github.event.inputs.version }}"
31+ if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
32+ echo "❌ Invalid version format. Use format: v1.2.3"
33+ exit 1
34+ fi
35+ echo "VERSION=${VERSION}" >> $GITHUB_ENV
36+ echo "VERSION_NUMBER=${VERSION#v}" >> $GITHUB_ENV
37+
38+ - name : Check if tag already exists
39+ run : |
40+ if git tag -l "${{ env.VERSION }}" | grep -q "${{ env.VERSION }}"; then
41+ echo "❌ Tag ${{ env.VERSION }} already exists!"
42+ exit 1
43+ fi
44+
45+ - name : Get previous tag for changelog
46+ run : |
47+ if [ -n "${{ github.event.inputs.previous_version }}" ]; then
48+ PREVIOUS_TAG="${{ github.event.inputs.previous_version }}"
49+ else
50+ PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
51+ fi
52+
53+ if [ -z "$PREVIOUS_TAG" ]; then
54+ echo "PREVIOUS_TAG=initial" >> $GITHUB_ENV
55+ else
56+ echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV
57+ fi
58+ echo "📝 Previous tag: ${PREVIOUS_TAG:-'none (first release)'}"
59+
60+ - name : Generate changelog from merged PRs and commits
61+ run : |
62+ if [ "${{ env.PREVIOUS_TAG }}" = "initial" ]; then
63+ # For first release, get all commits since beginning
64+ COMMIT_RANGE=""
65+ else
66+ # Get commits since previous tag
67+ COMMIT_RANGE="${{ env.PREVIOUS_TAG }}..HEAD"
68+ fi
69+
70+ # Create changelog
71+ {
72+ echo "CHANGELOG<<EOF"
73+ echo "## What's Changed"
74+ echo ""
75+
76+ # Get commits in chronological order
77+ if [ -z "$COMMIT_RANGE" ]; then
78+ COMMITS=$(git log --pretty=format:"%H|%an|%s" --reverse)
79+ else
80+ COMMITS=$(git log $COMMIT_RANGE --pretty=format:"%H|%an|%s" --reverse)
81+ fi
82+
83+ # Track if we found any changes
84+ FOUND_CHANGES=false
85+
86+ # Process each commit
87+ echo "$COMMITS" | while IFS='|' read -r COMMIT_HASH AUTHOR SUBJECT; do
88+ # Skip empty lines
89+ [ -z "$COMMIT_HASH" ] && continue
90+
91+ # Handle PR merge commits
92+ if [[ $SUBJECT =~ ^Merge\ pull\ request\ #([0-9]+)\ from\ (.+) ]]; then
93+ PR_NUMBER="${BASH_REMATCH[1]}"
94+ BRANCH_INFO="${BASH_REMATCH[2]}"
95+
96+ # Get PR title from GitHub API
97+ PR_TITLE=$(curl -s \
98+ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
99+ -H "Accept: application/vnd.github.v3+json" \
100+ "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" \
101+ | jq -r '.title // empty')
102+
103+ # Get PR author from GitHub API
104+ PR_AUTHOR=$(curl -s \
105+ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
106+ -H "Accept: application/vnd.github.v3+json" \
107+ "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" \
108+ | jq -r '.user.login // empty')
109+
110+ # Use PR title if available, otherwise use branch name
111+ if [ -n "$PR_TITLE" ] && [ "$PR_TITLE" != "null" ]; then
112+ TITLE="$PR_TITLE"
113+ else
114+ # Clean up branch name for display
115+ TITLE=$(echo "$BRANCH_INFO" | sed 's|.*/||' | sed 's|-| |g' | sed 's/_/ /g')
116+ fi
117+
118+ # Use PR author if available, otherwise use commit author
119+ if [ -n "$PR_AUTHOR" ] && [ "$PR_AUTHOR" != "null" ]; then
120+ DISPLAY_AUTHOR="@$PR_AUTHOR"
121+ else
122+ DISPLAY_AUTHOR="@$AUTHOR"
123+ fi
124+
125+ echo "* ${TITLE} by ${DISPLAY_AUTHOR} in https://github.com/${{ github.repository }}/pull/${PR_NUMBER}"
126+ FOUND_CHANGES=true
127+
128+ # Handle direct branch merges
129+ elif [[ $SUBJECT =~ ^Merge\ branch\ \'([^\']+)\' ]]; then
130+ BRANCH_NAME="${BASH_REMATCH[1]}"
131+
132+ # Clean up branch name for display
133+ CLEAN_BRANCH=$(echo "$BRANCH_NAME" | sed 's|-| |g' | sed 's/_/ /g')
134+
135+ echo "* Merge branch '${BRANCH_NAME}' by @${AUTHOR}"
136+ FOUND_CHANGES=true
137+
138+ # Handle direct commits (non-merge commits that might be significant)
139+ elif [[ ! $SUBJECT =~ ^Merge ]]; then
140+ # Skip version update commits and other automated commits
141+ if [[ ! $SUBJECT =~ ^Update\ version\.py ]] && [[ ! $SUBJECT =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ ! $AUTHOR =~ github-actions ]]; then
142+ # Only include if it looks like a meaningful change
143+ if [[ $SUBJECT =~ ^(feat|fix|add|update|improve|remove|refactor): ]] || [[ ${#SUBJECT} -gt 10 ]]; then
144+ echo "* ${SUBJECT} by @${AUTHOR} in ${COMMIT_HASH:0:7}"
145+ FOUND_CHANGES=true
146+ fi
147+ fi
148+ fi
149+ done
150+
151+ # If no changes found, add default message
152+ if [ "$FOUND_CHANGES" = false ]; then
153+ echo "* Other minor updates and improvements"
154+ fi
155+
156+ echo ""
157+ if [ "${{ env.PREVIOUS_TAG }}" != "initial" ]; then
158+ echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ env.PREVIOUS_TAG }}...${{ env.VERSION }}"
159+ else
160+ echo "**Full Changelog**: https://github.com/${{ github.repository }}/commits/${{ env.VERSION }}"
161+ fi
162+ echo "EOF"
163+ } >> $GITHUB_ENV
164+
165+ - name : Update version.py
166+ run : |
167+ TIMESTAMP=$(date +"%Y-%m-%d")
168+ VERSION_FILE="data/version.py"
169+
170+ # Create version.py if it doesn't exist
171+ if [ ! -f "$VERSION_FILE" ]; then
172+ echo "__version__ = \"0.0.0\"" > "$VERSION_FILE"
173+ echo "" >> "$VERSION_FILE"
174+ fi
175+
176+ # Prepend new release info
177+ TEMP_FILE=$(mktemp)
178+ echo "__version__ = \"${{ env.VERSION }}\"" > "$TEMP_FILE"
179+ echo "" >> "$TEMP_FILE"
180+ echo "\"\"\"" >> "$TEMP_FILE"
181+ echo "Release Notes for version ${{ env.VERSION }} ($TIMESTAMP):" >> "$TEMP_FILE"
182+ echo "" >> "$TEMP_FILE"
183+ echo "${{ env.CHANGELOG }}" | sed 's/^/# /' >> "$TEMP_FILE"
184+ echo "\"\"\"" >> "$TEMP_FILE"
185+ echo "" >> "$TEMP_FILE"
186+
187+ # Skip the first line of existing version.py (old __version__)
188+ if [ -f "$VERSION_FILE" ]; then
189+ tail -n +2 "$VERSION_FILE" >> "$TEMP_FILE"
190+ fi
191+ mv "$TEMP_FILE" "$VERSION_FILE"
192+
193+ - name : Commit version update
194+ run : |
195+ git config --global user.name "github-actions[bot]"
196+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
197+ git add data/version.py
198+
199+ # Check if there are changes to commit
200+ if git diff --staged --quiet; then
201+ echo "No changes to commit"
202+ else
203+ git commit -m "${{ env.VERSION }}"
204+ echo "✅ Committed version.py update with message: ${{ env.VERSION }}"
205+ fi
206+
207+ - name : Create and push tag
208+ run : |
209+ # Create the tag on the commit that includes the version update
210+ git tag -a "${{ env.VERSION }}" -m "Release ${{ env.VERSION }}"
211+
212+ # Push the commit and tag
213+ git push origin HEAD:${{ github.event.repository.default_branch }}
214+ git push origin "${{ env.VERSION }}"
215+
216+ echo "✅ Created and pushed tag: ${{ env.VERSION }}"
217+
218+ - name : Create GitHub Release
219+ uses : softprops/action-gh-release@v1
220+ with :
221+ tag_name : ${{ env.VERSION }}
222+ name : " Release ${{ env.VERSION }}"
223+ body : ${{ env.CHANGELOG }}
224+ draft : false
225+ prerelease : false
226+ generate_release_notes : false # Use only our custom changelog
227+ env :
228+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
229+
230+ - name : Output release information
231+ run : |
232+ echo "✅ Release ${{ env.VERSION }} created successfully!"
233+ echo "📝 Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ env.VERSION }}"
234+ echo "📄 Version file updated and committed with tag: ${{ env.VERSION }}"
235+ echo "🏷️ Tag includes the version commit"
0 commit comments