name: Unified CI/CD Pipeline
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main, develop]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
PYTHON_VERSION: "3.12"
NODE_VERSION: "20"
SIMPLENOTE_OFFLINE_MODE: "true"
TSX_CACHE_DIR: /tmp/tsx-cache
NODE_OPTIONS: "--max-old-space-size=4096"
PYTEST_TIMEOUT: "60"
REGISTRY: docker.io
IMAGE_NAME: docdyhr/simplenote-mcp-server
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate files
run: |
find . -type f -size +1M | grep -v "^./.git" | while read file; do
echo "Warning: Large file detected: $file ($(du -h "$file" | cut -f1))"
done
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
- name: Cache dependencies
uses: actions/cache@v5
id: cache
with:
path: |
~/.cache/pip
~/.local
.venv
key: deps-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
restore-keys: |
deps-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,test]"
- name: Check version consistency
run: |
python scripts/quality/check_version_consistency.py
- name: Run linters
run: |
ruff check .
ruff format --check .
mypy simplenote_mcp
test:
name: Test
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
- name: Restore dependencies
uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.local
.venv
key: deps-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,test]"
- name: Run tests
env:
SIMPLENOTE_EMAIL: test@example.com
SIMPLENOTE_PASSWORD: test-password
run: |
pytest tests/ -m "not integration" --cov=simplenote_mcp --cov-report=xml --cov-report=json --cov-report=term
- name: Check coverage threshold
run: |
python scripts/quality/check_coverage.py --threshold 65.0 --warn-only
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
fail_ci_if_error: true
security:
name: Security
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install security tools
run: |
pip install bandit safety
- name: Run security scans
run: |
bandit -r simplenote_mcp --severity-level high
pip install -e .
safety check
build_and_publish:
name: Build and Publish
needs: [test, security]
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
security-events: write
actions: read
attestations: write
id-token: write
steps:
- uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
platforms: linux/amd64,linux/arm64
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
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: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=sha,prefix={{date 'YYYYMMDD'}}-
type=schedule,pattern={{date 'YYYYMMDD'}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scanner
if: github.event_name != 'pull_request'
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH"
- name: Upload Trivy scan results to GitHub Security tab
if: github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: "trivy-results.sarif"
category: "trivy-container-scan"
- name: Update Docker Hub repository description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
repository: ${{ env.IMAGE_NAME }}
readme-filepath: ./DOCKER_README.md
short-description: "MCP server for Simplenote integration with note management and search capabilities."
local-test:
name: Local Test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,test]"
pip install build
- name: Run local test script
run: |
./test-ci-locally.sh
status:
name: CI Status
runs-on: ubuntu-latest
needs: [validate, test, security, build_and_publish, local-test]
if: always()
steps:
- name: Check status
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "❌ CI pipeline failed"
exit 1
elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "⚠️ CI pipeline was cancelled"
exit 1
else
echo "✅ CI pipeline passed successfully!"
fi