Skip to content
Open
Show file tree
Hide file tree
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
284 changes: 117 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,135 @@ 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- 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
# Previous release = newest v* tag strictly older (by version order)
# than the one being released. The new tag does not exist yet on
# workflow_run/workflow_dispatch and does on a tag push; either way it
# is not its own predecessor, and a re-release of an older version never
# picks a newer tag.
PREV_TAG=$( (git tag -l 'v*'; echo "v${VERSION}") | sort -uV |
awk -v target="v${VERSION}" '$0 == target { exit } { prev = $0 } END { print prev }')
if [ -n "$PREV_TAG" ]; then
RANGE="${PREV_TAG}..HEAD"
else
# No previous release: leave RANGE empty to cover the full history
RANGE=""
fi
echo "Generating changelog for ${RANGE:-full history}"

# Create git-cliff config inline
cat > cliff.toml << 'CLIFF_CONFIG'
[changelog]
header = ""
body = """
# Release {{ 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: bump version", skip = true },
{ message = "^Merge ", skip = true },
{ 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}" ${RANGE:+"$RANGE"} > changelog.md 2> cliff.err || CLIFF_EXIT_CODE=$?
cat cliff.err >&2

# 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 "${RANGE:-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 "${RANGE:-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
DELIM="CHANGELOG_$(openssl rand -hex 8)"
echo "changelog<<${DELIM}" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "${DELIM}" >> $GITHUB_OUTPUT

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

- name: Upload changelog artifact
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::error::Result;
use crate::mcp::protocol::{Tool, ToolResult};

/// Handler for MCP tool calls.
// async_trait expands to boxed futures, which trips clippy::double_must_use on newer stable.
#[allow(clippy::double_must_use)]
#[async_trait]
pub trait ToolHandler: Send + Sync {
/// Get the tool definition.
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum Message {
}

/// Transport trait for MCP communication.
// async_trait expands to boxed futures, which trips clippy::double_must_use on newer stable.
#[allow(clippy::double_must_use)]
#[async_trait]
pub trait Transport: Send + Sync {
/// Start the transport, returning channels for messages.
Expand Down
Loading
Loading