Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 102 additions & 25 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────
Expand All @@ -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
Expand All @@ -90,6 +92,7 @@ jobs:
- calendar-service
- notification-service
- payment-service
- security-service
- worker-service
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -127,34 +130,58 @@ 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"

# Pull new images.
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

Expand All @@ -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))
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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
9 changes: 9 additions & 0 deletions apps/bot/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ services:
# ── Бизнес-сервисы ────────────────────────────────────────────────────────────

booking-service:
image: ${IMAGE_PREFIX:-ghcr.io/hqzdev/metrix}/booking-service:${IMAGE_TAG:-latest}
build:
context: .
dockerfile: Dockerfile.service
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions docs/deployment/CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ CI (typecheck + tests + openapi + audit + contracts)
Каждый сервис публикуется в GHCR:

```
ghcr.io/<owner>/metrix/<service-name>:sha-<git-sha>
ghcr.io/<owner>/metrix/<service-name>:latest
ghcr.io/hqzdev/metrix/<service-name>:sha-<git-sha>
ghcr.io/hqzdev/metrix/<service-name>:latest
```

Тег `sha-<git-sha>` иммутабелен. `latest` всегда указывает на последний задеплоенный коммит.
GHCR/Docker repository path должен быть lowercase, иначе `docker buildx`
завершится ошибкой `repository name must be lowercase`.

## Окружения

Expand Down Expand Up @@ -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}/<service-name>:${IMAGE_TAG:-latest}` в `docker-compose.yml`.
4. Добавить `OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318` в environment нового сервиса в `docker-compose.yml`.
Loading