diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0627a5e..1a5cff1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,9 +44,47 @@ jobs: - 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: check + needs: build-image runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' timeout-minutes: 50 @@ -88,7 +126,23 @@ jobs: fi COMPOSE="docker compose -f /opt/crm-infra/docker-compose.prod.yml --project-directory /opt/crm" - # 5. Удалить старые/зависшие контейнеры, чтобы избежать + # 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 @@ -97,42 +151,37 @@ jobs: 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.5 Логин в GitHub Container Registry для скачивания prebuilt образа document-worker - echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin - - # 5.6 Мягкая очистка: удалить старые контейнеры и dangling образы, - # но оставить build cache для ускорения сборки CRM. + # 5.7 Мягкая очистка: удалить dangling образы, но оставить build cache + # для возможной fallback-сборки CRM. docker system prune -f - # 6. Собрать и запустить приложение. - # document-worker больше не собирается здесь — используется готовый образ из GHCR. + # 6. Fallback: если app-образ не скачался из GHCR — собрать на сервере. # BUILD_ARG инвалидирует слой сборки фронтенда, если код изменился, # но позволяет Docker кэшировать слои npm ci. # Retry 3 times to work around transient Alpine mirror I/O errors. - 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 + 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 - 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 - # 6.5 Получить свежий prebuilt образ document-worker из GHCR - $COMPOSE pull document-worker - $COMPOSE up -d app document-worker # 7. Дождаться healthcheck'ов @@ -169,9 +218,11 @@ jobs: docker system prune -f docker builder prune -f --keep-storage 5gb - # 9. Удалить старые образы crm-app, чтобы они не копились между деплоями. + # 9. Удалить старые образы app, чтобы они не копились между деплоями. # Текущий образ используется запущенным контейнером, поэтому он не удалится. CURRENT_IMAGE_ID=$(docker inspect --format='{{.Image}}' crm-app-1) - docker images crm-app --format "{{.ID}}" | grep -v "$CURRENT_IMAGE_ID" | xargs -r docker rmi -f || true + 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"