diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a093563..6d82a78 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -49,7 +49,7 @@ on: env: REGISTRY: ghcr.io - IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/metrix + IMAGE_PREFIX: ghcr.io/hqzdev/metrix jobs: # ── Guard: only proceed when CI workflow succeeded ───────────────────────── @@ -67,6 +67,8 @@ jobs: run: | if [[ -n "${{ inputs.image_tag }}" ]]; then echo "tag=${{ inputs.image_tag }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then + echo "tag=sha-${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" else echo "tag=sha-${{ github.sha }}" >> "$GITHUB_OUTPUT" fi @@ -90,6 +92,7 @@ jobs: - calendar-service - notification-service - payment-service + - security-service - worker-service steps: - uses: actions/checkout@v4 @@ -127,22 +130,46 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Validate staging deploy settings + env: + DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + SSH_HOST: ${{ secrets.SSH_HOST_STAGING }} + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + run: | + missing=0 + for name in DEPLOY_PATH SSH_HOST SSH_KNOWN_HOSTS SSH_PRIVATE_KEY SSH_USER; do + if [[ -z "${!name}" ]]; then + echo "::error::${name} is not configured for staging deployment" + missing=1 + fi + done + exit "$missing" + - name: Write SSH key + env: + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - name: Deploy to staging server env: - IMAGE_TAG: ${{ needs.guard.outputs.image_tag }} DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + IMAGE_PREFIX: ${{ env.IMAGE_PREFIX }} + IMAGE_TAG: ${{ needs.guard.outputs.image_tag }} + SSH_HOST: ${{ secrets.SSH_HOST_STAGING }} + SSH_USER: ${{ secrets.SSH_USER }} run: | + ssh_target="${SSH_USER}@${SSH_HOST}" ssh -i ~/.ssh/deploy_key \ -o StrictHostKeyChecking=yes \ - ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST_STAGING }} \ - bash -s << 'ENDSSH' + "$ssh_target" \ + "DEPLOY_PATH=${DEPLOY_PATH@Q} IMAGE_PREFIX=${IMAGE_PREFIX@Q} IMAGE_TAG=${IMAGE_TAG@Q} bash -s" << 'ENDSSH' set -euo pipefail cd "$DEPLOY_PATH/apps/bot" @@ -150,11 +177,11 @@ jobs: echo "${{ secrets.GITHUB_TOKEN }}" | \ docker login ghcr.io -u ${{ github.actor }} --password-stdin - IMAGE_PREFIX="${{ env.IMAGE_PREFIX }}" - TAG="${{ env.IMAGE_TAG }}" + TAG="${IMAGE_TAG}" for svc in admin-service analytics-service booking-service bot-gateway \ - calendar-service notification-service payment-service worker-service; do + calendar-service notification-service payment-service \ + security-service worker-service; do docker pull "${IMAGE_PREFIX}/${svc}:${TAG}" done @@ -165,7 +192,8 @@ jobs: export IMAGE_TAG="${TAG}" docker compose up -d --no-build \ admin-service analytics-service booking-service bot-gateway \ - calendar-service notification-service payment-service worker-service + calendar-service notification-service payment-service \ + security-service worker-service # Wait for all containers to be healthy (max 120 s). deadline=$(($(date +%s) + 120)) @@ -214,36 +242,60 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Validate production deploy settings + env: + DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + SSH_HOST: ${{ secrets.SSH_HOST_PRODUCTION }} + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + run: | + missing=0 + for name in DEPLOY_PATH SSH_HOST SSH_KNOWN_HOSTS SSH_PRIVATE_KEY SSH_USER; do + if [[ -z "${!name}" ]]; then + echo "::error::${name} is not configured for production deployment" + missing=1 + fi + done + exit "$missing" + - name: Write SSH key + env: + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - name: Deploy to production server env: - IMAGE_TAG: ${{ needs.guard.outputs.image_tag }} DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + IMAGE_PREFIX: ${{ env.IMAGE_PREFIX }} + IMAGE_TAG: ${{ needs.guard.outputs.image_tag }} + SSH_HOST: ${{ secrets.SSH_HOST_PRODUCTION }} + SSH_USER: ${{ secrets.SSH_USER }} run: | + ssh_target="${SSH_USER}@${SSH_HOST}" ssh -i ~/.ssh/deploy_key \ -o StrictHostKeyChecking=yes \ - ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST_PRODUCTION }} \ - bash -s << 'ENDSSH' + "$ssh_target" \ + "DEPLOY_PATH=${DEPLOY_PATH@Q} IMAGE_PREFIX=${IMAGE_PREFIX@Q} IMAGE_TAG=${IMAGE_TAG@Q} bash -s" << 'ENDSSH' set -euo pipefail cd "$DEPLOY_PATH/apps/bot" echo "${{ secrets.GITHUB_TOKEN }}" | \ docker login ghcr.io -u ${{ github.actor }} --password-stdin - IMAGE_PREFIX="${{ env.IMAGE_PREFIX }}" - TAG="${{ env.IMAGE_TAG }}" + TAG="${IMAGE_TAG}" # Save previous tag before overwriting — used by rollback. [[ -f .last-deployed-tag ]] && cp .last-deployed-tag .previous-deployed-tag for svc in admin-service analytics-service booking-service bot-gateway \ - calendar-service notification-service payment-service worker-service; do + calendar-service notification-service payment-service \ + security-service worker-service; do docker pull "${IMAGE_PREFIX}/${svc}:${TAG}" done @@ -252,7 +304,8 @@ jobs: export IMAGE_TAG="${TAG}" docker compose up -d --no-build \ admin-service analytics-service booking-service bot-gateway \ - calendar-service notification-service payment-service worker-service + calendar-service notification-service payment-service \ + security-service worker-service deadline=$(($(date +%s) + 120)) while [[ $(date +%s) -lt $deadline ]]; do @@ -290,25 +343,48 @@ jobs: name: Rollback production needs: smoke-production runs-on: ubuntu-latest - if: failure() + if: ${{ needs.smoke-production.result == 'failure' }} environment: name: production steps: + - name: Validate rollback settings + env: + DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + SSH_HOST: ${{ secrets.SSH_HOST_PRODUCTION }} + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + run: | + missing=0 + for name in DEPLOY_PATH SSH_HOST SSH_KNOWN_HOSTS SSH_PRIVATE_KEY SSH_USER; do + if [[ -z "${!name}" ]]; then + echo "::error::${name} is not configured for production rollback" + missing=1 + fi + done + exit "$missing" + - name: Write SSH key + env: + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - name: Rollback to previous image tag env: DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + SSH_HOST: ${{ secrets.SSH_HOST_PRODUCTION }} + SSH_USER: ${{ secrets.SSH_USER }} run: | + ssh_target="${SSH_USER}@${SSH_HOST}" ssh -i ~/.ssh/deploy_key \ -o StrictHostKeyChecking=yes \ - ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST_PRODUCTION }} \ - bash -s << 'ENDSSH' + "$ssh_target" \ + "DEPLOY_PATH=${DEPLOY_PATH@Q} bash -s" << 'ENDSSH' set -euo pipefail cd "$DEPLOY_PATH/apps/bot" @@ -323,7 +399,8 @@ jobs: export IMAGE_TAG="${PREV_TAG}" docker compose up -d --no-build \ admin-service analytics-service booking-service bot-gateway \ - calendar-service notification-service payment-service worker-service + calendar-service notification-service payment-service \ + security-service worker-service echo "Rollback complete. Previous tag: ${PREV_TAG}" ENDSSH diff --git a/apps/bot/docker-compose.yml b/apps/bot/docker-compose.yml index afda8d6..6fdf785 100644 --- a/apps/bot/docker-compose.yml +++ b/apps/bot/docker-compose.yml @@ -82,6 +82,7 @@ services: # ── Бизнес-сервисы ──────────────────────────────────────────────────────────── booking-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/booking-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -124,6 +125,7 @@ services: condition: service_healthy calendar-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/calendar-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -159,6 +161,7 @@ services: condition: service_healthy payment-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/payment-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -199,6 +202,7 @@ services: condition: service_started notification-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/notification-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -213,6 +217,7 @@ services: condition: service_healthy worker-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/worker-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -245,6 +250,7 @@ services: condition: service_healthy analytics-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/analytics-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -279,6 +285,7 @@ services: condition: service_healthy security-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/security-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -316,6 +323,7 @@ services: condition: service_healthy admin-service: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/admin-service:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service @@ -356,6 +364,7 @@ services: condition: service_started bot-gateway: + image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/bot-gateway:${IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile.service diff --git a/docs/deployment/CD.md b/docs/deployment/CD.md index 45f99f8..1406510 100644 --- a/docs/deployment/CD.md +++ b/docs/deployment/CD.md @@ -40,11 +40,13 @@ CI (typecheck + tests + openapi + audit + contracts) Каждый сервис публикуется в GHCR: ``` -ghcr.io//metrix/:sha- -ghcr.io//metrix/:latest +ghcr.io/hqzdev/metrix/:sha- +ghcr.io/hqzdev/metrix/:latest ``` Тег `sha-` иммутабелен. `latest` всегда указывает на последний задеплоенный коммит. +GHCR/Docker repository path должен быть lowercase, иначе `docker buildx` +завершится ошибкой `repository name must be lowercase`. ## Окружения @@ -103,4 +105,5 @@ Rollback происходит автоматически, если smoke-тес 1. Добавить имя сервиса в матрицу `build-push` в `cd.yml`. 2. Добавить имя сервиса в команды `docker compose up` в шагах `deploy-staging` и `deploy-production`. -3. Добавить `OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318` в environment нового сервиса в `docker-compose.yml`. +3. Добавить `image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/:${IMAGE_TAG:-latest}` в `docker-compose.yml`. +4. Добавить `OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318` в environment нового сервиса в `docker-compose.yml`.