diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c7c985a --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,236 @@ +# 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