stages: - test - build - deploy variables: DOCKERFILE_PATH: Dockerfile.production # Required: disable TLS so the docker client can reach the dind daemon DOCKER_TLS_CERTDIR: "" DOCKER_HOST: tcp://docker:2375 # ==================================== # 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 == $CI_DEFAULT_BRANCH 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 == $CI_DEFAULT_BRANCH # ==================================== # BUILD STAGE # ==================================== build_image: stage: build image: docker:24 services: - name: docker:24-dind alias: docker command: ["--tls=false"] variables: HEALTHCHECK_TCP_PORT: "2375" before_script: - until docker info >/dev/null 2>&1; do echo "Waiting for Docker daemon..."; sleep 1; done # Prefer GitLab's built-in registry variables, but allow explicit fallbacks # so the pipeline can publish to any OCI-compatible registry. - export REGISTRY_HOST="${CI_REGISTRY:-${REGISTRY_HOST:-}}" - export REGISTRY_USER="${CI_REGISTRY_USER:-${REGISTRY_USER:-}}" - export REGISTRY_PASSWORD="${CI_REGISTRY_PASSWORD:-${REGISTRY_PASSWORD:-}}" - export IMAGE_REPOSITORY="${CI_REGISTRY_IMAGE:-${REGISTRY_IMAGE:-}}" - test -n "$REGISTRY_HOST" || { echo "Registry host is not set. Configure CI_REGISTRY or REGISTRY_HOST."; exit 1; } - test -n "$REGISTRY_USER" || { echo "Registry user is not set. Configure CI_REGISTRY_USER or REGISTRY_USER."; exit 1; } - test -n "$REGISTRY_PASSWORD" || { echo "Registry password is not set. Configure CI_REGISTRY_PASSWORD or REGISTRY_PASSWORD."; exit 1; } - test -n "$IMAGE_REPOSITORY" || { echo "Image repository is not set. Configure CI_REGISTRY_IMAGE or REGISTRY_IMAGE."; exit 1; } - export DOCKER_IMAGE="$IMAGE_REPOSITORY:$CI_COMMIT_SHORT_SHA" - export DOCKER_IMAGE_LATEST="$IMAGE_REPOSITORY:latest" - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin 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: # Skip the container publish step unless either the GitLab registry or an # explicit fallback registry configuration is available. - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && (($CI_REGISTRY && $CI_REGISTRY_USER && $CI_REGISTRY_PASSWORD && $CI_REGISTRY_IMAGE) || ($REGISTRY_HOST && $REGISTRY_USER && $REGISTRY_PASSWORD && $REGISTRY_IMAGE))' - when: never # ==================================== # DEPLOY STAGE # ==================================== deploy_to_vps: stage: deploy image: alpine:latest needs: - build_image before_script: - export REGISTRY_HOST="${CI_REGISTRY:-${REGISTRY_HOST:-}}" - export REGISTRY_USER="${CI_REGISTRY_USER:-${REGISTRY_USER:-}}" - export REGISTRY_PASSWORD="${CI_REGISTRY_PASSWORD:-${REGISTRY_PASSWORD:-}}" - export IMAGE_REPOSITORY="${CI_REGISTRY_IMAGE:-${REGISTRY_IMAGE:-}}" - test -n "$REGISTRY_HOST" || { echo "Registry host is not set. Configure CI_REGISTRY or REGISTRY_HOST."; exit 1; } - test -n "$REGISTRY_USER" || { echo "Registry user is not set. Configure CI_REGISTRY_USER or REGISTRY_USER."; exit 1; } - test -n "$REGISTRY_PASSWORD" || { echo "Registry password is not set. Configure CI_REGISTRY_PASSWORD or REGISTRY_PASSWORD."; exit 1; } - test -n "$IMAGE_REPOSITORY" || { echo "Image repository is not set. Configure CI_REGISTRY_IMAGE or REGISTRY_IMAGE."; exit 1; } - export DOCKER_IMAGE="$IMAGE_REPOSITORY:$CI_COMMIT_SHORT_SHA" - 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 --' test -n '$REGISTRY_HOST' || { echo 'Registry host is not set.'; exit 1; } test -n '$REGISTRY_USER' || { echo 'Registry user is not set.'; exit 1; } test -n '$REGISTRY_PASSWORD' || { echo 'Registry password is not set.'; exit 1; } echo '$REGISTRY_PASSWORD' | docker login '$REGISTRY_HOST' -u '$REGISTRY_USER' --password-stdin echo '-- Pulling pre-built image --' docker pull $DOCKER_IMAGE echo '-- Ensuring uploads volume exists --' docker volume create rentaldrivego-prod_api_uploads >/dev/null echo '-- Restarting services --' APP_IMAGE=$IMAGE_REPOSITORY \ 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: # Deploy only when both registry access and VPS credentials are available. - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && ((($CI_REGISTRY && $CI_REGISTRY_USER && $CI_REGISTRY_PASSWORD && $CI_REGISTRY_IMAGE) || ($REGISTRY_HOST && $REGISTRY_USER && $REGISTRY_PASSWORD && $REGISTRY_IMAGE)) && $SSH_PRIVATE_KEY && $VPS_IP && $VPS_USER)' - when: never