226 lines
9.9 KiB
YAML
226 lines
9.9 KiB
YAML
stages:
|
|
- test
|
|
- build
|
|
- deploy
|
|
|
|
variables:
|
|
DOCKERFILE_PATH: Dockerfile.production
|
|
DEPLOY_ROOT: /opt/rentaldrivego
|
|
# 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:
|
|
- |
|
|
attempts=0
|
|
until docker info >/dev/null 2>&1; do
|
|
attempts=$((attempts + 1))
|
|
if [ "$attempts" -ge 60 ]; then
|
|
echo "Docker daemon did not become ready after 60 seconds." >&2
|
|
echo "On self-hosted runners, docker:dind requires the runner Docker executor to run with privileged = true." >&2
|
|
exit 1
|
|
fi
|
|
echo "Waiting for Docker daemon..."
|
|
sleep 1
|
|
done
|
|
# Use one registry mode at a time:
|
|
# 1) explicit REGISTRY_* vars for external/custom registries
|
|
# 2) GitLab's built-in CI_REGISTRY_* vars for the integrated registry
|
|
- |
|
|
if [ -n "${REGISTRY_HOST:-}${REGISTRY_USER:-}${REGISTRY_PASSWORD:-}${REGISTRY_IMAGE:-}" ]; then
|
|
test -n "${REGISTRY_HOST:-}" || { echo "REGISTRY_HOST is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
test -n "${REGISTRY_USER:-}" || { echo "REGISTRY_USER is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
test -n "${REGISTRY_PASSWORD:-}" || { echo "REGISTRY_PASSWORD is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
test -n "${REGISTRY_IMAGE:-}" || { echo "REGISTRY_IMAGE is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
export IMAGE_REPOSITORY="$REGISTRY_IMAGE"
|
|
else
|
|
export REGISTRY_HOST="${CI_REGISTRY:-}"
|
|
export REGISTRY_USER="${CI_REGISTRY_USER:-}"
|
|
export REGISTRY_PASSWORD="${CI_REGISTRY_PASSWORD:-}"
|
|
export IMAGE_REPOSITORY="${CI_REGISTRY_IMAGE:-}"
|
|
fi
|
|
- 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 Connectivity Preflight ---"
|
|
echo "REGISTRY_HOST=$REGISTRY_HOST"
|
|
cat /etc/resolv.conf || true
|
|
|
|
if command -v getent >/dev/null 2>&1; then
|
|
getent hosts "$REGISTRY_HOST" || true
|
|
elif command -v nslookup >/dev/null 2>&1; then
|
|
nslookup "$REGISTRY_HOST" || true
|
|
else
|
|
echo "No host lookup tool available in runner image."
|
|
fi
|
|
|
|
probe_output="$(wget -S --spider --timeout=15 --tries=1 "https://$REGISTRY_HOST/v2/" 2>&1 || true)"
|
|
printf '%s\n' "$probe_output"
|
|
|
|
printf '%s\n' "$probe_output" | grep -F "401 Unauthorized" >/dev/null || {
|
|
echo "Registry preflight failed: expected HTTPS /v2/ to return 401 Unauthorized before docker login." >&2
|
|
exit 1
|
|
}
|
|
|
|
printf '%s\n' "$probe_output" | grep -Fi "Docker-Distribution-Api-Version: registry/2.0" >/dev/null || {
|
|
echo "Registry preflight failed: registry API header missing from HTTPS /v2/ response." >&2
|
|
exit 1
|
|
}
|
|
- 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:
|
|
# On the default branch, always attempt the image build so missing registry
|
|
# configuration fails loudly in before_script instead of skipping the job.
|
|
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
- when: never
|
|
|
|
# ====================================
|
|
# DEPLOY STAGE
|
|
# ====================================
|
|
deploy_to_vps:
|
|
stage: deploy
|
|
image: alpine:latest
|
|
needs:
|
|
- build_image
|
|
before_script:
|
|
- |
|
|
if [ -n "${REGISTRY_HOST:-}${REGISTRY_USER:-}${REGISTRY_PASSWORD:-}${REGISTRY_IMAGE:-}" ]; then
|
|
test -n "${REGISTRY_HOST:-}" || { echo "REGISTRY_HOST is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
test -n "${REGISTRY_USER:-}" || { echo "REGISTRY_USER is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
test -n "${REGISTRY_PASSWORD:-}" || { echo "REGISTRY_PASSWORD is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
test -n "${REGISTRY_IMAGE:-}" || { echo "REGISTRY_IMAGE is not set. Configure all REGISTRY_* variables together."; exit 1; }
|
|
export IMAGE_REPOSITORY="$REGISTRY_IMAGE"
|
|
else
|
|
export REGISTRY_HOST="${CI_REGISTRY:-}"
|
|
export REGISTRY_USER="${CI_REGISTRY_USER:-}"
|
|
export REGISTRY_PASSWORD="${CI_REGISTRY_PASSWORD:-}"
|
|
export IMAGE_REPOSITORY="${CI_REGISTRY_IMAGE:-}"
|
|
fi
|
|
- 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 ---"
|
|
- |
|
|
REGISTRY_PASSWORD_B64="$(printf '%s' "$REGISTRY_PASSWORD" | base64 | tr -d '\n')"
|
|
|
|
echo "-- Syncing deployment assets --"
|
|
ssh $VPS_USER@$VPS_IP "
|
|
set -e
|
|
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 $VPS_USER@$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 $VPS_USER@$VPS_IP:$DEPLOY_ROOT/scripts/
|
|
scp docker/pgmanage/override.py $VPS_USER@$VPS_IP:$DEPLOY_ROOT/docker/pgmanage/
|
|
|
|
echo "-- Running deploy script on VPS --"
|
|
ssh $VPS_USER@$VPS_IP "
|
|
set -e
|
|
cd '$DEPLOY_ROOT'
|
|
APP_IMAGE='$IMAGE_REPOSITORY' \
|
|
APP_VERSION='$CI_COMMIT_SHORT_SHA' \
|
|
REGISTRY_HOST='$REGISTRY_HOST' \
|
|
REGISTRY_USER='$REGISTRY_USER' \
|
|
REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \
|
|
bash scripts/docker-prod-deploy.sh
|
|
"
|
|
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
|