Files
iistwin/.github/workflows/deploy.yml
Ильяс Султанов d0d73a59f2 ci(deploy): app-образ собирается в GitHub Actions и пушится в GHCR — сервер только скачивает
Раньше образ собирался на VPS (3 ядра) при каждом деплое: 13–15 минут даунтайма.
Теперь: job build-image (buildx + кэш слоёв GHA) пушит ghcr.io/iisultanov/iistwin-app:latest
и :<sha>; SSH-скрипт скачивает образы ДО остановки контейнеров (даунтайм ~10 секунд
на подмену), fallback на сборку на сервере сохранён при недоступности GHCR.
Инфра-репо: сервису app добавлен image: (iistwin-infra@0960563).
2026-09-21 20:59:31 +03:00

229 lines
9.6 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: Deploy to Server
on:
push:
branches:
- main
schedule:
# Daily production deploy at 03:00 UTC.
# All feature branches merged into main during the day are deployed at this time.
- cron: '0 3 * * *'
workflow_dispatch:
# Manual deploy for emergencies only.
jobs:
check:
name: Type Check & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
# googleapis добавляет очень тяжёлые .d.ts — без увеличения heap tsc падает с OOM (Ineffective mark-compacts)
env:
NODE_OPTIONS: --max-old-space-size=6144
run: npm run check
- name: Type check document worker
run: |
cd server/documents/worker
npm ci
npm run check
- name: Build
run: npm run build
build-image:
name: Build & Push App Image
needs: check
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Готовый образ app для продакшена — как у document-worker.
# Раньше образ собирался на слабом VPS (3 ядра) при каждом деплое: 13–15 минут
# даунтайма. Теперь собирается здесь (быстрые раннеры + кэш слоёв GHA),
# а сервер только скачивает его.
- name: Build and push app image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/iisultanov/iistwin-app:latest
ghcr.io/iisultanov/iistwin-app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_SHA=${{ github.sha }}
deploy:
name: Deploy to VPS
needs: build-image
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
timeout-minutes: 50
concurrency:
group: production-deploy
cancel-in-progress: false
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
env:
GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
timeout: 60s
command_timeout: 45m
envs: GHCR_USERNAME,GHCR_TOKEN
script: |
set -e
cd /opt/crm
# 1. Получить ровно ту версию кода, что в GitHub (без merge-конфликтов)
git fetch origin main
git reset --hard origin/main
# 2. Прод-compose — из приватного инфра-репозитория (единственный источник правды).
# Раньше: git stash кастомизаций + копия из бэкапа вне репо (дрейф, шаг 0.14).
# Файл используется напрямую с -f (без копирования в /opt/crm) —
# git status основного репо на сервере остаётся чистым.
if [ ! -d /opt/crm-infra/.git ]; then
GIT_SSH_COMMAND="ssh -i /home/deploy/.ssh/id_ed25519_infra" \
git clone git@github.com:iisultanov/iistwin-infra.git /opt/crm-infra
else
git -C /opt/crm-infra fetch origin main
git -C /opt/crm-infra reset --hard origin/main
fi
COMPOSE="docker compose -f /opt/crm-infra/docker-compose.prod.yml --project-directory /opt/crm"
# 5. Логин в GitHub Container Registry для скачивания образов
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin
# 5.5 Скачать свежие образы, ПОКА СТАРЫЕ КОНТЕЙНЕРЫ ЕЩЁ РАБОТАЮТ —
# даунтайм останется только на подмену контейнеров (~10 секунд),
# а не на всю сборку/скачивание. Образ app собирается в CI (GHCR),
# fallback на сборку на сервере — ниже, после остановки.
APP_PULL_OK=false
if $COMPOSE pull app; then
APP_PULL_OK=true
echo "App image pulled from GHCR"
else
echo "GHCR pull failed — will build on server (fallback)"
fi
$COMPOSE pull document-worker || true
# 5.6 Удалить старые/зависшие контейнеры, чтобы избежать
# Conflict: container name is already in use
docker stop crm-app-1 crm-document-worker-1 2>/dev/null || true
docker rm -f crm-app-1 crm-document-worker-1 2>/dev/null || true
$COMPOSE stop app document-worker || true
$COMPOSE rm -f app document-worker || true
docker ps -aq --filter "name=crm-app" | xargs -r docker rm -f || true
docker ps -aq --filter "name=crm-document-worker" | xargs -r docker rm -f || true
# 5.7 Мягкая очистка: удалить dangling образы, но оставить build cache
# для возможной fallback-сборки CRM.
docker system prune -f
# 6. Fallback: если app-образ не скачался из GHCR — собрать на сервере.
# BUILD_ARG инвалидирует слой сборки фронтенда, если код изменился,
# но позволяет Docker кэшировать слои npm ci.
# Retry 3 times to work around transient Alpine mirror I/O errors.
if [ "$APP_PULL_OK" != "true" ]; then
BUILD_SHA=${{ github.sha }}
BUILD_OK=false
for attempt in 1 2 3; do
echo "=== Docker build attempt $attempt ==="
if $COMPOSE build --build-arg BUILD_SHA=$BUILD_SHA --build-arg ALPINE_MIRROR=mirrors.aliyun.com app; then
BUILD_OK=true
break
fi
echo "Build failed on attempt $attempt"
if [ "$attempt" -lt 3 ]; then
echo "Cleaning Docker cache and retrying in 30s..."
docker system prune -a -f
docker builder prune -f
sleep 30
fi
done
if [ "$BUILD_OK" != "true" ]; then
echo "Docker build failed after 3 attempts"
exit 1
fi
fi
$COMPOSE up -d app document-worker
# 7. Дождаться healthcheck'ов
echo "Waiting for healthchecks..."
APP_HEALTHY=false
WORKER_HEALTHY=false
for i in $(seq 1 30); do
if [ "$APP_HEALTHY" != "true" ] && docker ps | grep crm-app | grep -q healthy; then
echo "iistwin app is healthy!"
APP_HEALTHY=true
fi
if [ "$WORKER_HEALTHY" != "true" ] && docker ps | grep crm-document-worker | grep -q healthy; then
echo "document-worker is healthy!"
WORKER_HEALTHY=true
fi
if [ "$APP_HEALTHY" = "true" ] && [ "$WORKER_HEALTHY" = "true" ]; then
break
fi
sleep 2
done
if [ "$APP_HEALTHY" != "true" ]; then
echo "App healthcheck failed"
exit 1
fi
if [ "$WORKER_HEALTHY" != "true" ]; then
echo "Document worker healthcheck failed"
exit 1
fi
# 8. Мягкая очистка Docker: оставляем build cache и tagged базовые образы,
# чтобы следующий деплой переиспользовал слои npm ci и сборки.
# Полная очистка запускается ежедневно через cron (disk-check.sh / docker-cleanup.sh).
docker system prune -f
docker builder prune -f --keep-storage 5gb
# 9. Удалить старые образы app, чтобы они не копились между деплоями.
# Текущий образ используется запущенным контейнером, поэтому он не удалится.
CURRENT_IMAGE_ID=$(docker inspect --format='{{.Image}}' crm-app-1)
docker images ghcr.io/iisultanov/iistwin-app --format "{{.ID}}" | grep -v "$CURRENT_IMAGE_ID" | xargs -r docker rmi -f || true
# Легаси-образы эпохи сборки на сервере (crm-app) — больше не используются.
docker images crm-app --format "{{.ID}}" | xargs -r docker rmi -f || true
echo "Cleanup completed"