Skip to content
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 71 additions & 167 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ jobs:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}

# Generate AI-powered changelog
# Generate changelog using git-cliff (fast, free, no API keys needed)
changelog:
needs: [check, bump-version, build]
if: |
Expand All @@ -224,185 +224,89 @@ jobs:
- name: Pull latest changes
run: git pull origin ${{ github.ref_name }} || true

- name: Get previous tag
id: prev_tag
- name: Install git-cliff
run: |
# Get the most recent tag before this release
PREV_TAG=$(git tag -l 'v*' --sort=-v:refname | head -2 | tail -1)
if [ -z "$PREV_TAG" ]; then
# No previous tag, use first commit
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREV_TAG"
# Install git-cliff (Rust-based changelog generator)
curl -sSfL https://github.com/orhun/git-cliff/releases/download/v2.7.0/git-cliff-2.7.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv git-cliff-2.7.0/git-cliff /usr/local/bin/
git-cliff --version

- name: Collect commits
id: commits
run: |
PREV_TAG="${{ steps.prev_tag.outputs.prev_tag }}"

# Get commits with conventional commit format parsing
echo "## Commits since $PREV_TAG" > commits.md
echo "" >> commits.md

# Group commits by type
git log "$PREV_TAG"..HEAD --pretty=format:"%s|%h|%an" | while IFS='|' read -r msg hash author; do
echo "$msg|$hash|$author"
done > raw_commits.txt

# Parse and categorize commits
echo "### ✨ Features" > features.md
echo "" >> features.md
echo "### 🐛 Bug Fixes" > fixes.md
echo "" >> fixes.md
echo "### 🔒 Security" > security.md
echo "" >> security.md
echo "### 📚 Documentation" > docs.md
echo "" >> docs.md
echo "### 🔧 Maintenance" > chores.md
echo "" >> chores.md
echo "### 🎨 Refactoring" > refactor.md
echo "" >> refactor.md
echo "### ⚡ Performance" > perf.md
echo "" >> perf.md
echo "### 🧪 Tests" > tests.md
echo "" >> tests.md
echo "### 📦 Other Changes" > other.md
echo "" >> other.md

while IFS='|' read -r msg hash author; do
# Extract type from conventional commit
if [[ "$msg" =~ ^feat(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> features.md
elif [[ "$msg" =~ ^fix(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> fixes.md
elif [[ "$msg" =~ ^security(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> security.md
elif [[ "$msg" =~ ^docs?(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> docs.md
elif [[ "$msg" =~ ^chore(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> chores.md
elif [[ "$msg" =~ ^refactor(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> refactor.md
elif [[ "$msg" =~ ^perf(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> perf.md
elif [[ "$msg" =~ ^test(\(.+\))?:\ (.+) ]]; then
scope="${BASH_REMATCH[1]}"
desc="${BASH_REMATCH[2]}"
echo "- ${desc} (\`${hash}\`) - @${author}" >> tests.md
else
# Other commits
echo "- ${msg} (\`${hash}\`) - @${author}" >> other.md
fi
done < raw_commits.txt

- name: Generate changelog with AI
- name: Generate changelog
id: generate
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
VERSION="${{ needs.check.outputs.version }}"

# Combine categorized commits
{
echo "# Release v${VERSION}"
echo ""
echo "Released on $(date '+%Y-%m-%d')"
echo ""

# Add non-empty sections
for file in features.md fixes.md security.md docs.md refactor.md perf.md tests.md chores.md other.md; do
if [ -f "$file" ] && [ $(wc -l < "$file") -gt 2 ]; then
cat "$file"
echo ""
fi
done
} > changelog_draft.md

# If OpenAI API key is available, enhance with AI
if [ -n "$OPENAI_API_KEY" ]; then
echo "Enhancing changelog with AI..."

# Prepare prompt for AI
COMMITS=$(cat raw_commits.txt | head -50)

# Call OpenAI API to generate summary
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d @- <<EOF
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a technical writer creating release notes for a software project. Generate a concise, professional summary of the changes. Focus on user-facing improvements and important technical changes. Use bullet points for clarity. Keep it under 200 words."
},
{
"role": "user",
"content": "Generate a release summary for version ${VERSION} based on these commits:\n\n${COMMITS}"
}
],
"max_tokens": 500,
"temperature": 0.7
}
EOF
)

# Extract the summary from the response
SUMMARY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty')

if [ -n "$SUMMARY" ]; then
{
echo "# Release v${VERSION}"
echo ""
echo "Released on $(date '+%Y-%m-%d')"
echo ""
echo "## 📋 Summary"
echo ""
echo "$SUMMARY"
echo ""
echo "---"
echo ""
echo "## 📝 Detailed Changes"
echo ""
# Add non-empty sections
for file in features.md fixes.md security.md docs.md refactor.md perf.md tests.md chores.md other.md; do
if [ -f "$file" ] && [ $(wc -l < "$file") -gt 2 ]; then
cat "$file"
echo ""
fi
done
} > changelog.md
else
echo "AI summary generation failed, using draft changelog"
cp changelog_draft.md changelog.md
fi
else
echo "No OpenAI API key, using standard changelog"
cp changelog_draft.md changelog.md
# Get previous tag for range
PREV_TAG=$(git tag -l 'v*' --sort=-v:refname | head -2 | tail -1)
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "Generating changelog from $PREV_TAG to HEAD"

# Create git-cliff config inline
cat > cliff.toml << 'CLIFF_CONFIG'
[changelog]
header = ""
body = """
# Release v{{ version }}

Released on {{ timestamp | date(format="%Y-%m-%d") }}

{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | split(pat="\n") | first | trim }} (`{{ commit.id | truncate(length=7, end="") }}`) - @{{ commit.author.name }}
{%- endfor %}
{% endfor %}
"""
footer = ""
trim = true

[git]
conventional_commits = true
filter_unconventional = false
split_commits = false
commit_parsers = [
{ message = "^feat", group = "✨ Features" },
{ message = "^fix", group = "🐛 Bug Fixes" },
{ message = "^security", group = "🔒 Security" },
{ message = "^doc", group = "📚 Documentation" },
{ message = "^perf", group = "⚡ Performance" },
{ message = "^refactor", group = "🎨 Refactoring" },
{ message = "^test", group = "🧪 Tests" },
{ message = "^chore", group = "🔧 Maintenance" },
{ message = "^ci", group = "🔧 Maintenance" },
{ message = "^build", group = "🔧 Maintenance" },
{ message = ".*", group = "📦 Other" },
]
filter_commits = false
tag_pattern = "v[0-9].*"
CLIFF_CONFIG

# Generate changelog for this release
git-cliff --config cliff.toml --tag "v${VERSION}" "${PREV_TAG}..HEAD" > changelog.md

# If git-cliff fails or produces empty output, fall back to simple format
if [ ! -s changelog.md ]; then
echo "git-cliff produced empty output, using fallback"
{
echo "# Release v${VERSION}"
echo ""
echo "Released on $(date '+%Y-%m-%d')"
echo ""
echo "### 📦 Changes"
echo ""
git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (\`%h\`) - @%an"
} > changelog.md
fi

# Output changelog for use in release
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

# Also save as artifact
# Display changelog
echo "=== Generated Changelog ==="
cat changelog.md

- name: Upload changelog artifact
Expand Down