name: PR Preview on: pull_request: types: [opened, synchronize, reopened, closed] env: REGISTRY: registry.oci.euphon.net BASE_DOMAIN: oil.oci.euphon.net jobs: deploy-preview: if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Unit tests run: cd frontend && npm ci && npm run test:unit - name: Deploy Preview Environment run: | PR_ID="${{ github.event.pull_request.number }}" NS="oil-pr-${PR_ID}" HOST="pr-${PR_ID}.${BASE_DOMAIN}" IMAGE="${REGISTRY}/oil-calculator:pr-${PR_ID}" # Sync source to oci build server rsync -az --exclude node_modules --exclude .git --exclude .venv \ . oci:/tmp/oil-pr-${PR_ID}-build/ ssh oci bash -s "${PR_ID}" "${NS}" "${HOST}" "${IMAGE}" << 'DEPLOY_SCRIPT' set -e PR_ID="$1"; NS="$2"; HOST="$3"; IMAGE="$4" cd /tmp/oil-pr-${PR_ID}-build # Copy production DB into build context so it's baked into image PROD_POD=$(sudo k3s kubectl get pods -n oil-calculator -l app=oil-calculator \ --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") if [ -n "$PROD_POD" ]; then sudo k3s kubectl cp "oil-calculator/${PROD_POD}:/data/oil_calculator.db" /tmp/pr-${PR_ID}.db mkdir -p data cp /tmp/pr-${PR_ID}.db data/oil_calculator.db fi # Build image (with DB baked in) cat > Dockerfile.preview << 'DEOF' FROM node:20-slim AS frontend-build WORKDIR /build COPY frontend/package.json frontend/package-lock.json ./ RUN npm ci COPY frontend/ ./ RUN npm run build FROM python:3.12-slim WORKDIR /app COPY backend/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY backend/ ./backend/ COPY --from=frontend-build /build/dist ./frontend/ # Bake production DB copy into image COPY data/oil_calculator.db /data/oil_calculator.db ENV DB_PATH=/data/oil_calculator.db ENV FRONTEND_DIR=/app/frontend EXPOSE 8000 CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] DEOF docker build -f Dockerfile.preview -t "${IMAGE}" . docker push "${IMAGE}" # Create namespace sudo k3s kubectl create namespace "${NS}" --dry-run=client -o yaml | sudo k3s kubectl apply -f - # Copy regcred from production namespace sudo k3s kubectl get secret regcred -n oil-calculator -o json | \ sed "s/\"namespace\":\"oil-calculator\"/\"namespace\":\"${NS}\"/" | \ sudo k3s kubectl apply -f - # Deploy pod + service + ingress (no PVC needed, DB is in image) cat << EOYAML | sudo k3s kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: oil-calculator namespace: ${NS} spec: replicas: 1 selector: matchLabels: app: oil-calculator template: metadata: labels: app: oil-calculator spec: imagePullSecrets: - name: regcred containers: - name: app image: ${IMAGE} ports: - containerPort: 8000 resources: requests: cpu: 50m memory: 64Mi limits: cpu: 500m memory: 256Mi --- apiVersion: v1 kind: Service metadata: name: oil-calculator namespace: ${NS} spec: selector: app: oil-calculator ports: - port: 80 targetPort: 8000 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: oil-calculator namespace: ${NS} annotations: traefik.ingress.kubernetes.io/router.tls.certresolver: le spec: ingressClassName: traefik tls: - hosts: - ${HOST} rules: - host: ${HOST} http: paths: - path: / pathType: Prefix backend: service: name: oil-calculator port: number: 80 EOYAML # Wait for rollout sudo k3s kubectl rollout status deploy/oil-calculator -n "${NS}" --timeout=120s # Cleanup build dir rm -rf /tmp/oil-pr-${PR_ID}-build /tmp/pr-${PR_ID}.db echo "Preview deployed: https://${HOST}" DEPLOY_SCRIPT - name: Comment PR with preview URL run: | PR_ID="${{ github.event.pull_request.number }}" HOST="pr-${PR_ID}.${BASE_DOMAIN}" curl -s -X POST \ "https://git.euphon.cloud/api/v1/repos/${{ github.repository }}/issues/${PR_ID}/comments" \ -H "Authorization: token ${{ secrets.GIT_TOKEN }}" \ -H "Content-Type: application/json" \ -d "{\"body\": \"🚀 Preview deployed: https://${HOST}\n\nDB is a copy of production. Changes here won't affect prod.\"}" teardown-preview: if: github.event.action == 'closed' runs-on: ubuntu-latest steps: - name: Teardown Preview Environment run: | PR_ID="${{ github.event.pull_request.number }}" NS="oil-pr-${PR_ID}" IMAGE="${REGISTRY}/oil-calculator:pr-${PR_ID}" ssh oci " sudo k3s kubectl delete namespace ${NS} --ignore-not-found docker rmi ${IMAGE} 2>/dev/null || true " - name: Comment PR run: | PR_ID="${{ github.event.pull_request.number }}" curl -s -X POST \ "https://git.euphon.cloud/api/v1/repos/${{ github.repository }}/issues/${PR_ID}/comments" \ -H "Authorization: token ${{ secrets.GIT_TOKEN }}" \ -H "Content-Type: application/json" \ -d "{\"body\": \"🗑️ Preview environment torn down.\"}"