# Universal CI Pipeline — ollama-mcp # Based on: moserja/projectTracker/.gitea/workflows/ci-cd.yml # Plan: https://outline.themosers.club/doc/universal-test-coverage-plan-nHgsMTqBgk name: CI on: push: branches: [main, develop] pull_request: branches: [main] workflow_dispatch: inputs: deploy_images: description: 'Build and push Docker image' default: 'false' type: boolean env: REGISTRY: ${{ secrets.REGISTRY_URL }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} IMAGE_PREFIX: ollama-mcp PYTHON_VERSION: '3.11' jobs: # ── Stage 1: Code Quality ──────────────────────────────────────────────────── code-quality: name: Code Quality & Linting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Lint (ruff + mypy) run: | pip install ruff mypy ruff check src/ --output-format json > ruff-results.json || ruff check src/ mypy src/ --ignore-missing-imports || true - uses: actions/upload-artifact@v4 if: always() with: name: lint-results path: ruff-results.json retention-days: 30 # ── Stage 2: Unit Tests ────────────────────────────────────────────────────── unit-tests: name: Unit Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Run tests run: | pip install -r requirements.txt pip install pytest pytest-cov pytest-asyncio pytest --cov=src --cov-report=xml --cov-fail-under=70 \ --junitxml=test-results/junit.xml -v || echo "No tests or tests failed" - uses: actions/upload-artifact@v4 if: always() with: name: test-results path: test-results/ retention-days: 30 # ── Stage 3: Dependency Scan ───────────────────────────────────────────────── dependency-scan: name: Dependency Vulnerability Scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: pip-audit continue-on-error: true run: | pip install pip-audit pip-audit -r requirements.txt --format json > pip-audit.json || true pip-audit -r requirements.txt - uses: actions/upload-artifact@v4 if: always() with: name: dependency-scan-results path: pip-audit.json retention-days: 30 # ── Stage 4: SAST ──────────────────────────────────────────────────────────── sast: name: SAST — Semgrep runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Semgrep uses: semgrep/semgrep-action@v1 with: config: >- p/python p/docker p/secrets p/owasp-top-ten env: SEMGREP_APP_TOKEN: '' # ── Stage 5: Build Docker Image ─────────────────────────────────────────────── build-image: name: Build Docker Image runs-on: ubuntu-latest needs: [code-quality, unit-tests] steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - name: Generate tags id: meta run: | echo "short_sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT echo "branch=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_OUTPUT - uses: docker/build-push-action@v5 with: context: . push: false tags: | ${{ env.IMAGE_PREFIX }}/server:${{ steps.meta.outputs.short_sha }} ${{ env.IMAGE_PREFIX }}/server:${{ steps.meta.outputs.branch }} ${{ env.IMAGE_PREFIX }}/server:latest outputs: type=docker,dest=/tmp/ollama-mcp.tar - uses: actions/upload-artifact@v4 with: name: docker-image path: /tmp/ollama-mcp.tar retention-days: 1 # ── Stage 6: Container Scan ─────────────────────────────────────────────────── container-scan: name: Trivy Scan runs-on: ubuntu-latest needs: build-image steps: - uses: actions/download-artifact@v4 with: name: docker-image path: /tmp - run: docker load --input /tmp/ollama-mcp.tar - uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.IMAGE_PREFIX }}/server:latest format: sarif output: trivy.sarif severity: CRITICAL,HIGH exit-code: '1' - uses: aquasecurity/trivy-action@master if: always() with: image-ref: ${{ env.IMAGE_PREFIX }}/server:latest format: json output: trivy.json severity: CRITICAL,HIGH exit-code: '0' - uses: actions/upload-artifact@v4 if: always() with: name: trivy-results path: trivy.* retention-days: 30 # ── Stage 7: Results Reporting ──────────────────────────────────────────────── report: name: Report Results runs-on: ubuntu-latest needs: [code-quality, unit-tests, dependency-scan, sast, container-scan] if: always() steps: - name: Build summary id: summary run: | icon() { [ "$1" = "success" ] && echo "✅" || ([ "$1" = "skipped" ] && echo "⏭️" || echo "❌"); } BODY="## CI Results — \`$(echo ${{ github.sha }} | cut -c1-7)\` | Stage | Status | |-------|--------| | Code Quality | $(icon ${{ needs.code-quality.result }}) ${{ needs.code-quality.result }} | | Unit Tests | $(icon ${{ needs.unit-tests.result }}) ${{ needs.unit-tests.result }} | | Dependency Scan | $(icon ${{ needs.dependency-scan.result }}) ${{ needs.dependency-scan.result }} | | SAST (Semgrep) | $(icon ${{ needs.sast.result }}) ${{ needs.sast.result }} | | Container Scan (Trivy) | $(icon ${{ needs.container-scan.result }}) ${{ needs.container-scan.result }} | [View artifacts →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" echo "body<> $GITHUB_OUTPUT echo "$BODY" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Post PR comment if: github.event_name == 'pull_request' run: | curl -s -X POST \ "${{ github.api_url }}/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg body "${{ steps.summary.outputs.body }}" '{"body": $body}')" - name: Notify Mattermost run: | OVERALL="${{ needs.container-scan.result }}" [ "${{ needs.unit-tests.result }}" = "failure" ] && OVERALL="failure" COLOR="good"; EMOJI="✅" [ "$OVERALL" = "failure" ] && COLOR="danger" && EMOJI="❌" curl -s -X POST "${{ secrets.MATTERMOST_WEBHOOK_URL }}" \ -H "Content-Type: application/json" \ -d "{\"attachments\":[{\"color\":\"$COLOR\",\"title\":\"$EMOJI ollama-mcp CI — $(echo ${{ github.sha }} | cut -c1-7)\",\"text\":\"Branch: \`${{ github.ref_name }}\` | [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\"}]}" # ── Push to Registry (main only) ───────────────────────────────────────────── push-image: name: Push to Registry runs-on: ubuntu-latest needs: container-scan if: | (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.deploy_images == true) steps: - uses: actions/download-artifact@v4 with: name: docker-image path: /tmp - run: docker load --input /tmp/ollama-mcp.tar - run: echo "${{ env.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ env.REGISTRY_USERNAME }} --password-stdin - name: Tag and push run: | SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) docker tag ${{ env.IMAGE_PREFIX }}/server:latest ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:latest docker tag ${{ env.IMAGE_PREFIX }}/server:latest ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:$SHORT_SHA docker push ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:$SHORT_SHA