feat: streamline GitHub Actions workflows and improve contributor experience

- Replace complex auto-version.yml with simple PR Docker build workflow
  - Builds Docker images for all PRs using pr-number-sha tagging
  - Removes redundant versioning logic (semantic-release handles this)
  - Adds automatic PR comments with Docker usage instructions

- Optimize test.yml workflow triggers
  - Remove redundant push triggers on main branch
  - Focus on PR testing only for better developer feedback

- Add docker-release.yml for production releases
  - Triggers on GitHub release publication
  - Multi-platform builds (linux/amd64, linux/arm64)
  - Updates release notes with Docker installation instructions

- Add semantic-release.yml workflow for automated versioning
  - Uses conventional commits for version bumping
  - Automatically generates releases and tags
  - Integrates with Docker workflow via release triggers

- Add pre-commit configuration for automatic code quality
  - Includes ruff (with auto-fix), black, isort
  - Provides faster development workflow option

- Enhance contribution documentation
  - Add pre-commit hook option as recommended approach
  - Keep manual script option for comprehensive testing
  - Improve developer workflow guidance

Fixes #215 (automatic changelog generation)
Addresses #110 (Docker builds automation)
References #107 (improved version tracking)

This creates a clean, modern CI/CD pipeline that eliminates redundancy
while addressing multiple community requests around changelog generation,
Docker builds, and release automation.
This commit is contained in:
Sven Lito
2025-08-08 17:33:51 +07:00
parent e29deb23db
commit af3a81543c
8 changed files with 397 additions and 283 deletions

View File

@@ -1,248 +1,111 @@
name: Auto Version
name: PR Docker Build
on:
pull_request:
types: [closed]
branches: [main]
types: [opened, synchronize, reopened]
permissions:
contents: read
packages: write
pull-requests: write
jobs:
version:
# Only run if PR was merged (not just closed)
if: github.event.pull_request.merged == true
docker:
name: Build PR Docker Image
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout repository
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
fetch-depth: 0
token: ${{ secrets.PAT }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: actions/setup-python@v4
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
python-version: '3.9'
images: ghcr.io/${{ github.repository }}
tags: |
# PR-specific tag for testing
type=raw,value=pr-${{ github.event.number }}-${{ github.sha }}
type=raw,value=pr-${{ github.event.number }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Determine version bump type
id: bump_type
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
echo "PR Title: $PR_TITLE"
# Convert to lowercase for case-insensitive matching
PR_TITLE_LOWER=$(echo "$PR_TITLE" | tr '[:upper:]' '[:lower:]')
# Determine bump type based on PR title prefix
if [[ "$PR_TITLE_LOWER" =~ ^(breaking|breaking[[:space:]]change): ]]; then
echo "Detected BREAKING CHANGE - major version bump"
echo "bump_type=major" >> $GITHUB_OUTPUT
echo "should_bump=true" >> $GITHUB_OUTPUT
echo "should_build_docker=true" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE_LOWER" =~ ^feat: ]]; then
echo "Detected new feature - minor version bump"
echo "bump_type=minor" >> $GITHUB_OUTPUT
echo "should_bump=true" >> $GITHUB_OUTPUT
echo "should_build_docker=true" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE_LOWER" =~ ^(fix|perf|refactor): ]]; then
echo "Detected fix/perf/refactor - patch version bump"
echo "bump_type=patch" >> $GITHUB_OUTPUT
echo "should_bump=true" >> $GITHUB_OUTPUT
echo "should_build_docker=true" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE_LOWER" =~ ^docker: ]]; then
echo "Detected docker build request - no version bump but build Docker"
echo "bump_type=none" >> $GITHUB_OUTPUT
echo "should_bump=false" >> $GITHUB_OUTPUT
echo "should_build_docker=true" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE_LOWER" =~ ^(docs|chore|test|ci|style)\+docker: ]]; then
echo "Detected non-versioned change with Docker build request"
echo "bump_type=none" >> $GITHUB_OUTPUT
echo "should_bump=false" >> $GITHUB_OUTPUT
echo "should_build_docker=true" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE_LOWER" =~ ^(docs|chore|test|ci|style): ]]; then
echo "Detected non-versioned change - no version bump"
echo "bump_type=none" >> $GITHUB_OUTPUT
echo "should_bump=false" >> $GITHUB_OUTPUT
echo "should_build_docker=false" >> $GITHUB_OUTPUT
else
echo "No recognized prefix - no version bump"
echo "bump_type=none" >> $GITHUB_OUTPUT
echo "should_bump=false" >> $GITHUB_OUTPUT
echo "should_build_docker=false" >> $GITHUB_OUTPUT
fi
- name: Get current version
if: steps.bump_type.outputs.should_bump == 'true'
id: current_version
run: |
CURRENT_VERSION=$(python -c "from config import __version__; print(__version__)")
echo "Current version: $CURRENT_VERSION"
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Bump version
if: steps.bump_type.outputs.should_bump == 'true'
id: new_version
run: |
python scripts/bump_version.py ${{ steps.bump_type.outputs.bump_type }}
NEW_VERSION=$(python -c "from config import __version__; print(__version__)")
echo "New version: $NEW_VERSION"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Commit version change
if: steps.bump_type.outputs.should_bump == 'true'
run: |
git add config.py
git commit -m "chore: bump version to ${{ steps.new_version.outputs.version }}
Automated version bump from PR #${{ github.event.pull_request.number }}
${{ github.event.pull_request.title }}
Co-authored-by: ${{ github.event.pull_request.user.login }} <${{ github.event.pull_request.user.id }}+${{ github.event.pull_request.user.login }}@users.noreply.github.com>"
git push
- name: Create git tag
if: steps.bump_type.outputs.should_bump == 'true'
run: |
git tag -a "v${{ steps.new_version.outputs.version }}" -m "Release v${{ steps.new_version.outputs.version }}
Changes in this release:
- ${{ github.event.pull_request.title }}
PR: #${{ github.event.pull_request.number }}
Author: @${{ github.event.pull_request.user.login }}"
git push origin "v${{ steps.new_version.outputs.version }}"
- name: Generate release notes
if: steps.bump_type.outputs.should_bump == 'true'
id: release_notes
run: |
# Extract PR body for release notes
PR_BODY=$(cat << 'EOF'
${{ github.event.pull_request.body }}
EOF
)
# Create release notes
RELEASE_NOTES=$(cat << EOF
## What's Changed
${{ github.event.pull_request.title }} by @${{ github.event.pull_request.user.login }} in #${{ github.event.pull_request.number }}
### Details
$PR_BODY
### Version Info
- Previous version: ${{ steps.current_version.outputs.version }}
- New version: ${{ steps.new_version.outputs.version }}
- Bump type: ${{ steps.bump_type.outputs.bump_type }}
**Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ steps.current_version.outputs.version }}...v${{ steps.new_version.outputs.version }}
EOF
)
# Save to file for GitHub release
echo "$RELEASE_NOTES" > release_notes.md
- name: Create GitHub release
if: steps.bump_type.outputs.should_bump == 'true'
uses: softprops/action-gh-release@v1
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
tag_name: v${{ steps.new_version.outputs.version }}
name: Release v${{ steps.new_version.outputs.version }}
body_path: release_notes.md
draft: false
prerelease: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trigger Docker build
if: steps.bump_type.outputs.should_build_docker == 'true'
run: |
echo "🐳 Triggering Docker build and publish workflow"
# The Docker workflow will be triggered by the tag creation (if version bumped)
# or by repository_dispatch (if docker: prefix without version bump)
if [ "${{ steps.bump_type.outputs.should_bump }}" == "false" ]; then
# For docker: prefix without version bump, trigger via repository_dispatch
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/dispatches" \
-d '{"event_type":"docker-build","client_payload":{"pr_number":"${{ github.event.pull_request.number }}","pr_title":"${{ github.event.pull_request.title }}","commit_sha":"${{ github.sha }}"}}'
- name: Comment on PR with Docker info
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const sha = context.sha.substring(0, 7);
const repo = context.repo.repo;
const owner = context.repo.owner;
# Add comment to PR about Docker build
COMMENT_BODY="🐳 **Docker Image Build Triggered**
const comment = `🐳 **Docker Image Built Successfully**
This PR triggered a Docker image build because of the \`+docker\` suffix in the title.
This PR has been built and pushed as a Docker image for testing:
**Expected Image Tags:**
- \`ghcr.io/${{ github.repository_owner }}/zen-mcp-server:pr-${{ github.event.pull_request.number }}\`
- \`ghcr.io/${{ github.repository_owner }}/zen-mcp-server:main-${{ github.sha }}\`
**Available Tags:**
- \`ghcr.io/${owner}/${repo}:pr-${prNumber}-${context.sha}\`
- \`ghcr.io/${owner}/${repo}:pr-${prNumber}\` (latest for this PR)
**To test the image after build completes:**
**Test the changes:**
\`\`\`bash
docker pull ghcr.io/${{ github.repository_owner }}/zen-mcp-server:pr-${{ github.event.pull_request.number }}
docker pull ghcr.io/${owner}/${repo}:pr-${prNumber}
\`\`\`
**Claude Desktop config for testing:**
**Claude Desktop configuration:**
\`\`\`json
{
\"mcpServers\": {
\"gemini\": {
\"command\": \"docker\",
\"args\": [
\"run\", \"--rm\", \"-i\",
\"-e\", \"GEMINI_API_KEY\",
\"ghcr.io/${{ github.repository_owner }}/zen-mcp-server:pr-${{ github.event.pull_request.number }}\"
"mcpServers": {
"zen-mcp-server": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "GEMINI_API_KEY",
"ghcr.io/${owner}/${repo}:pr-${prNumber}"
],
\"env\": {
\"GEMINI_API_KEY\": \"your-api-key-here\"
"env": {
"GEMINI_API_KEY": "your-api-key-here"
}
}
}
}
\`\`\`
View the build progress in the [Actions tab](https://github.com/${{ github.repository }}/actions)."
The image will be updated automatically when you push new commits to this PR.`;
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
-d "{\"body\":\"$COMMENT_BODY\"}"
fi
github.rest.issues.createComment({
issue_number: prNumber,
owner: owner,
repo: repo,
body: comment
});
- name: Summary
- name: Create deployment summary
run: |
if [ "${{ steps.bump_type.outputs.should_bump }}" == "true" ]; then
echo "### ✅ Version Bumped Successfully" >> $GITHUB_STEP_SUMMARY
echo "## 🐳 PR Docker Build Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Previous version**: ${{ steps.current_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **New version**: ${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Bump type**: ${{ steps.bump_type.outputs.bump_type }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag**: v${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **PR**: #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
echo "- **Docker**: Will build and publish with new tag" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.bump_type.outputs.should_build_docker }}" == "true" ]; then
echo "### 🐳 Docker Build Requested" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "No version bump but Docker image will be built and published." >> $GITHUB_STEP_SUMMARY
echo "- **PR**: #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
echo "- **Title**: ${{ github.event.pull_request.title }}" >> $GITHUB_STEP_SUMMARY
echo "- **Docker tag**: Based on commit SHA" >> $GITHUB_STEP_SUMMARY
else
echo "### No Version Bump Required" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "PR title prefix did not require a version bump." >> $GITHUB_STEP_SUMMARY
echo "- **PR**: #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
echo "- **Title**: ${{ github.event.pull_request.title }}" >> $GITHUB_STEP_SUMMARY
fi
echo "**PR**: #${{ github.event.number }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "**Images built:**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY

116
.github/workflows/docker-release.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Docker Release Build
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Tag to build (leave empty for latest release)'
required: false
type: string
permissions:
contents: read
packages: write
jobs:
docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# If triggered by workflow_dispatch with a tag, checkout that tag
ref: ${{ inputs.tag || github.event.release.tag_name }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
# Tag with the release version
type=semver,pattern={{version}},value=${{ inputs.tag || github.event.release.tag_name }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag || github.event.release.tag_name }}
type=semver,pattern={{major}},value=${{ inputs.tag || github.event.release.tag_name }}
# Also tag as latest for the most recent release
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Update release with Docker info
if: github.event_name == 'release'
run: |
RELEASE_TAG="${{ github.event.release.tag_name }}"
DOCKER_TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ')
# Add Docker information to the release
gh release edit "$RELEASE_TAG" --notes-file - << EOF
${{ github.event.release.body }}
---
## 🐳 Docker Images
This release is available as Docker images:
$(echo "$DOCKER_TAGS" | sed 's/ghcr.io/- `ghcr.io/g' | sed 's/ /`\n/g')
**Quick start with Docker:**
\`\`\`bash
docker pull ghcr.io/${{ github.repository }}:$RELEASE_TAG
\`\`\`
**Claude Desktop configuration:**
\`\`\`json
{
"mcpServers": {
"zen-mcp-server": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "GEMINI_API_KEY",
"ghcr.io/${{ github.repository }}:$RELEASE_TAG"
],
"env": {
"GEMINI_API_KEY": "your-api-key-here"
}
}
}
}
\`\`\`
EOF
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create deployment summary
run: |
echo "## 🐳 Docker Release Build Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Release**: ${{ inputs.tag || github.event.release.tag_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Images built:**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY

61
.github/workflows/semantic-release.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Semantic Release
on:
push:
branches:
- main
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
concurrency: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: true
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install python-semantic-release
- name: Verify tests pass
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
python -m pytest tests/ -v --ignore=simulator_tests/ -m "not integration"
- name: Run semantic release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
semantic-release version
semantic-release publish
- name: Upload build artifacts to release
if: hashFiles('dist/*') != ''
run: |
# Get the latest release tag
LATEST_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName')
if [ ! -z "$LATEST_TAG" ]; then
echo "Uploading artifacts to release $LATEST_TAG"
gh release upload "$LATEST_TAG" dist/* --clobber
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,8 +1,6 @@
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [main]
@@ -57,4 +55,3 @@ jobs:
- name: Run ruff linter
run: ruff check . --exclude test_simulation_files

40
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,40 @@
---
default_stages: [pre-commit, pre-push]
repos:
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
- id: black
exclude: ^test_simulation_files/
- repo: https://github.com/pycqa/isort
rev: 6.0.1
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.8
hooks:
- id: ruff
args: [--fix]
exclude: ^test_simulation_files/
# Configuration for specific tools
default_language_version:
python: python3
# Exclude patterns
exclude: |
(?x)^(
\.git/|
\.venv/|
venv/|
\.zen_venv/|
__pycache__/|
\.pytest_cache/|
logs/|
dist/|
build/|
test_simulation_files/
)$

View File

@@ -23,8 +23,16 @@ We maintain high code quality standards. **All contributions must pass our autom
#### Required Code Quality Checks
Before submitting any PR, run our automated quality check script:
**Option 1 - Automated (Recommended):**
```bash
# Install pre-commit hooks (one-time setup)
pre-commit install
# Now linting runs automatically on every commit
# Includes: ruff (with auto-fix), black, isort
```
**Option 2 - Manual:**
```bash
# Run the comprehensive quality checks script
./code_quality_checks.sh

View File

@@ -1,6 +1,6 @@
[project]
name = "zen-mcp-server"
version = "0.1.0"
version = "1.1.0"
description = "AI-powered MCP server with multiple model providers"
requires-python = ">=3.9"
dependencies = [
@@ -83,6 +83,33 @@ ignore = [
"tests/*" = ["B011"]
"tests/conftest.py" = ["E402"] # Module level imports not at top of file - needed for test setup
[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
branch = "main"
build_command = "python -m pip install --upgrade build && python -m build"
dist_path = "dist/"
upload_to_vcs_release = true
upload_to_repository = false
remove_dist = false
commit_version_number = true
commit_message = "chore(release): {version}\n\nAutomatically generated by python-semantic-release"
tag_format = "v{version}"
[tool.semantic_release.branches.main]
match = "main"
prerelease = false
[tool.semantic_release.changelog]
exclude_commit_patterns = []
[tool.semantic_release.commit_parser_options]
allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"]
minor_tags = ["feat"]
patch_tags = ["fix", "perf"]
[tool.semantic_release.remote.token]
env = "GH_TOKEN"
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"

View File

@@ -4,3 +4,5 @@ pytest-mock>=3.11.0
black>=23.0.0
ruff>=0.1.0
isort>=5.12.0
python-semantic-release>=10.3.0
build>=1.0.0