# Universal CI Pipeline — penpot-mcp # Plan: https://outline.themosers.club/doc/universal-test-coverage-plan-nHgsMTqBgk name: CI on: push: branches-ignore: [] pull_request: branches: [main, test] env: REGISTRY: ${{ secrets.REGISTRY_URL }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} IMAGE_PREFIX: penpot-mcp PYTHON_VERSION: '3.11' jobs: 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 }} - run: | pip install ruff mypy ruff check src/ || ruff check src/ mypy src/ --ignore-missing-imports || true 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 }} - run: | pip install -r requirements.txt pytest pytest-cov pytest-asyncio pytest --cov=src --cov-fail-under=70 --junitxml=test-results/junit.xml -v || echo "No tests yet" dependency-scan: name: Dependency Scan runs-on: ubuntu-latest if: github.ref == 'refs/heads/test' || github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - run: pip install pip-audit && pip-audit -r requirements.txt || true sast: name: SAST — Semgrep runs-on: ubuntu-latest if: github.ref == 'refs/heads/test' || github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: semgrep/semgrep-action@v1 with: config: "p/python p/docker p/secrets" env: SEMGREP_APP_TOKEN: '' build-image: name: Build Docker Image runs-on: ubuntu-latest needs: [code-quality, unit-tests] if: github.ref == 'refs/heads/test' || github.ref == 'refs/heads/main' 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:latest outputs: type=docker,dest=/tmp/penpot-mcp.tar - uses: actions/upload-artifact@v4 with: name: docker-image path: /tmp/penpot-mcp.tar retention-days: 1 container-scan: name: Trivy Scan runs-on: ubuntu-latest needs: build-image if: github.ref == 'refs/heads/test' || github.ref == 'refs/heads/main' steps: - uses: actions/download-artifact@v4 with: name: docker-image path: /tmp - run: docker load --input /tmp/penpot-mcp.tar - uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.IMAGE_PREFIX }}/server:latest format: json output: trivy.json severity: CRITICAL,HIGH exit-code: '1' - uses: actions/upload-artifact@v4 if: always() with: name: trivy-results path: trivy.json retention-days: 30 push-image: name: Push to Registry runs-on: ubuntu-latest needs: container-scan if: github.ref == 'refs/heads/main' steps: - uses: actions/download-artifact@v4 with: name: docker-image path: /tmp - run: docker load --input /tmp/penpot-mcp.tar - run: echo "${{ env.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ env.REGISTRY_USERNAME }} --password-stdin - 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 report: name: Notify Mattermost runs-on: ubuntu-latest needs: [code-quality, unit-tests, container-scan] if: always() steps: - run: | OVERALL="success" for s in "${{ needs.unit-tests.result }}" "${{ needs.container-scan.result }}"; do [ "$s" = "failure" ] && OVERALL="failure" && break done 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 penpot-mcp CI — $(echo ${{ github.sha }} | cut -c1-7)\",\"text\":\"Branch: ${{ github.ref_name }}\"}]}"