Skip to content
Open
Changes from all 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
268 changes: 103 additions & 165 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,123 @@ 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 v2.11.0 (Rust-based changelog generator)
# Using latest version to address CVE-2024-32650 in rustls dependency
VERSION="2.11.0"
TARBALL="git-cliff-${VERSION}-x86_64-unknown-linux-gnu.tar.gz"
DOWNLOAD_URL="https://github.com/orhun/git-cliff/releases/download/v${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
# Download the tarball and checksum
curl -sSfL "${DOWNLOAD_URL}/${TARBALL}" -o "${TARBALL}"
curl -sSfL "${DOWNLOAD_URL}/${TARBALL}.sha512" -o "${TARBALL}.sha512"

# Verify checksum before installation
echo "Verifying checksum..."
sha512sum -c "${TARBALL}.sha512"

# Extract and install
tar xzf "${TARBALL}"
sudo mv "git-cliff-${VERSION}/git-cliff" /usr/local/bin/

# Cleanup
rm -rf "${TARBALL}" "${TARBALL}.sha512" "git-cliff-${VERSION}"

- name: Generate changelog with AI
git-cliff --version

- 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
# 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 with proper error handling
# Capture exit code to distinguish between failure and empty output
CLIFF_EXIT_CODE=0
git-cliff --config cliff.toml --tag "v${VERSION}" "${PREV_TAG}..HEAD" > changelog.md 2>&1 || CLIFF_EXIT_CODE=$?

# Check if git-cliff failed (non-zero exit code)
if [ $CLIFF_EXIT_CODE -ne 0 ]; then
echo "git-cliff failed with exit code $CLIFF_EXIT_CODE, 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
# Check if git-cliff succeeded but produced empty output
elif [ ! -s changelog.md ]; then
echo "git-cliff produced empty output (no commits in range), 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
else
echo "No OpenAI API key, using standard changelog"
cp changelog_draft.md changelog.md
echo "git-cliff succeeded with valid output"
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