stages: - test - build - deploy variables: DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA DOCKER_IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest DOCKERFILE_PATH: Dockerfile.production # Required: disable TLS so the docker client can reach the dind daemon DOCKER_TLS_CERTDIR: "" # ==================================== # TEST STAGE # Runs on every push and merge request # ==================================== unit_tests: stage: test image: node:20-bookworm before_script: - npm ci - npm run db:generate script: - npm run type-check - npm run test:api rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "main" integration_tests: stage: test image: node:20-bookworm services: - name: postgres:16-alpine alias: postgres variables: POSTGRES_DB: rentaldrivego_test POSTGRES_USER: postgres POSTGRES_PASSWORD: password - name: redis:7-alpine alias: redis variables: # Used by db:deploy (Prisma migrations) and any code reading DATABASE_URL directly DATABASE_URL: "postgresql://postgres:password@postgres:5432/rentaldrivego_test" REDIS_URL: "redis://redis:6379" NODE_ENV: test before_script: - npm ci - npm run db:generate # Overwrite the local .env.test so vitest integration config gets CI service hostnames # (the file uses postgres/redis aliases, not localhost) - | cat > apps/api/.env.test << 'EOF' DATABASE_URL=postgresql://postgres:password@postgres:5432/rentaldrivego_test REDIS_URL=redis://redis:6379 JWT_SECRET=test-secret JWT_EXPIRY=8h NODE_ENV=test FILE_STORAGE_ROOT=/tmp/rentaldrivego-test-storage EOF - npm run db:deploy script: - npm run test:api:integration rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "main" # ==================================== # BUILD STAGE # ==================================== build_image: stage: build image: docker:24 services: - name: docker:24-dind alias: docker before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - echo "--- Building Docker Image ---" # NEXT_PUBLIC_* vars are inlined into Next.js bundles at build time — must be passed as ARGs - | docker build \ --build-arg NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --build-arg NEXT_PUBLIC_MARKETPLACE_URL=$NEXT_PUBLIC_MARKETPLACE_URL \ --build-arg NEXT_PUBLIC_DASHBOARD_URL=$NEXT_PUBLIC_DASHBOARD_URL \ --build-arg NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \ -t $DOCKER_IMAGE \ -t $DOCKER_IMAGE_LATEST \ -f $DOCKERFILE_PATH . - echo "--- Pushing to Registry ---" - docker push $DOCKER_IMAGE - docker push $DOCKER_IMAGE_LATEST rules: - if: $CI_COMMIT_BRANCH == "main" # ==================================== # DEPLOY STAGE # ==================================== deploy_to_vps: stage: deploy image: alpine:latest needs: - build_image before_script: - apk add --no-cache openssh-client # Load the SSH private key stored in the CI variable SSH_PRIVATE_KEY - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh && chmod 700 ~/.ssh # Scan and trust the VPS host key (avoids manual known_hosts management) - ssh-keyscan -H $VPS_IP >> ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts script: - echo "--- Deploying $DOCKER_IMAGE to VPS ---" - | ssh $VPS_USER@$VPS_IP " set -e cd /opt/rentaldrivego echo '-- Pulling latest code --' git pull echo '-- Logging into registry --' docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY echo '-- Pulling pre-built image --' docker pull $DOCKER_IMAGE echo '-- Restarting services --' APP_VERSION=$CI_COMMIT_SHORT_SHA \ docker compose -p rentaldrivego-prod \ --env-file .env.docker.production \ -f docker-compose.production.yml \ up -d --no-build echo '-- Pruning old images --' docker image prune -f " rules: - if: $CI_COMMIT_BRANCH == "main"