ci.yml•8.58 kB
name: CI
# How the CI Pipeline Works
# - The CI workflow runs on all push events and pull requests.
# - For pull requests from forks (coming from a different repository), we skip jobs/actions that need secrets (e.g.,
#   publishing results, integration tests) to avoid exposing them. Only safe checks like local tests and flake8 are run.
# - For pull requests from branches in this repository, the workflow is skipped to avoid running it twice (once for the
#   push and once for the PR).
# - The full workflow (including jobs requiring secrets) runs only on pushes events to branches in this repository
on: [ push, pull_request ]
concurrency: ci-${{ github.ref }}
permissions:
  contents: read
  checks: write
jobs:
  build:
    name: Build, test and package
    # run this job only for push (avoiding pull requests from the same repo) or for pull requests from different repo
    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    outputs:
      is_semantic_tag: ${{ steps.get_package_version.outputs.is_semantic_tag }}
      tag: ${{ steps.get_package_version.outputs.tag }}
      version: ${{ steps.get_package_version.outputs.version }}
      wheel_artifact_id: ${{ steps.wheel_artifact_upload.outputs.artifact-id }}
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install uv
          uv sync --frozen --no-editable --extra dev
      # Runs tests with coverage and generates coverage.xml file and test-results.xml file
      # and checks flake8 code-style formatting that is compatible with isort & black
      # and verifies TOOLS.md is up-to-date with tool definitions
      # see setup in pyproject.toml
      - name: Unit tests, code style and documentation check
        run: |
          uv run tox
      - name: Publish test results
        uses: dorny/test-reporter@v1
        # run this step even if a previous step failed, but only for push events or pull requests from the same repo
        if: (success() || failure()) && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
        with:
          name: Test results (${{ matrix.python-version }})
          path: ./test-results.xml
          reporter: 'java-junit'
      - name: Upload coverage to Codecov
        # run this step only for push events or pull requests from the same repo
        if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml
          fail_ci_if_error: true
          token: ${{ secrets.CODECOV_TOKEN }}
      - name: Build wheels package
        run: |
          uv build --wheel --no-sources
      - name: Get package version
        id: get_package_version
        if: matrix.python-version == '3.10'
        run: |
          VERSION=`uv run python3 -c 'import importlib.metadata; print(importlib.metadata.version("keboola_mcp_server"))'`
          TAG="${GITHUB_REF##*/}"
          IS_SEMANTIC_TAG=$(echo "$TAG" | grep -q '^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$' && echo true || echo false)
          echo "Version = '$VERSION', Tag = '$TAG', is semantic tag = '$IS_SEMANTIC_TAG'"
          echo "is_semantic_tag=$IS_SEMANTIC_TAG" >> $GITHUB_OUTPUT
          echo "tag=$TAG" >> $GITHUB_OUTPUT
          echo "version=${VERSION}" >> $GITHUB_OUTPUT
      - name: Upload wheel package
        id: wheel_artifact_upload
        # run this step only for push events or pull requests from the same repo
        if: matrix.python-version == '3.10' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
        uses: actions/upload-artifact@v4
        with:
          name: keboola_mcp_server-${{ steps.get_package_version.outputs.version }}-py3-none-any.whl
          path: dist/keboola_mcp_server-${{ steps.get_package_version.outputs.version }}-py3-none-any.whl
          if-no-files-found: error
          compression-level: 0  # wheels are ZIP archives
          retention-days: 7
  integration_tests:
    name: Integration Tests
    needs: build
    # run this job only for push events (not pull requests)
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
      # This ensures tests run sequentially, not in parallel
      max-parallel: 1
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install uv
          uv sync --frozen --no-editable --extra dev
      - name: Integration tests
        # run actual tests only for pushes to the same repository (not forks)
        if: github.repository == github.event.repository.full_name
        env:
          INTEGTEST_STORAGE_TOKEN: ${{ secrets.INTEGTEST_STORAGE_TOKEN }}
          INTEGTEST_STORAGE_API_URL: ${{ vars.INTEGTEST_STORAGE_API_URL }}
          INTEGTEST_WORKSPACE_SCHEMA: ${{ vars.INTEGTEST_WORKSPACE_SCHEMA }}
        run: |
          uv run tox -e integtests
      - name: Skip integration tests for forks
        # show a message when tests are skipped for forks
        if: github.repository != github.event.repository.full_name
        run: |
          echo "Integration tests skipped for fork repository"
          echo "Dependencies installed successfully - setup is working correctly"
      - name: Publish integration test results
        uses: dorny/test-reporter@v1
        # publish results only when actual tests were run (same repository)
        if: (always()) && github.repository == github.event.repository.full_name
        with:
          name: Integration test results (${{ matrix.python-version }})
          path: ./integtest-results.xml
          reporter: 'java-junit'
  deploy_to_pypi:
    name: Deploy to pypi.org
    needs:
      - build
      - integration_tests
    runs-on: ubuntu-latest
    if: |
      startsWith(github.ref, 'refs/tags/') &&
      needs.build.outputs.is_semantic_tag == 'true' &&
      needs.build.outputs.tag == format('v{0}', needs.build.outputs.version)
    steps:
      - name: Download wheel package
        uses: actions/download-artifact@v4
        with:
          name: keboola_mcp_server-${{ needs.build.outputs.version }}-py3-none-any.whl
          path: dist/
      - name: Publish package
        uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}
  register-with-anthropic:
    name: Register with Anthropic
    needs:
      - build
      - deploy_to_pypi
    runs-on: ubuntu-latest
    if: |
      startsWith(github.ref, 'refs/tags/') &&
      needs.build.outputs.is_semantic_tag == 'true' &&
      needs.build.outputs.tag == format('v{0}', needs.build.outputs.version)
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Prepare server.json
        run: |
          sed 's/__MCP_SERVER_VERSION__/${{ needs.build.outputs.version }}/g' server.json.template > server.json
          cat server.json
      - name: Install mcp-publisher
        run: |
          PUBLISHER=1.2.3
          curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v${PUBLISHER}/mcp-publisher_${PUBLISHER}_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
          sudo mv mcp-publisher /usr/local/bin/
      - name: Create private key file
        run: |
          echo "${{ secrets.ANTHROPIC_REGISTRY_PVK }}" > key.pem
          chmod 600 key.pem
      - name: Register with Anthropic
        run: |
          set -euo pipefail
          mcp-publisher login dns --domain keboola.com --private-key $(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')
          mcp-publisher publish
      - name: Clean up private key
        if: always()
        run: |
          rm -f key.pem