gendesign/.forgejo/workflows/deploy.yml
lekss361 11e78d738e fix(deploy): force-recreate caddy чтобы новые compose mounts применились
Раньше deploy.yml делал `docker compose exec caddy caddy reload` — это
только reload Caddyfile, но НЕ пересоздаёт container. Когда в
docker-compose.prod.yml добавлялся новый volume mount (например
./preview:/srv/preview:ro в PR #268), он не появлялся в running container
до manual `docker compose up -d --force-recreate caddy` через SSH.

Теперь deploy всегда вызывает force-recreate caddy после image pull —
идемпотентно, +~5 сек к deploy time, гарантирует что новые mounts +
Caddyfile блоки активны.
2026-05-17 11:30:45 +03:00

275 lines
11 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
# Forgejo Actions equivalent of .github/workflows/deploy.yml
# Migration 2026-05-16: GitHub → Forgejo (git.gendsgn.ru)
# Builds images on Forgejo runner, pushes to ghcr.io (GitHub Container Registry),
# SSH-deploys to Beget VPS (46.173.16.127).
on:
push:
branches: [main]
paths:
- "backend/**"
- "frontend/**"
- "docker-compose.prod.yml"
- "Caddyfile"
- ".forgejo/workflows/deploy.yml"
- "data/sql/*.sql"
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
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/**'
- 'data/sql/**'
frontend:
- 'frontend/**'
infra:
- 'docker-compose.prod.yml'
- 'Caddyfile'
- '.forgejo/workflows/deploy.yml'
build-backend:
runs-on: ubuntu-latest
needs: changes
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 (shell-based — docker/login-action@v3 unreliable под Forgejo Actions)
env:
GHCR_PAT: ${{ secrets.GHCR_PAT }}
run: |
echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
- 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=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache,mode=max
tags: |
${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }}
build-worker:
runs-on: ubuntu-latest
needs: changes
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 (shell-based — docker/login-action@v3 unreliable под Forgejo Actions)
env:
GHCR_PAT: ${{ secrets.GHCR_PAT }}
run: |
echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
- 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=registry,ref=${{ env.IMAGE_WORKER }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_WORKER }}:buildcache,mode=max
tags: |
${{ env.IMAGE_WORKER }}:latest
${{ env.IMAGE_WORKER }}:${{ github.sha }}
build-frontend:
runs-on: ubuntu-latest
needs: changes
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 (shell-based — docker/login-action@v3 unreliable под Forgejo Actions)
env:
GHCR_PAT: ${{ secrets.GHCR_PAT }}
run: |
echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
- 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
build-args: |
NEXT_PUBLIC_GLITCHTIP_DSN=${{ secrets.GLITCHTIP_FRONTEND_DSN }}
NEXT_PUBLIC_ENVIRONMENT=production
cache-from: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache,mode=max
tags: |
${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
deploy:
runs-on: ubuntu-latest
needs: [changes, build-backend, build-worker, build-frontend]
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 }}
GHCR_PAT: ${{ secrets.GHCR_PAT }}
GLITCHTIP_BACKEND_DSN: ${{ secrets.GLITCHTIP_BACKEND_DSN }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT }}
envs: IMAGE_TAG,SENTRY_RELEASE_VAL,GHCR_PAT,GLITCHTIP_BACKEND_DSN
script: |
set -euo pipefail
cd /opt/gendesign
# Sync compose / Caddyfile / init scripts from the repo.
# Origin теперь Forgejo (после migration 2026-05-16) — HTTPS basic auth
# через PAT в URL не нужен т.к. repo читается через deploy key (SSH)
# ИЛИ public read-only mode. Forgejo gendesign — private, нужен auth:
# `git remote get-url origin` должен указывать на Forgejo с auth.
git fetch origin main
git reset --hard origin/main
# Sentry release tracking
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
# GlitchTip wiring: убираем legacy SENTRY_DSN (auto-promote-логика в
# backend/app/core/config.py:_promote_legacy_sentry_dsn раньше брала
# его и слала события в чужой sentry.io). Устанавливаем GLITCHTIP_DSN
# из Forgejo secret. Пустой secret = no-op (SDK не инициализируется).
sed -i '/^SENTRY_DSN=/d' backend/.env.runtime
if grep -q '^GLITCHTIP_DSN=' backend/.env.runtime; then
sed -i "s|^GLITCHTIP_DSN=.*|GLITCHTIP_DSN=$GLITCHTIP_BACKEND_DSN|" backend/.env.runtime
else
printf 'GLITCHTIP_DSN=%s\n' "$GLITCHTIP_BACKEND_DSN" >> backend/.env.runtime
fi
chmod 600 backend/.env.runtime
# External network для Caddy + obsidian-stack share
docker network inspect gendesign_shared >/dev/null 2>&1 \
|| docker network create gendesign_shared
# Re-login to GHCR (PAT может быть rotated после initial setup)
echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
export IMAGE_TAG="$IMAGE_TAG"
docker compose -p gendesign -f docker-compose.prod.yml pull
# Apply pending SQL migrations
set -a; source .env; set +a
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=on -c "
CREATE TABLE IF NOT EXISTS _schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"
for sql_file in $(ls -1 data/sql/*.sql 2>/dev/null | sort); do
fname=$(basename "$sql_file")
applied=$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
"SELECT COUNT(*) FROM _schema_migrations WHERE filename='$fname'")
if [ "$applied" = "0" ]; then
echo "→ Applying migration: $fname"
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=on \
< "$sql_file" \
|| { echo "FAILED on migration: $fname"; exit 1; }
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
"INSERT INTO _schema_migrations (filename) VALUES ('$fname') ON CONFLICT DO NOTHING;"
else
echo "✓ Already applied: $fname"
fi
done
echo "All migrations applied."
docker compose -p gendesign -f docker-compose.prod.yml up -d
# backend/.env.runtime изменения (SENTRY_RELEASE, GLITCHTIP_DSN)
# требуют --force-recreate — обычный `up -d` не перечитывает env_file
# если только image не сменился. На deploy где меняется только runtime
# без backend image change — без этого backend остаётся со старым DSN.
docker compose -p gendesign -f docker-compose.prod.yml up -d \
--force-recreate --no-deps backend worker beat
# Caddy: force-recreate чтобы подхватить изменения в Caddyfile
# И в особенности новые volume mounts из docker-compose.prod.yml
# (`reload` не пересоздаёт container, поэтому новые binds не появляются —
# был случай 2026-05-17 с PR #268 preview/ — потребовался manual SSH fix).
docker compose -p gendesign -f docker-compose.prod.yml up -d \
--force-recreate --no-deps caddy
# Cleanup old images
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
docker image prune -af || true
docker builder prune -af || true
# Health check
for i in $(seq 1 30); do
curl -fsS http://localhost:8000/health && break
sleep 1
done