commit abd37403f508f9ae87bcd772bf6e73eec9b4c6a4 Author: Vishesh 'ironeagle' Bangotra Date: Mon Nov 3 20:15:52 2025 +0530 coverage init diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..669b2fa --- /dev/null +++ b/.drone.yml @@ -0,0 +1,143 @@ +--- +kind: pipeline +type: docker +name: default + +platform: + os: linux + arch: arm64 + +workspace: + path: /drone/src + +volumes: + - name: dockersock + host: + path: /var/run/docker.sock + +steps: + - name: fetch-tags + image: docker:24 + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - apk add --no-cache git + - git fetch --tags + - | + # Get latest Git tag and trim newline + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null | tr -d '\n') + echo "Latest Git tag fetched: $LATEST_TAG" + + # Save to file for downstream steps + echo "$LATEST_TAG" > /drone/src/LATEST_TAG.txt + + # Read back for verification + IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n') + echo "Image tag read from file: $IMAGE_TAG" + + # Validate + if [ -z "$IMAGE_TAG" ]; then + echo "❌ No git tags found! Cannot continue." + exit 1 + fi + + - name: check-remote-image + image: docker:24 + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n') + + - echo "Checking if apps/coverage:$IMAGE_TAG exists on remote Docker..." + - echo "Existing Docker tags for apps/coverage:" + - docker images --format "{{.Repository}}:{{.Tag}}" | grep "^apps/coverage" || echo "(none)" + - | + if docker image inspect apps/coverage:$IMAGE_TAG > /dev/null 2>&1; then + echo "✅ Docker image apps/coverage:$IMAGE_TAG already exists — skipping build" + exit 78 + else + echo "⚙️ Docker image apps/coverage:$IMAGE_TAG not found — proceeding to build..." + fi + + - name: build-image + image: docker:24 + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n') + + - echo "🔨 Building Docker image apps/coverage:$IMAGE_TAG ..." + - | + docker build \ + --network=host \ + -t apps/coverage:$IMAGE_TAG \ + -t apps/coverage:latest \ + /drone/src + + - name: push-image + image: docker:24 + environment: + REGISTRY_HOST: + from_secret: REGISTRY_HOST + REGISTRY_USER: + from_secret: REGISTRY_USER + REGISTRY_PASS: + from_secret: REGISTRY_PASS + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n') + + - echo "🔑 Logging into registry $REGISTRY_HOST ..." + - echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin + - echo "🏷️ Tagging images with registry prefix..." + - docker tag apps/coverage:$IMAGE_TAG $REGISTRY_HOST/apps/coverage:$IMAGE_TAG + - docker tag apps/coverage:$IMAGE_TAG $REGISTRY_HOST/apps/coverage:latest + - echo "📤 Pushing apps/coverage:$IMAGE_TAG ..." + - docker push $REGISTRY_HOST/apps/coverage:$IMAGE_TAG + - echo "📤 Pushing apps/coverage:latest ..." + - docker push $REGISTRY_HOST/apps/coverage:latest + + - name: stop-old + image: docker:24 + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - echo "🛑 Stopping old container..." + - docker rm -f coverage || true + + - name: run-container + image: docker:24 + volumes: + - name: dockersock + path: /var/run/docker.sock + environment: + MONGO_HOST: + from_secret: MONGO_HOST + MONGO_USER: + from_secret: MONGO_USER + MONGO_PASS: + from_secret: MONGO_PASS + MONGO_PORT: + from_secret: MONGO_PORT + commands: + - IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n') + + - echo "🚀 Starting container apps/coverage:$IMAGE_TAG ..." + - | + docker run -d \ + --name coverage \ + -p 9002:8000 \ + --restart always \ + apps/coverage:$IMAGE_TAG + +# Trigger rules +trigger: + event: + - tag + - custom diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a253698 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Virtual environments +venv/ +env/ +.venv + +# PyCharm +.idea/ + +# Environment variables +.env +.env.local + +# OS +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..99ea405 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# =========================== +# Stage 1: Build environment +# =========================== +FROM python:3.13-slim AS builder + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential curl && \ + pip install --upgrade pip uv && \ + rm -rf /var/lib/apt/lists/* + +# Copy requirement file first for better caching +COPY requirements.txt . + +# ✅ Install dependencies +RUN pip install -r requirements.txt + +# Copy source code +COPY . . + +# =========================== +# Stage 2: Runtime environment +# =========================== +FROM python:3.13-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + APP_HOME=/app + +WORKDIR $APP_HOME + +# Install runtime dependency (curl for healthcheck) +RUN apt-get update && apt-get install -y --no-install-recommends curl && \ + rm -rf /var/lib/apt/lists/* + +# ✅ Copy Python and pip-installed packages from builder +COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13 +COPY --from=builder /usr/local/bin /usr/local/bin + +# Copy application code +COPY . . + +# Expose FastAPI port +EXPOSE 8000 + +# Healthcheck endpoint +HEALTHCHECK CMD curl --fail http://localhost:8000/health || exit 1 + +# ✅ Start FastAPI app +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/main.py b/main.py new file mode 100644 index 0000000..b38281d --- /dev/null +++ b/main.py @@ -0,0 +1,58 @@ +from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import JSONResponse +from pathlib import Path +import xml.etree.ElementTree as ET + +app = FastAPI() + + +@app.get("/health") +async def health_check(): + return {"status": "ok"} + + +def parse_coverage_percentage(coverage_file: Path) -> float: + """Parse the overall coverage percentage from coverage.xml.""" + tree = ET.parse(coverage_file) + root = tree.getroot() + line_rate = float(root.attrib.get("line-rate", 0)) + return round(line_rate * 100, 2) + + +def get_color(pct: float) -> str: + """Return badge color based on coverage.""" + if pct >= 90: + return "brightgreen" + if pct >= 75: + return "green" + if pct >= 60: + return "yellow" + if pct >= 40: + return "orange" + return "red" + + +@app.get("/badge", response_class=JSONResponse) +async def get_coverage_badge(project: str = Query(..., description="Project folder name")): + """ + Return a Shields.io-compatible badge JSON for the given project's coverage. + + Example: + /badge?project=mongo-ops + Use with Shields.io: + https://img.shields.io/endpoint?url=https://api.aetoskia.com/coverage/badge?project=mongo-ops + """ + project_dir = Path.cwd() / project + coverage_file = project_dir / "coverage.xml" + + if not coverage_file.exists(): + raise HTTPException(status_code=404, detail=f"No coverage.xml found in '{project}'") + + pct = parse_coverage_percentage(coverage_file) + + return { + "schemaVersion": 1, + "label": f"{project} coverage", + "message": f"{pct}%", + "color": get_color(pct) + } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fd0fad1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.120.4 +uvicorn==0.38.0