diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b7abdcf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +node_modules +**/dist +**/.env +**/.env.* +apps/stt/.venv +uploads +docs +e2e +test-results +playwright-report +graphify-out +.worktrees +*.md +crm-node_modules.tgz diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..91ad123 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,124 @@ +name: Build & Push Docker Image + +on: + push: + tags: + - "*" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Start time + run: echo "BUILD_STARTED=$(date +%s)" >> "$GITHUB_ENV" + + - name: Free disk space + id: prune + run: | + df -h / /workspace /var/lib/containerd 2>/dev/null || df -h / + docker builder prune -af || true + docker image prune -af || true + docker system prune -af --volumes || true + df -h / /workspace /var/lib/containerd 2>/dev/null || df -h / + + - name: Checkout code + id: checkout + uses: actions/checkout@v4 + + - name: Resolve image tag + id: meta + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + TAG="${GITHUB_REF_NAME}" + else + TAG="$(git rev-parse --short HEAD)" + fi + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "registry=dock.monsys.ir" >> "$GITHUB_OUTPUT" + + - name: Login to registry + id: login + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + USER_NAME="${REGISTRY_USERNAME:-admin}" + printf '%s\n' "$REGISTRY_PASSWORD" | docker login "dock.monsys.ir" -u "$USER_NAME" --password-stdin + + - name: Build Docker images + id: build + run: | + set -eu + tag="${{ steps.meta.outputs.tag }}" + reg="${{ steps.meta.outputs.registry }}" + docker build -f Dockerfile.api -t "$reg/montact-api:$tag" -t "$reg/montact-api:latest" . + docker build -f Dockerfile.web -t "$reg/montact-web:$tag" -t "$reg/montact-web:latest" . + docker build -f Dockerfile.stt -t "$reg/montact-stt:$tag" -t "$reg/montact-stt:latest" . + + - name: Push Docker images + id: push + run: | + set -eu + tag="${{ steps.meta.outputs.tag }}" + reg="${{ steps.meta.outputs.registry }}" + for name in montact-api montact-web montact-stt; do + docker push "$reg/$name:$tag" + docker push "$reg/$name:latest" + done + + - name: Notify Bale + if: always() + continue-on-error: true + env: + BALE_TOKEN: ${{ secrets.BALE_BOT_TOKEN || vars.BALE_BOT_TOKEN }} + BALE_CHAT_ID: ${{ vars.BALE_CHAT_ID }} + TAG: ${{ steps.meta.outputs.tag }} + REG: ${{ steps.meta.outputs.registry }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }} + STEP_OUTCOMES: | + Checkout code=${{ steps.checkout.outcome }} + Resolve image tag=${{ steps.meta.outcome }} + Login to registry=${{ steps.login.outcome }} + Build Docker images=${{ steps.build.outcome }} + Push Docker images=${{ steps.push.outcome }} + run: | + if [ -z "$BALE_TOKEN" ] || [ -z "$BALE_CHAT_ID" ]; then + echo "BALE_BOT_TOKEN / BALE_CHAT_ID not set in Gitea — skipping notification" + exit 0 + fi + + failed_step=$(printf '%s\n' "$STEP_OUTCOMES" | grep -E '=(failure|cancelled)$' | head -n1 | cut -d= -f1) + if [ -n "$failed_step" ]; then + title="❌ بیلد ناموفق" + elif printf '%s\n' "$STEP_OUTCOMES" | grep -q '^Push Docker images=success$'; then + title="✅ بیلد موفق" + else + title="⚠️ بیلد کامل نشد" + fi + + commit=$(git log -1 --pretty='%h — %s' 2>/dev/null || echo "${GITHUB_SHA:0:7}") + elapsed=$(( $(date +%s) - ${BUILD_STARTED:-$(date +%s)} )) + + text="${title} + پروژه: ${GITHUB_REPOSITORY} + تگ: ${TAG:-${GITHUB_REF_NAME}}" + if [ -z "$failed_step" ] && [ -n "$TAG" ]; then + text="${text} + image: ${REG}/montact-api:${TAG} + image: ${REG}/montact-web:${TAG} + image: ${REG}/montact-stt:${TAG}" + fi + [ -n "$failed_step" ] && text="${text} + مرحلهٔ خطا: ${failed_step}" + text="${text} + کامیت: ${commit} + توسط: ${GITHUB_ACTOR} + مدت: $((elapsed / 60))m$((elapsed % 60))s + لاگ: ${RUN_URL}" + msg_file=$(mktemp) + printf '%s\n' "$text" | sed 's/^[[:space:]]*//' > "$msg_file" + + curl -sS -m 20 -X POST "https://tapi.bale.ai/bot${BALE_TOKEN}/sendMessage" \ + --data-urlencode "chat_id=${BALE_CHAT_ID}" \ + --data-urlencode "text@${msg_file}" diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..adfa0b9 --- /dev/null +++ b/Dockerfile.api @@ -0,0 +1,29 @@ +# NestJS API + Prisma. Build context is the monorepo root. +FROM node:20-bookworm-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +COPY apps/api/package.json apps/api/package.json +COPY apps/web/package.json apps/web/package.json +COPY packages/shared/package.json packages/shared/package.json +RUN npm ci +COPY apps/api apps/api +COPY packages/shared packages/shared +RUN npm run build -w @mento/shared \ + && npm run prisma:generate -w @mento/api \ + && npm run build -w @mento/api + +FROM node:20-bookworm-slim +WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/package.json /app/package-lock.json ./ +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/packages/shared ./packages/shared +COPY --from=build /app/apps/api/package.json ./apps/api/package.json +COPY --from=build /app/apps/api/dist ./apps/api/dist +COPY --from=build /app/apps/api/prisma ./apps/api/prisma +WORKDIR /app/apps/api +ENV NODE_ENV=production +EXPOSE 3000 +CMD ["node", "dist/src/main.js"] diff --git a/Dockerfile.stt b/Dockerfile.stt new file mode 100644 index 0000000..781fe44 --- /dev/null +++ b/Dockerfile.stt @@ -0,0 +1,15 @@ +# Local Faster-Whisper. Not published on Ingress. +FROM python:3.11-slim-bookworm +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY apps/stt/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt +COPY apps/stt/main.py ./main.py +ENV STT_MODEL=small \ + STT_DEVICE=cpu \ + STT_COMPUTE_TYPE=int8 \ + HF_HOME=/models +EXPOSE 3040 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3040"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..e5c127a --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,16 @@ +# Vue panel. The built app calls /api on the same host; Traefik sends that to the API. +FROM node:20-bookworm-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +COPY apps/api/package.json apps/api/package.json +COPY apps/web/package.json apps/web/package.json +COPY packages/shared/package.json packages/shared/package.json +RUN npm ci +COPY apps/web apps/web +COPY packages/shared packages/shared +RUN npm run build -w @mento/shared && npm run build -w @mento/web + +FROM nginx:1.27-alpine +COPY docker/web.nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/apps/web/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/docker/web.nginx.conf b/docker/web.nginx.conf new file mode 100644 index 0000000..8e047e5 --- /dev/null +++ b/docker/web.nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + client_max_body_size 50m; + + location / { + try_files $uri $uri/ /index.html; + } +}