Files
call-center/.gitlab-ci.yml
T

319 lines
12 KiB
YAML

stages:
- test
- build
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
CI_PYTHON_PACKAGES_DIR: "$CI_PROJECT_DIR/.ci-packages"
APP_IMAGE_REPOSITORY: "$CI_REGISTRY_IMAGE/app"
ASTERISK_IMAGE_REPOSITORY: "$CI_REGISTRY_IMAGE/asterisk"
APP_IMAGE_TAG: "$CI_COMMIT_SHA"
ASTERISK_IMAGE_TAG: "$CI_COMMIT_SHA"
APP_IMAGE: "$CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA"
ASTERISK_IMAGE: "$CI_REGISTRY_IMAGE/asterisk:$CI_COMMIT_SHA"
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: "tcp://docker:2376"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
PYTHON_BIN: "python3"
CI_POSTGRES_ADMIN_URL: ""
.python_job:
tags:
- crm-shell
before_script:
- rm -rf "$CI_PYTHON_PACKAGES_DIR"
- mkdir -p "$CI_PYTHON_PACKAGES_DIR"
- $PYTHON_BIN -m pip install --target "$CI_PYTHON_PACKAGES_DIR" -r requirements.txt
- export PYTHONPATH="$CI_PYTHON_PACKAGES_DIR${PYTHONPATH:+:$PYTHONPATH}"
test:
extends: .python_job
stage: test
script:
- $PYTHON_BIN -m pytest -q --ignore=tests/test_realtime_voice_service.py
allow_failure: true
postgres-readiness:
extends: .python_job
stage: test
rules:
- if: '$ENABLE_POSTGRES_READINESS == "1" && $CI_POSTGRES_ADMIN_URL'
- when: never
variables:
SCHEMA_MANAGEMENT_MODE: migrations
CC_DATA_DIR: .testdata_pg
EVENT_BUS_ENABLED: "0"
APP_TOKEN_SECRET: ci-secret
ALLOW_LEGACY_HEADER_AUTH: "1"
TELEGRAM_BOT_ENABLED: "0"
WHATSAPP_OUTBOUND_ENABLED: "0"
AI_PROVIDER: stub
AI_TELEGRAM_ENABLED: "0"
AI_WHATSAPP_ENABLED: "0"
AI_VOICE_ENABLED: "0"
script:
- |
export CI_PG_DB="mvpcc_ci_${CI_PIPELINE_IID}_${CI_JOB_ID}"
$PYTHON_BIN - <<'PY'
import os
import psycopg
admin_url = os.environ["CI_POSTGRES_ADMIN_URL"]
db_name = os.environ["CI_PG_DB"]
with psycopg.connect(admin_url, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute(f'DROP DATABASE IF EXISTS "{db_name}" WITH (FORCE)')
cur.execute(f'CREATE DATABASE "{db_name}"')
PY
- export DATABASE_URL="postgresql://postgres@127.0.0.1:5433/${CI_PG_DB}"
- $PYTHON_BIN scripts/migrate_core_db.py
- $PYTHON_BIN -m pytest -q tests/test_postgres_readiness.py
after_script:
- |
export CI_PG_DB="mvpcc_ci_${CI_PIPELINE_IID}_${CI_JOB_ID}"
$PYTHON_BIN - <<'PY'
import os
import psycopg
admin_url = os.environ["CI_POSTGRES_ADMIN_URL"]
db_name = os.environ["CI_PG_DB"]
with psycopg.connect(admin_url, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute(f'DROP DATABASE IF EXISTS "{db_name}" WITH (FORCE)')
PY
.docker_build:
tags:
- call-center-prod
variables:
DOCKER_HOST: ""
DOCKER_TLS_VERIFY: ""
DOCKER_CERT_PATH: ""
DOCKER_TLS_CERTDIR: ""
image: docker:27
services:
- name: docker:27-dind
alias: docker
before_script:
- command -v docker >/dev/null
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
rules:
- if: '$ENABLE_REGISTRY_BUILD == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$ENABLE_REGISTRY_BUILD == "1" && $CI_COMMIT_TAG'
- when: never
build-app-image:
extends: .docker_build
stage: build
needs:
- test
- job: postgres-readiness
optional: true
script:
- docker build -t "$APP_IMAGE" .
- docker push "$APP_IMAGE"
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag "$APP_IMAGE" "$APP_IMAGE_REPOSITORY:latest"
docker push "$APP_IMAGE_REPOSITORY:latest"
fi
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag "$APP_IMAGE" "$APP_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
docker push "$APP_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
fi
build-asterisk-image:
extends: .docker_build
stage: build
needs:
- test
- job: postgres-readiness
optional: true
script:
- docker build -f deployment/asterisk/Dockerfile -t "$ASTERISK_IMAGE" .
- docker push "$ASTERISK_IMAGE"
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag "$ASTERISK_IMAGE" "$ASTERISK_IMAGE_REPOSITORY:latest"
docker push "$ASTERISK_IMAGE_REPOSITORY:latest"
fi
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag "$ASTERISK_IMAGE" "$ASTERISK_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
docker push "$ASTERISK_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
fi
deploy-production:
stage: deploy
tags:
- call-center-prod
image: alpine:3.21
needs:
- build-app-image
- build-asterisk-image
environment:
name: production
before_script:
- command -v ssh >/dev/null
- command -v rsync >/dev/null
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- printf '%s\n' "$DEPLOY_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
script:
- test -n "$DEPLOY_HOST"
- test -n "$DEPLOY_USER"
- test -n "$DEPLOY_PATH"
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_PATH/deployment'"
- rsync -av deployment/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/deployment/"
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"mkdir -p '$DEPLOY_PATH/.data_local/generated_ivr_yandex/ivr' '$DEPLOY_PATH/.data_local/recordings' '$DEPLOY_PATH/.asterisk_assets/ivr'"
- |
if [ -d .asterisk_assets/ ]; then
rsync -av .asterisk_assets/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/.asterisk_assets/"
fi
- |
if [ -d .asterisk_assets/ivr/ ]; then
rsync -av --delete .asterisk_assets/ivr/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/.data_local/generated_ivr_yandex/ivr/"
fi
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"printf 'APP_IMAGE=%s\nASTERISK_IMAGE=%s\n' '$APP_IMAGE' '$ASTERISK_IMAGE' > '$DEPLOY_PATH/deployment/.env.images'"
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"echo '$CI_REGISTRY_PASSWORD' | docker login '$CI_REGISTRY' -u '$CI_REGISTRY_USER' --password-stdin && \
cd '$DEPLOY_PATH/deployment' && \
docker compose --env-file .env.images -f docker-compose.server.registry.yml pull && \
docker compose --env-file .env.images -f docker-compose.server.registry.yml up -d && \
docker compose --env-file .env.images -f docker-compose.asterisk.server.registry.yml pull && \
docker compose --env-file .env.images -f docker-compose.asterisk.server.registry.yml up -d"
rules:
- if: '$ENABLE_REGISTRY_BUILD == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- when: never
allow_failure: false
deploy-production-source:
stage: deploy
tags:
- call-center-prod
needs:
- test
- job: postgres-readiness
optional: true
variables:
DOCKER_HOST: ""
DOCKER_TLS_VERIFY: ""
DOCKER_CERT_PATH: ""
DOCKER_TLS_CERTDIR: ""
APP_IMAGE_NAME: call-center-app
environment:
name: production
before_script:
- command -v docker >/dev/null
- command -v ssh >/dev/null
- command -v scp >/dev/null
- command -v gzip >/dev/null
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- cp "$DEPLOY_SSH_PRIVATE_KEY" ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
script:
- test -n "$DEPLOY_HOST"
- test -n "$DEPLOY_USER"
- test -n "$DEPLOY_COMPOSE_FILE"
- IMAGE_ARCHIVE="call-center-app-${CI_COMMIT_SHORT_SHA}.tar.gz"
- docker build -t "${APP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" -t "${APP_IMAGE_NAME}:active-main" -t "${APP_IMAGE_NAME}:latest" -t "${APP_IMAGE_NAME}:ai-orchestrator" -t "${APP_IMAGE_NAME}:streaming-asr" -t "${APP_IMAGE_NAME}:asterisk-bridge" -t "${APP_IMAGE_NAME}:reporting-supervisor" .
- docker save "${APP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" "${APP_IMAGE_NAME}:active-main" "${APP_IMAGE_NAME}:latest" "${APP_IMAGE_NAME}:ai-orchestrator" "${APP_IMAGE_NAME}:streaming-asr" "${APP_IMAGE_NAME}:asterisk-bridge" "${APP_IMAGE_NAME}:reporting-supervisor" | gzip > "$IMAGE_ARCHIVE"
- |
DEPLOY_COMPOSE_DIR="${DEPLOY_COMPOSE_FILE%/*}"
if [ "$DEPLOY_COMPOSE_DIR" = "$DEPLOY_COMPOSE_FILE" ]; then
DEPLOY_COMPOSE_DIR="."
fi
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p '$DEPLOY_COMPOSE_DIR'"
scp -r deployment/. "${DEPLOY_USER}@${DEPLOY_HOST}:$DEPLOY_COMPOSE_DIR/"
- scp "$IMAGE_ARCHIVE" "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/$IMAGE_ARCHIVE"
- |
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "
set -e
gunzip -c /tmp/$IMAGE_ARCHIVE | sudo docker load
rm -f /tmp/$IMAGE_ARCHIVE
sudo env CALL_CENTER_IMAGE='${APP_IMAGE_NAME}:active-main' APP_IMAGE='${APP_IMAGE_NAME}:active-main' docker compose -f '${DEPLOY_COMPOSE_FILE}' config -q
sudo env CALL_CENTER_IMAGE='${APP_IMAGE_NAME}:active-main' APP_IMAGE='${APP_IMAGE_NAME}:active-main' docker compose -f '${DEPLOY_COMPOSE_FILE}' up -d --no-build --remove-orphans --force-recreate
if sudo docker ps -a --format '{{.Names}}' | grep -Fxq 'call-center-ai-voice-runtime-service-1'; then
sudo docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' call-center-ai-voice-runtime-service-1 > /tmp/call-center-ai-voice-runtime.env
sudo docker rm -f call-center-ai-voice-runtime-service-1
sudo docker run -d --name call-center-ai-voice-runtime-service-1 --restart unless-stopped --network call-center_default --network-alias ai-voice-runtime-service -v /opt/konturai-migration/call-center/.data_local:/app/.data_local --add-host host.docker.internal:host-gateway --env-file /tmp/call-center-ai-voice-runtime.env call-center-app:active-main sh -c 'uvicorn \${APP_MODULE} --host 0.0.0.0 --port 8000'
rm -f /tmp/call-center-ai-voice-runtime.env
fi
sudo docker image prune -f --filter dangling=true
sudo docker ps --filter 'name=^call-center-app$' --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
"
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
allow_failure: false
hotfix-analyst-assets:
stage: deploy
tags:
- call-center-prod
variables:
DOCKER_HOST: ""
DOCKER_TLS_VERIFY: ""
DOCKER_CERT_PATH: ""
DOCKER_TLS_CERTDIR: ""
needs:
- test
script:
- command -v docker >/dev/null
- |
GATEWAY_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E '^(call-center-app|call-center-api-gateway($|[-_]))' | head -n1)"
test -n "$GATEWAY_CONTAINER"
echo "Using gateway container: $GATEWAY_CONTAINER"
docker exec "$GATEWAY_CONTAINER" sh -lc 'mkdir -p /app/gateway /app/ui/analyst /app/ui/operator'
docker cp gateway/app.py "$GATEWAY_CONTAINER:/app/gateway/app.py"
docker cp ui/analyst/index.html "$GATEWAY_CONTAINER:/app/ui/analyst/index.html"
docker cp ui/analyst/app.js "$GATEWAY_CONTAINER:/app/ui/analyst/app.js"
docker cp ui/analyst/mock-analytics.json "$GATEWAY_CONTAINER:/app/ui/analyst/mock-analytics.json"
docker cp ui/operator/styles.css "$GATEWAY_CONTAINER:/app/ui/operator/styles.css"
docker restart "$GATEWAY_CONTAINER"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- when: never
allow_failure: false
prune-demo-data-production:
stage: deploy
tags:
- call-center-prod
needs:
- deploy-production-source
variables:
DOCKER_HOST: ""
DOCKER_TLS_VERIFY: ""
DOCKER_CERT_PATH: ""
DOCKER_TLS_CERTDIR: ""
DEPLOY_DIR: /home/gitlab-runner/deploy/call-center
KEEP_LATEST_INTERACTIONS: "6"
script:
- command -v python3 >/dev/null
- test -d "$DEPLOY_DIR"
- test -f "$DEPLOY_DIR/.env.production"
- mkdir -p "$DEPLOY_DIR/.db_backups"
- python3 scripts/prune_demo_interactions.py --env-file "$DEPLOY_DIR/.env.production" --base-dir "$DEPLOY_DIR" --backup-dir "$DEPLOY_DIR/.db_backups" --keep-latest-interactions "$KEEP_LATEST_INTERACTIONS"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- when: never
allow_failure: false