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