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" - 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 docker compose -f '${DEPLOY_COMPOSE_FILE}' up -d --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