diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml new file mode 100644 index 0000000..031ccca --- /dev/null +++ b/.gitea/workflows/build-and-deploy.yml @@ -0,0 +1,135 @@ +name: Build & Deploy + +on: + push: + branches: [develop] + +concurrency: + group: build-${{ gitea.ref }} + cancel-in-progress: false + +env: + NODE_VERSION: "20" + # Gitea built-in container registry (HTTPS on port 443) + REGISTRY_HOST: 192.168.3.80 + DOCKERFILE_PATH: Dockerfile.production + # VPS deployment target + DEPLOY_ROOT: /opt/rentaldrivego + +jobs: + build-image: + name: Build & Push Docker Image + runs-on: ubuntu-latest + outputs: + image_repository: ${{ steps.image-meta.outputs.repository }} + docker_image: ${{ steps.image-meta.outputs.full }} + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker image metadata + id: image-meta + run: | + # Gitea container registry path: host/owner/repo + REPO="${{ gitea.repository }}" + TAG="${{ gitea.sha }}" + echo "repository=${REPO}" >> "$GITHUB_OUTPUT" + echo "full=${REGISTRY_HOST}/${REPO}:${TAG}" >> "$GITHUB_OUTPUT" + echo "latest=${REGISTRY_HOST}/${REPO}:latest" >> "$GITHUB_OUTPUT" + + - name: Log in to Gitea Container Registry + run: | + echo "${{ secrets.GITEA_TOKEN }}" | \ + docker login "$REGISTRY_HOST" \ + -u "${{ gitea.actor }}" \ + --password-stdin + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ env.DOCKERFILE_PATH }} + push: true + tags: | + ${{ steps.image-meta.outputs.full }} + ${{ steps.image-meta.outputs.latest }} + build-args: | + API_INTERNAL_URL=http://api:4000/api/v1 + DASHBOARD_INTERNAL_URL=http://dashboard:3001 + ADMIN_INTERNAL_URL=http://admin:3002 + NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} + NEXT_PUBLIC_MARKETPLACE_URL=${{ secrets.NEXT_PUBLIC_MARKETPLACE_URL }} + NEXT_PUBLIC_DASHBOARD_URL=${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }} + NEXT_PUBLIC_ADMIN_URL=${{ secrets.NEXT_PUBLIC_ADMIN_URL }} + + deploy: + name: Deploy to VPS + runs-on: ubuntu-latest + needs: [build-image] + env: + DOCKER_IMAGE: ${{ needs.build-image.outputs.docker_image }} + IMAGE_REPOSITORY: ${{ needs.build-image.outputs.image_repository }} + steps: + - uses: actions/checkout@v4 + + - name: Check deploy credentials + id: check-creds + run: | + if [ -z "${{ secrets.VPS_SSH_KEY }}" ] || \ + [ -z "${{ secrets.VPS_IP }}" ] || \ + [ -z "${{ secrets.VPS_USER }}" ]; then + echo "VPS secrets not fully configured — skipping deploy" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install SSH client + if: steps.check-creds.outputs.skip == 'false' + run: | + apt-get update -qq && apt-get install -y -qq openssh-client + + - name: Set up SSH key + if: steps.check-creds.outputs.skip == 'false' + run: | + mkdir -p ~/.ssh && chmod 700 ~/.ssh + echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H "${{ secrets.VPS_IP }}" >> ~/.ssh/known_hosts 2>/dev/null + chmod 644 ~/.ssh/known_hosts + + - name: Sync deployment assets + if: steps.check-creds.outputs.skip == 'false' + run: | + ssh "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}" \ + "mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage' '$DEPLOY_ROOT/docker/registry/auth' '$DEPLOY_ROOT/dynamic'" + scp docker-compose.production.yml \ + docker-compose.portainer.production.yml \ + docker-compose.registry.production.yml \ + docker-compose.registry.local.yml \ + traefik.yaml \ + "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}:$DEPLOY_ROOT/" + scp scripts/docker-prod-common.sh \ + scripts/docker-prod-deploy.sh \ + scripts/docker-prod-up-registry.sh \ + scripts/docker-registry-local-up.sh \ + "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}:$DEPLOY_ROOT/scripts/" + scp docker/pgmanage/override.py \ + "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}:$DEPLOY_ROOT/docker/pgmanage/" + + - name: Run deploy script on VPS + if: steps.check-creds.outputs.skip == 'false' + run: | + REGISTRY_PASSWORD_B64="$(printf '%s' "${{ secrets.GITEA_TOKEN }}" | base64 | tr -d '\n')" + ssh "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}" " + set -e + cd '$DEPLOY_ROOT' + APP_IMAGE='$IMAGE_REPOSITORY' \ + APP_VERSION='${{ gitea.sha }}' \ + REGISTRY_HOST='$REGISTRY_HOST' \ + REGISTRY_USER='${{ gitea.actor }}' \ + REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \ + bash scripts/docker-prod-deploy.sh + " diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..6838358 --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,108 @@ +name: Test + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + +concurrency: + group: test-${{ gitea.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: "20" + +jobs: + api-tests: + name: API Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + - run: npm ci + - run: npm run db:generate + - run: npm run type-check + - run: npm run test:api + + marketplace-tests: + name: Marketplace Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + - run: npm ci + - run: npm run build --workspace @rentaldrivego/types + - run: npm run test:marketplace + + admin-tests: + name: Admin Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + - run: npm ci + - run: npm run test:admin + + dashboard-tests: + name: Dashboard Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + - run: npm ci + - run: npm run build --workspace @rentaldrivego/types + - run: npm run test:dashboard + + integration-tests: + name: API Integration Tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: rentaldrivego_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + DATABASE_URL: postgresql://postgres:password@postgres:5432/rentaldrivego_test + REDIS_URL: redis://redis:6379 + NODE_ENV: test + JWT_SECRET: test-secret + JWT_EXPIRY: 8h + FILE_STORAGE_ROOT: /tmp/rentaldrivego-test-storage + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + - run: npm ci + - run: npm run db:generate + - run: npm run db:deploy + - run: npx prisma db push --schema packages/database/prisma/schema.prisma + - run: npm run test:api:integration diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..721171f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "save-serve.language": "en" +} \ No newline at end of file diff --git a/docs/deployment_setup_servers/traefik.env b/docs/deployment_setup_servers/traefik.env new file mode 100644 index 0000000..94c0f4d --- /dev/null +++ b/docs/deployment_setup_servers/traefik.env @@ -0,0 +1,3 @@ +ACME_EMAIL=rentaldrivego@gmail.com +TRAEFIK_DOMAIN=rentaldrivego.ma +TRAEFIK_DOMAIN_WWW=www.rentaldrivego.ma \ No newline at end of file diff --git a/docs/deployment_setup_servers/traefik.yml b/docs/deployment_setup_servers/traefik.yml new file mode 100644 index 0000000..e6fd4dc --- /dev/null +++ b/docs/deployment_setup_servers/traefik.yml @@ -0,0 +1,36 @@ +services: + traefik: + image: traefik:latest + command: + - --api.dashboard=false + - --api.insecure=false + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.docker.network=traefik-proxy + - --providers.file.directory=/etc/traefik/dynamic + - --providers.file.watch=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.websecure.transport.respondingTimeouts.readTimeout=0 + - --entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0 + - --entrypoints.websecure.transport.respondingTimeouts.idleTimeout=1800s + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --certificatesresolvers.letsencrypt.acme.tlschallenge=true + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + restart: unless-stopped + ports: + - 80:80 + - 443:443 + volumes: + - traefik-letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./dynamic:/etc/traefik/dynamic:ro + networks: + - traefik-proxy +networks: + traefik-proxy: + external: true +volumes: + traefik-letsencrypt: