Skip to content

Commit cf8666b

Browse files
committed
ci: Add workflow to auto-create release notes
1 parent 891b208 commit cf8666b

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

.github/workflows/auto-release.yml

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
name: "Release subfolder-specific notes via GitHub API"
2+
3+
on:
4+
push:
5+
tags:
6+
- '*/*' # matches tags containing '/', e.g. "component/v1.2.3"
7+
8+
permissions:
9+
contents: write # needed to create/update releases
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17+
with:
18+
fetch-depth: 0 # full history & tags
19+
20+
- name: Determine tag and subfolder
21+
id: parse
22+
run: |
23+
TAG_FULL="${GITHUB_REF##refs/tags/}" # e.g. "project-a/v1.2.3"
24+
echo "Tag full: $TAG_FULL"
25+
if [[ "$TAG_FULL" != */* ]]; then
26+
echo "ERROR: Tag '$TAG_FULL' does not contain '/'. Expected format 'folder/v<semver>'."
27+
exit 1
28+
fi
29+
SUBFOLDER="${TAG_FULL%%/*}"
30+
VERSION_PART="${TAG_FULL#*/}"
31+
# Validate version starts with 'v' followed by digit
32+
if [[ ! "$VERSION_PART" =~ ^v[0-9] ]]; then
33+
echo "ERROR: Version '$VERSION_PART' does not start with 'v' and digit(s) (e.g. v1.2.3)."
34+
exit 1
35+
fi
36+
echo "subfolder=$SUBFOLDER"
37+
echo "tag_full=$TAG_FULL"
38+
echo "version_part=$VERSION_PART"
39+
echo "subfolder=$SUBFOLDER" >> "$GITHUB_OUTPUT"
40+
echo "tag_full=$TAG_FULL" >> "$GITHUB_OUTPUT"
41+
echo "version_part=$VERSION_PART" >> "$GITHUB_OUTPUT"
42+
43+
- name: Find previous tag for this subfolder
44+
id: find_prev
45+
run: |
46+
TAG="${{ steps.parse.outputs.tag_full }}"
47+
SUBFOLDER="${{ steps.parse.outputs.subfolder }}"
48+
TAG_PATTERN="${SUBFOLDER}/v*"
49+
echo "Listing tags matching: $TAG_PATTERN"
50+
ALL_TAGS=$(git tag --list "$TAG_PATTERN")
51+
if [ -z "$ALL_TAGS" ]; then
52+
echo "No prior tags for $SUBFOLDER; first release"
53+
echo "prev_tag=" >> "$GITHUB_OUTPUT"
54+
exit 0
55+
fi
56+
# Semver-like sort; sort -V handles v-prefix:
57+
SORTED=$(echo "$ALL_TAGS" | sort -V)
58+
echo "All sorted tags for $SUBFOLDER:"
59+
echo "$SORTED"
60+
PREV_TAG=""
61+
for t in $SORTED; do
62+
if [ "$t" = "$TAG" ]; then
63+
break
64+
fi
65+
PREV_TAG="$t"
66+
done
67+
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$TAG" ]; then
68+
echo "No previous tag before $TAG"; echo "prev_tag=" >> "$GITHUB_OUTPUT"
69+
else
70+
echo "Previous tag: $PREV_TAG"
71+
echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
72+
fi
73+
74+
- name: Generate release notes body
75+
id: gen_notes
76+
env:
77+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78+
run: |
79+
TAG="${{ steps.parse.outputs.tag_full }}"
80+
SUBFOLDER="${{ steps.parse.outputs.subfolder }}"
81+
PREV_TAG="${{ steps.find_prev.outputs.prev_tag }}"
82+
if [ -z "$PREV_TAG" ]; then
83+
# First release: from root commit
84+
ROOT=$(git rev-list --max-parents=0 HEAD)
85+
RANGE="$ROOT..$TAG"
86+
else
87+
RANGE="$PREV_TAG..$TAG"
88+
fi
89+
echo "Computing git log for $SUBFOLDER/ in range $RANGE"
90+
# Format commits; customize as needed
91+
LOG=""
92+
# Process commits in current range
93+
for COMMIT in $(git log $RANGE --pretty=format:"%H" -- "$SUBFOLDER/"); do
94+
# Get commit metadata from Git first (as fallback)
95+
SHORT_SHA=$(echo "$COMMIT" | cut -c1-7)
96+
GIT_MESSAGE=$(git log -1 --pretty=format:"%s" "$COMMIT")
97+
GIT_AUTHOR=$(git log -1 --pretty=format:"%an" "$COMMIT")
98+
GIT_EMAIL=$(git log -1 --pretty=format:"%ae" "$COMMIT")
99+
100+
# Collect contributor info
101+
CONTRIBUTORS+=("$GIT_AUTHOR<$GIT_EMAIL>")
102+
103+
# Try to get enhanced metadata from GitHub API
104+
COMMIT_DATA=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
105+
"https://api.github.com/repos/${GITHUB_REPOSITORY}/commits/${COMMIT}")
106+
107+
# Check if the API response is valid JSON
108+
if echo "$COMMIT_DATA" | jq empty 2>/dev/null; then
109+
# Extract message (first line only) and escape problematic characters
110+
MESSAGE=$(echo "$COMMIT_DATA" | jq -r '.commit.message // empty' | head -n 1 | sed 's/"/\\"/g')
111+
112+
# If empty message from API, fall back to git
113+
if [ -z "$MESSAGE" ]; then
114+
MESSAGE="$GIT_MESSAGE"
115+
fi
116+
117+
# Extract GitHub username (if linked)
118+
AUTHOR_LOGIN=$(echo "$COMMIT_DATA" | jq -r '.author.login // empty')
119+
120+
if [ -z "$AUTHOR_LOGIN" ] || [ "$AUTHOR_LOGIN" = "null" ]; then
121+
AUTHOR_INFO="($GIT_AUTHOR)"
122+
else
123+
AUTHOR_INFO="(@$AUTHOR_LOGIN)"
124+
fi
125+
else
126+
# API failed, use git info
127+
MESSAGE="$GIT_MESSAGE"
128+
AUTHOR_INFO="($GIT_AUTHOR)"
129+
fi
130+
131+
# Append to changelog
132+
LOG+="- \`$SHORT_SHA\` $MESSAGE $AUTHOR_INFO"$'\n'
133+
done
134+
135+
if [ -z "$LOG" ]; then
136+
BODY="No changes in \`$SUBFOLDER/\` since last release."
137+
else
138+
PREV_LABEL=${PREV_TAG:-"beginning"}
139+
140+
# Create comparison URL for full changelog
141+
if [ -n "$PREV_TAG" ]; then
142+
COMPARE_URL="https://github.com/${GITHUB_REPOSITORY}/compare/${PREV_TAG}...${TAG}"
143+
CHANGELOG_LINK="**Full Changelog**: ${COMPARE_URL}"
144+
else
145+
CHANGELOG_LINK=""
146+
fi
147+
148+
# Add changelog link to the body if available
149+
if [ -n "$CHANGELOG_LINK" ]; then
150+
BODY="## Changes in \`$SUBFOLDER\` since \`${PREV_LABEL}\` to \`${TAG}\`:\n\n${LOG}\n\n${CHANGELOG_LINK}"
151+
else
152+
BODY="## Changes in \`$SUBFOLDER\` since \`${PREV_LABEL}\` to \`${TAG}\`:\n\n${LOG}"
153+
fi
154+
fi
155+
# Expose multi-line
156+
echo -e "body<<EOF\n$BODY\nEOF"
157+
echo -e "body<<EOF\n$BODY\nEOF" >> "$GITHUB_OUTPUT"
158+
159+
- name: Create or update release via REST API
160+
id: api_release
161+
env:
162+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163+
run: |
164+
OWNER_REPO="${GITHUB_REPOSITORY}" # e.g. "owner/repo"
165+
TAG="${{ steps.parse.outputs.tag_full }}"
166+
BODY='${{ steps.gen_notes.outputs.body }}'
167+
echo "$OWNER_REPO"
168+
echo "$TAG"
169+
echo "$BODY"
170+
# 1. Try to get existing release by tag
171+
echo "Checking for existing release for tag $TAG..."
172+
RESPONSE=$(curl -s -o response.json -w "%{http_code}" \
173+
-H "Accept: application/vnd.github+json" \
174+
-H "Authorization: Bearer $GITHUB_TOKEN" \
175+
"https://api.github.com/repos/${OWNER_REPO}/releases/tags/${TAG}")
176+
if [ "$RESPONSE" = "200" ]; then
177+
# Release exists; parse its id and update
178+
RELEASE_ID=$(jq -r .id response.json)
179+
echo "Found existing release with ID $RELEASE_ID; updating body..."
180+
# PATCH to update
181+
curl -s -X PATCH \
182+
-H "Accept: application/vnd.github+json" \
183+
-H "Authorization: Bearer $GITHUB_TOKEN" \
184+
"https://api.github.com/repos/${OWNER_REPO}/releases/${RELEASE_ID}" \
185+
-d "$(jq -n --arg body "$BODY" --arg name "$TAG" '{body: $body, name: $name}')"
186+
echo "Release updated."
187+
elif [ "$RESPONSE" = "404" ]; then
188+
# No existing release; create new
189+
echo "No existing release; creating new release for tag $TAG..."
190+
curl -s -X POST \
191+
-H "Accept: application/vnd.github+json" \
192+
-H "Authorization: Bearer $GITHUB_TOKEN" \
193+
"https://api.github.com/repos/${OWNER_REPO}/releases" \
194+
-d "$(jq -n --arg tag_name "$TAG" --arg name "$TAG" --arg body "$BODY" '{tag_name: $tag_name, name: $name, body: $body, draft: false, prerelease: false}')"
195+
echo "Release created."
196+
else
197+
echo "Unexpected response when checking release: HTTP $RESPONSE"
198+
cat response.json
199+
exit 1
200+
fi

0 commit comments

Comments
 (0)