diff --git a/.dockerignore b/.dockerignore index 488723c..5199434 100644 --- a/.dockerignore +++ b/.dockerignore @@ -50,7 +50,6 @@ pytest.ini .env .env.local examples/ -scripts/bump_version.py code_quality_checks.sh run_integration_tests.sh diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0b8a086..0e46604 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,28 +1,35 @@ ## PR Title Format -**Please ensure your PR title follows one of these formats:** +**Please ensure your PR title follows [Conventional Commits](https://www.conventionalcommits.org/) format:** -### Version Bumping Prefixes (trigger Docker build + version bump): -- `feat: ` - New features (triggers MINOR version bump) -- `fix: ` - Bug fixes (triggers PATCH version bump) -- `breaking: ` or `BREAKING CHANGE: ` - Breaking changes (triggers MAJOR version bump) -- `perf: ` - Performance improvements (triggers PATCH version bump) -- `refactor: ` - Code refactoring (triggers PATCH version bump) +### Version Bumping Types (trigger semantic release): +- `feat: ` - New features → **MINOR** version bump (1.1.0 → 1.2.0) +- `fix: ` - Bug fixes → **PATCH** version bump (1.1.0 → 1.1.1) +- `perf: ` - Performance improvements → **PATCH** version bump (1.1.0 → 1.1.1) -### Non-Version Prefixes (no version bump): -- `docs: ` - Documentation only +### Breaking Changes (trigger MAJOR version bump): +For breaking changes, use any commit type above with `BREAKING CHANGE:` in the commit body or `!` after the type: +- `feat!: ` → **MAJOR** version bump (1.1.0 → 2.0.0) +- `fix!: ` → **MAJOR** version bump (1.1.0 → 2.0.0) + +### Non-Versioning Types (no release): +- `build: ` - Build system changes - `chore: ` - Maintenance tasks -- `test: ` - Test additions/changes - `ci: ` - CI/CD changes -- `style: ` - Code style changes +- `docs: ` - Documentation only +- `refactor: ` - Code refactoring (no functional changes) +- `style: ` - Code style/formatting changes +- `test: ` - Test additions/changes -### Docker Build Options: -- `docker: ` - Force Docker build without version bump -- `docs+docker: ` - Documentation + Docker build -- `chore+docker: ` - Maintenance + Docker build -- `test+docker: ` - Tests + Docker build -- `ci+docker: ` - CI changes + Docker build -- `style+docker: ` - Style changes + Docker build +### Docker Build Triggering: + +Docker builds are **independent** of versioning and trigger based on: + +**Automatic**: When PRs modify relevant files: +- Python files (`*.py`), `requirements*.txt`, `pyproject.toml` +- Docker files (`Dockerfile`, `docker-compose.yml`, `.dockerignore`) + +**Manual**: Add the `docker-build` label to force builds for any PR. ## Description diff --git a/.github/workflows/auto-version.yml b/.github/workflows/auto-version.yml deleted file mode 100644 index 343d61d..0000000 --- a/.github/workflows/auto-version.yml +++ /dev/null @@ -1,248 +0,0 @@ -name: Auto Version - -on: - pull_request: - types: [closed] - branches: [main] - -jobs: - version: - # Only run if PR was merged (not just closed) - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.PAT }} - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - - 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 - 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 }} - - - 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 }}"}}' - - # Add comment to PR about Docker build - COMMENT_BODY="🐳 **Docker Image Build Triggered** - - This PR triggered a Docker image build because of the \`+docker\` suffix in the title. - - **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 }}\` - - **To test the image after build completes:** - \`\`\`bash - docker pull ghcr.io/${{ github.repository_owner }}/zen-mcp-server:pr-${{ github.event.pull_request.number }} - \`\`\` - - **Claude Desktop config for testing:** - \`\`\`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 }}\" - ], - \"env\": { - \"GEMINI_API_KEY\": \"your-api-key-here\" - } - } - } - } - \`\`\` - - View the build progress in the [Actions tab](https://github.com/${{ github.repository }}/actions)." - - 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 - - - name: Summary - run: | - if [ "${{ steps.bump_type.outputs.should_bump }}" == "true" ]; then - echo "### ✅ Version Bumped Successfully" >> $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 - diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml new file mode 100644 index 0000000..c05519e --- /dev/null +++ b/.github/workflows/docker-pr.yml @@ -0,0 +1,129 @@ +name: PR Docker Build + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + paths: + - '**.py' + - 'requirements*.txt' + - 'pyproject.toml' + - 'Dockerfile' + - 'docker-compose.yml' + - '.dockerignore' + +permissions: + contents: read + packages: write + pull-requests: write + +jobs: + docker: + name: Build Docker Image + runs-on: ubuntu-latest + if: | + github.event.action == 'opened' || + github.event.action == 'synchronize' || + github.event.action == 'reopened' || + contains(github.event.pull_request.labels.*.name, 'docker-build') + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event.pull_request.head.repo.full_name == github.repository + 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: | + # PR-specific tag for testing + type=raw,value=pr-${{ github.event.number }}-${{ github.sha }} + type=raw,value=pr-${{ github.event.number }} + + - name: Build and push Docker image (internal PRs) + if: github.event.pull_request.head.repo.full_name == github.repository + 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: Build Docker image (fork PRs) + if: github.event.pull_request.head.repo.full_name != github.repository + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Add Docker build comment (internal PRs) + if: github.event.pull_request.head.repo.full_name == github.repository + uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3 + with: + header: docker-build + message: | + ## 🐳 Docker Build Complete + + **PR**: #${{ github.event.number }} | **Commit**: `${{ github.sha }}` + + ``` + ${{ steps.meta.outputs.tags }} + ``` + + **Test:** `docker pull ghcr.io/${{ github.repository }}:pr-${{ github.event.number }}` + + **Claude config:** + ```json + { + "mcpServers": { + "zen": { + "command": "docker", + "args": ["run", "--rm", "-i", "-e", "GEMINI_API_KEY", "ghcr.io/${{ github.repository }}:pr-${{ github.event.number }}"], + "env": { "GEMINI_API_KEY": "your-key" } + } + } + } + ``` + + 💡 Add `docker-build` label to manually trigger builds + + + - name: Update job summary (internal PRs) + if: github.event.pull_request.head.repo.full_name == github.repository + run: | + { + echo "## 🐳 Docker Build Complete" + echo "**PR**: #${{ github.event.number }} | **Commit**: ${{ github.sha }}" + echo '```' + echo "${{ steps.meta.outputs.tags }}" + echo '```' + } >> $GITHUB_STEP_SUMMARY + + - name: Update job summary (fork PRs) + if: github.event.pull_request.head.repo.full_name != github.repository + run: | + { + echo "## 🐳 Docker Build Complete (Build Only)" + echo "**PR**: #${{ github.event.number }} | **Commit**: ${{ github.sha }}" + echo "✅ Multi-platform Docker build successful" + echo "Note: Fork PRs only build (no push) for security" + } >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..445052a --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml new file mode 100644 index 0000000..48c3aa3 --- /dev/null +++ b/.github/workflows/semantic-pr.yml @@ -0,0 +1,47 @@ +--- +name: Semantic PR + +on: + pull_request: + types: [opened, edited, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + semantic-pr: + name: Validate PR + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check PR Title + id: lint-pr-title + uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Add PR error comment + uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3 + if: always() && (steps.lint-pr-title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. + + Details: + + ``` + ${{ steps.lint-pr-title.outputs.error_message }} + ``` + + - name: Delete PR error comment + uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3 + if: ${{ steps.lint-pr-title.outputs.error_message == null }} + with: + header: pr-title-lint-error + delete: true \ No newline at end of file diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml new file mode 100644 index 0000000..f9d0de3 --- /dev/null +++ b/.github/workflows/semantic-release.yml @@ -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 }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c70ff3..ffc28c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,8 @@ name: Tests on: - push: - branches: [ main, develop ] pull_request: - branches: [ main ] + branches: [main] jobs: test: @@ -14,47 +12,46 @@ jobs: python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - - - name: Run unit tests - run: | - # Run only unit tests (exclude simulation tests and integration tests) - # Integration tests require local-llama which isn't available in CI - python -m pytest tests/ -v --ignore=simulator_tests/ -m "not integration" - env: - # Ensure no API key is accidentally used in CI - GEMINI_API_KEY: "" - OPENAI_API_KEY: "" + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run unit tests + run: | + # Run only unit tests (exclude simulation tests and integration tests) + # Integration tests require local-llama which isn't available in CI + python -m pytest tests/ -v --ignore=simulator_tests/ -m "not integration" + env: + # Ensure no API key is accidentally used in CI + GEMINI_API_KEY: "" + OPENAI_API_KEY: "" lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt - - - name: Run black formatter check - run: black --check . --exclude="test_simulation_files/" - - - name: Run ruff linter - run: ruff check . --exclude test_simulation_files + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Run black formatter check + run: black --check . --exclude="test_simulation_files/" + + - name: Run ruff linter + run: ruff check . --exclude test_simulation_files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d17cf3c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +--- +default_stages: [pre-commit, pre-push] +repos: + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + + - 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] + +# 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/ + ) diff --git a/docs/contributions.md b/docs/contributions.md index 185b727..12147d0 100644 --- a/docs/contributions.md +++ b/docs/contributions.md @@ -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 @@ -32,7 +40,7 @@ Before submitting any PR, run our automated quality check script: This script automatically runs: - Ruff linting with auto-fix -- Black code formatting +- Black code formatting - Import sorting with isort - Complete unit test suite (361 tests) - Verification that all checks pass 100% @@ -56,7 +64,7 @@ python -m pytest -xvs python communication_simulator_test.py ``` -**Important**: +**Important**: - **Every single test must pass** - we have zero tolerance for failing tests in CI - All linting must pass cleanly (ruff, black, isort) - Import sorting must be correct @@ -69,12 +77,12 @@ python communication_simulator_test.py 1. **New features MUST include tests**: - Add unit tests in `tests/` for new functions or classes - Test both success and error cases - + 2. **Tool changes require simulator tests**: - Add simulator tests in `simulator_tests/` for new or modified tools - Use realistic prompts that demonstrate the feature - Validate output through server logs - + 3. **Bug fixes require regression tests**: - Add a test that would have caught the bug - Ensure the bug cannot reoccur @@ -136,14 +144,14 @@ def process_model_response( max_tokens: Optional[int] = None ) -> ProcessedResult: """Process and validate model response. - + Args: response: Raw response from the model provider max_tokens: Optional token limit for truncation - + Returns: ProcessedResult with validated and formatted content - + Raises: ValueError: If response is invalid or exceeds limits """ @@ -237,4 +245,4 @@ Contributors are recognized in: - Release notes for significant contributions - Special mentions for exceptional work -Thank you for contributing to Zen MCP Server! Your efforts help make this tool better for everyone. \ No newline at end of file +Thank you for contributing to Zen MCP Server! Your efforts help make this tool better for everyone. diff --git a/pyproject.toml b/pyproject.toml index 3080a66..46158cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ @@ -84,6 +84,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" diff --git a/requirements-dev.txt b/requirements-dev.txt index 86e039d..43273b4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,6 @@ pytest-asyncio>=0.21.0 pytest-mock>=3.11.0 black>=23.0.0 ruff>=0.1.0 -isort>=5.12.0 \ No newline at end of file +isort>=5.12.0 +python-semantic-release>=10.3.0 +build>=1.0.0 diff --git a/scripts/bump_version.py b/scripts/bump_version.py deleted file mode 100755 index 57e34ad..0000000 --- a/scripts/bump_version.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -""" -Version bumping utility for Gemini MCP Server - -This script handles semantic version bumping for the project by: -- Reading current version from config.py -- Applying the appropriate version bump (major, minor, patch) -- Updating config.py with new version and timestamp -- Preserving file structure and formatting -""" - -import re -import sys -from datetime import datetime -from pathlib import Path - - -def parse_version(version_string: str) -> tuple[int, int, int]: - """Parse semantic version string into tuple of integers.""" - match = re.match(r"^(\d+)\.(\d+)\.(\d+)", version_string) - if not match: - raise ValueError(f"Invalid version format: {version_string}") - return int(match.group(1)), int(match.group(2)), int(match.group(3)) - - -def bump_version(version: tuple[int, int, int], bump_type: str) -> tuple[int, int, int]: - """Apply version bump according to semantic versioning rules.""" - major, minor, patch = version - - if bump_type == "major": - return (major + 1, 0, 0) - elif bump_type == "minor": - return (major, minor + 1, 0) - elif bump_type == "patch": - return (major, minor, patch + 1) - else: - raise ValueError(f"Invalid bump type: {bump_type}") - - -def update_config_file(new_version: str) -> None: - """Update version and timestamp in config.py while preserving structure.""" - config_path = Path(__file__).parent.parent / "config.py" - - if not config_path.exists(): - raise FileNotFoundError(f"config.py not found at {config_path}") - - # Read the current content - content = config_path.read_text() - - # Update version using regex to preserve formatting - version_pattern = r'(__version__\s*=\s*["\'])[\d\.]+(["\'])' - content = re.sub(version_pattern, rf"\g<1>{new_version}\g<2>", content) - - # Update the __updated__ field with current date - current_date = datetime.now().strftime("%Y-%m-%d") - updated_pattern = r'(__updated__\s*=\s*["\'])[\d\-]+(["\'])' - content = re.sub(updated_pattern, rf"\g<1>{current_date}\g<2>", content) - - # Write back the updated content - config_path.write_text(content) - print(f"Updated config.py: version={new_version}, updated={current_date}") - - -def get_current_version() -> str: - """Extract current version from config.py.""" - config_path = Path(__file__).parent.parent / "config.py" - - if not config_path.exists(): - raise FileNotFoundError(f"config.py not found at {config_path}") - - content = config_path.read_text() - match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) - - if not match: - raise ValueError("Could not find __version__ in config.py") - - return match.group(1) - - -def main(): - """Main entry point for version bumping.""" - if len(sys.argv) != 2: - print("Usage: python bump_version.py ") - sys.exit(1) - - bump_type = sys.argv[1].lower() - if bump_type not in ["major", "minor", "patch"]: - print(f"Invalid bump type: {bump_type}") - print("Valid types: major, minor, patch") - sys.exit(1) - - try: - # Get current version - current = get_current_version() - print(f"Current version: {current}") - - # Parse and bump version - version_tuple = parse_version(current) - new_version_tuple = bump_version(version_tuple, bump_type) - new_version = f"{new_version_tuple[0]}.{new_version_tuple[1]}.{new_version_tuple[2]}" - - # Update config file - update_config_file(new_version) - - # Output new version for GitHub Actions - print(f"New version: {new_version}") - - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main()