gendesign/.github/workflows/deploy.yml
lekss361 9658baa6b2 fix(deploy): keep last 2 SHA-tagged images per repo, drop older
Previously `docker image prune -af --filter until=72h` only removed
images older than 72h. With multiple deploys/day we accumulate many
fresh (<72h) tagged images — each gendesign-backend / worker / frontend
SHA-tag is 500MB-1GB, filling /var/lib/docker.

New logic: for each ghcr.io/lekss361/gendesign-* repo, list tags sorted
by Created DESC, skip :latest, keep top 2, remove the rest. Still runs
the standard prune after for dangling layers + build cache.
2026-05-11 17:45:20 +03:00

242 lines
8.7 KiB
YAML
Raw 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
# Деплоится только при изменениях основного стека.
# Obsidian-стек (CouchDB) — отдельный workflow `deploy-obsidian.yml`.
on:
push:
branches: [main]
paths:
- "backend/**"
- "frontend/**"
- "docker-compose.prod.yml"
- "Caddyfile"
- ".github/workflows/deploy.yml"
# NB: shared compose-fragments (network) — тоже триггерят main
# потому что Caddy шарит сеть с obsidian-stack
workflow_dispatch:
concurrency:
group: deploy-prod
cancel-in-progress: false
env:
IMAGE_BACKEND: ghcr.io/lekss361/gendesign-backend
IMAGE_WORKER: ghcr.io/lekss361/gendesign-worker
IMAGE_FRONTEND: ghcr.io/lekss361/gendesign-frontend
jobs:
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
infra: ${{ steps.filter.outputs.infra }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
infra:
- 'docker-compose.prod.yml'
- 'Caddyfile'
- '.github/workflows/deploy.yml'
build-backend:
runs-on: ubuntu-latest
needs: changes
permissions:
contents: read
packages: write
if: |
needs.changes.outputs.backend == 'true' ||
needs.changes.outputs.infra == 'true' ||
github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build & push backend (lean — без Chromium)
uses: docker/build-push-action@v6
with:
context: ./backend
target: runner
push: true
cache-from: type=gha,scope=backend-lean
cache-to: type=gha,mode=max,scope=backend-lean
tags: |
${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }}
build-worker:
runs-on: ubuntu-latest
needs: changes
permissions:
contents: read
packages: write
if: |
needs.changes.outputs.backend == 'true' ||
needs.changes.outputs.infra == 'true' ||
github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build & push worker (с Chromium для Playwright)
uses: docker/build-push-action@v6
with:
context: ./backend
target: runner-with-chromium
push: true
cache-from: type=gha,scope=worker-chromium
cache-to: type=gha,mode=max,scope=worker-chromium
tags: |
${{ env.IMAGE_WORKER }}:latest
${{ env.IMAGE_WORKER }}:${{ github.sha }}
build-frontend:
runs-on: ubuntu-latest
needs: changes
permissions:
contents: read
packages: write
if: |
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.infra == 'true' ||
github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build & push frontend
uses: docker/build-push-action@v6
with:
context: ./frontend
push: true
cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend
tags: |
${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
deploy:
runs-on: ubuntu-latest
needs: [changes, build-backend, build-worker, build-frontend]
permissions:
contents: read
# Запускается если ни один build не завершился failure.
# `always()` позволяет выполнить job даже когда зависимые jobs были skipped.
if: |
always() &&
!cancelled() &&
needs.build-backend.result != 'failure' &&
needs.build-worker.result != 'failure' &&
needs.build-frontend.result != 'failure'
steps:
- name: Deploy to VM via SSH
uses: appleboy/ssh-action@v1.0.3
env:
IMAGE_TAG: latest
SENTRY_RELEASE_VAL: ${{ github.sha }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT || 22 }}
envs: IMAGE_TAG,SENTRY_RELEASE_VAL
script: |
set -euo pipefail
cd /opt/gendesign
# Sync compose / Caddyfile / init scripts from the repo.
# --ff-only refuses if there are local commits on the VM (we don't expect any).
git fetch origin main
git reset --hard origin/main
# Sentry release tracking — записываем git-sha в .env.runtime.
# ВАЖНО: НЕ перезаписываем файл целиком (там могут быть
# COUCHDB_PASSWORD и др. user-managed secrets). Заменяем только
# строку SENTRY_RELEASE или добавляем если её нет.
# IMAGE_TAG=latest для docker pull; SENTRY_RELEASE_VAL = git sha для трекинга.
mkdir -p backend
touch backend/.env.runtime
if grep -q '^SENTRY_RELEASE=' backend/.env.runtime; then
sed -i "s|^SENTRY_RELEASE=.*|SENTRY_RELEASE=$SENTRY_RELEASE_VAL|" backend/.env.runtime
else
printf 'SENTRY_RELEASE=%s\n' "$SENTRY_RELEASE_VAL" >> backend/.env.runtime
fi
chmod 600 backend/.env.runtime
# Создать external network если её нет (нужна Caddy для маршрута
# obsidian.gendsgn.ru → couchdb из отдельного obsidian-stack).
docker network inspect gendesign_shared >/dev/null 2>&1 \
|| docker network create gendesign_shared
export IMAGE_TAG="$IMAGE_TAG"
# Project name явно — `gendesign` (по имени папки auto), но
# фиксируем для consistency с obsidian-stack (-p gendesign-obsidian).
docker compose -p gendesign -f docker-compose.prod.yml pull
docker compose -p gendesign -f docker-compose.prod.yml up -d
# Caddyfile is bind-mounted; up -d won't re-read it. Reload explicitly.
# `|| true` so a temporary Caddy hiccup doesn't fail the whole deploy.
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
caddy reload --config /etc/caddy/Caddyfile || true
# Clean up disk:
# 1. Для каждого gendesign-* образа оставляем 2 самых свежих
# SHA-тега (currently running + 1 fallback для rollback),
# остальные удаляем независимо от возраста. `:latest`
# никогда не трогаем. Docker images сортирует по Created
# DESC по умолчанию — берём всё после первых 2 строк.
for repo in ghcr.io/lekss361/gendesign-backend \
ghcr.io/lekss361/gendesign-worker \
ghcr.io/lekss361/gendesign-frontend; do
docker images "$repo" --format '{{.Repository}}:{{.Tag}}' \
| grep -v ':latest$' \
| tail -n +3 \
| xargs -r docker rmi 2>/dev/null || true
done
# 2. dangling/unused images (untagged layers) + build cache.
docker image prune -af --filter "until=72h" || true
docker builder prune -af || true
# Wait for backend to be healthy (up to 30 s).
for i in $(seq 1 30); do
curl -fsS http://localhost:8000/health && break
sleep 1
done