fix(ci): switch deploy to .forgejo/workflows + shell GHCR login #192

Merged
lekss361 merged 1 commit from fix/forgejo-ghcr-login into main 2026-05-16 05:25:00 +00:00

View file

@ -1,7 +1,9 @@
name: Deploy name: Deploy
# Деплоится только при изменениях основного стека. # Forgejo Actions equivalent of .github/workflows/deploy.yml
# Obsidian-стек (CouchDB) — отдельный workflow `deploy-obsidian.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: on:
push: push:
branches: [main] branches: [main]
@ -10,10 +12,8 @@ on:
- "frontend/**" - "frontend/**"
- "docker-compose.prod.yml" - "docker-compose.prod.yml"
- "Caddyfile" - "Caddyfile"
- ".github/workflows/deploy.yml" - ".forgejo/workflows/deploy.yml"
- "data/sql/*.sql" - "data/sql/*.sql"
# NB: shared compose-fragments (network) — тоже триггерят main
# потому что Caddy шарит сеть с obsidian-stack
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@ -28,9 +28,6 @@ env:
jobs: jobs:
changes: changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs: outputs:
backend: ${{ steps.filter.outputs.backend }} backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }} frontend: ${{ steps.filter.outputs.frontend }}
@ -43,19 +40,17 @@ jobs:
filters: | filters: |
backend: backend:
- 'backend/**' - 'backend/**'
- 'data/sql/**'
frontend: frontend:
- 'frontend/**' - 'frontend/**'
infra: infra:
- 'docker-compose.prod.yml' - 'docker-compose.prod.yml'
- 'Caddyfile' - 'Caddyfile'
- '.github/workflows/deploy.yml' - '.forgejo/workflows/deploy.yml'
build-backend: build-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: changes needs: changes
permissions:
contents: read
packages: write
if: | if: |
needs.changes.outputs.backend == 'true' || needs.changes.outputs.backend == 'true' ||
needs.changes.outputs.infra == 'true' || needs.changes.outputs.infra == 'true' ||
@ -63,12 +58,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to GHCR - name: Login to GHCR (shell-based — docker/login-action@v3 unreliable под Forgejo Actions)
uses: docker/login-action@v3 env:
with: GHCR_PAT: ${{ secrets.GHCR_PAT }}
registry: ghcr.io run: |
username: ${{ github.actor }} echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -79,8 +73,8 @@ jobs:
context: ./backend context: ./backend
target: runner target: runner
push: true push: true
cache-from: type=gha,scope=backend-lean cache-from: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache
cache-to: type=gha,mode=max,scope=backend-lean cache-to: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_BACKEND }}:latest ${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
@ -88,9 +82,6 @@ jobs:
build-worker: build-worker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: changes needs: changes
permissions:
contents: read
packages: write
if: | if: |
needs.changes.outputs.backend == 'true' || needs.changes.outputs.backend == 'true' ||
needs.changes.outputs.infra == 'true' || needs.changes.outputs.infra == 'true' ||
@ -98,12 +89,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to GHCR - name: Login to GHCR (shell-based — docker/login-action@v3 unreliable под Forgejo Actions)
uses: docker/login-action@v3 env:
with: GHCR_PAT: ${{ secrets.GHCR_PAT }}
registry: ghcr.io run: |
username: ${{ github.actor }} echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -114,8 +104,8 @@ jobs:
context: ./backend context: ./backend
target: runner-with-chromium target: runner-with-chromium
push: true push: true
cache-from: type=gha,scope=worker-chromium cache-from: type=registry,ref=${{ env.IMAGE_WORKER }}:buildcache
cache-to: type=gha,mode=max,scope=worker-chromium cache-to: type=registry,ref=${{ env.IMAGE_WORKER }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_WORKER }}:latest ${{ env.IMAGE_WORKER }}:latest
${{ env.IMAGE_WORKER }}:${{ github.sha }} ${{ env.IMAGE_WORKER }}:${{ github.sha }}
@ -123,9 +113,6 @@ jobs:
build-frontend: build-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: changes needs: changes
permissions:
contents: read
packages: write
if: | if: |
needs.changes.outputs.frontend == 'true' || needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.infra == 'true' || needs.changes.outputs.infra == 'true' ||
@ -133,12 +120,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to GHCR - name: Login to GHCR (shell-based — docker/login-action@v3 unreliable под Forgejo Actions)
uses: docker/login-action@v3 env:
with: GHCR_PAT: ${{ secrets.GHCR_PAT }}
registry: ghcr.io run: |
username: ${{ github.actor }} echo "$GHCR_PAT" | docker login ghcr.io -u lekss361 --password-stdin
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -148,8 +134,8 @@ jobs:
with: with:
context: ./frontend context: ./frontend
push: true push: true
cache-from: type=gha,scope=frontend cache-from: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache
cache-to: type=gha,mode=max,scope=frontend cache-to: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_FRONTEND }}:latest ${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
@ -157,10 +143,6 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [changes, build-backend, build-worker, build-frontend] needs: [changes, build-backend, build-worker, build-frontend]
permissions:
contents: read
# Запускается если ни один build не завершился failure.
# `always()` позволяет выполнить job даже когда зависимые jobs были skipped.
if: | if: |
always() && always() &&
!cancelled() && !cancelled() &&
@ -173,26 +155,26 @@ jobs:
env: env:
IMAGE_TAG: latest IMAGE_TAG: latest
SENTRY_RELEASE_VAL: ${{ github.sha }} SENTRY_RELEASE_VAL: ${{ github.sha }}
GHCR_PAT: ${{ secrets.GHCR_PAT }}
with: with:
host: ${{ secrets.DEPLOY_HOST }} host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }} username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }} key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT || 22 }} port: ${{ secrets.DEPLOY_PORT }}
envs: IMAGE_TAG,SENTRY_RELEASE_VAL envs: IMAGE_TAG,SENTRY_RELEASE_VAL,GHCR_PAT
script: | script: |
set -euo pipefail set -euo pipefail
cd /opt/gendesign cd /opt/gendesign
# Sync compose / Caddyfile / init scripts from the repo. # Sync compose / Caddyfile / init scripts from the repo.
# --ff-only refuses if there are local commits on the VM (we don't expect any). # 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 fetch origin main
git reset --hard origin/main git reset --hard origin/main
# Sentry release tracking — записываем git-sha в .env.runtime. # Sentry release tracking
# ВАЖНО: НЕ перезаписываем файл целиком (там могут быть
# COUCHDB_PASSWORD и др. user-managed secrets). Заменяем только
# строку SENTRY_RELEASE или добавляем если её нет.
# IMAGE_TAG=latest для docker pull; SENTRY_RELEASE_VAL = git sha для трекинга.
mkdir -p backend mkdir -p backend
touch backend/.env.runtime touch backend/.env.runtime
if grep -q '^SENTRY_RELEASE=' backend/.env.runtime; then if grep -q '^SENTRY_RELEASE=' backend/.env.runtime; then
@ -202,27 +184,19 @@ jobs:
fi fi
chmod 600 backend/.env.runtime chmod 600 backend/.env.runtime
# Создать external network если её нет (нужна Caddy для маршрута # External network для Caddy + obsidian-stack share
# obsidian.gendsgn.ru → couchdb из отдельного obsidian-stack).
docker network inspect gendesign_shared >/dev/null 2>&1 \ docker network inspect gendesign_shared >/dev/null 2>&1 \
|| docker network create gendesign_shared || 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" 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 pull
# ── Apply pending SQL migrations ────────────────────────────────── # Apply pending SQL migrations
# Tracking table: _schema_migrations (filename TEXT PRIMARY KEY).
# Each NN_*.sql file is applied exactly once, in sort order.
# ON_ERROR_STOP=on ensures a failing migration aborts the deploy.
# POSTGRES_USER/PASSWORD/DB живут в /opt/gendesign/.env (root —
# их читает compose через env_file для postgres service).
# backend/.env содержит ТОЛЬКО backend-app vars (SCRAPE_ADMIN_TOKEN,
# OPENROUTESERVICE_API_KEY, etc), без POSTGRES_*.
set -a; source .env; set +a set -a; source .env; set +a
# Ensure tracking table exists (idempotent).
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \ 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 " psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=on -c "
CREATE TABLE IF NOT EXISTS _schema_migrations ( CREATE TABLE IF NOT EXISTS _schema_migrations (
@ -231,7 +205,6 @@ jobs:
); );
" "
# Apply each pending .sql file in NN order.
for sql_file in $(ls -1 data/sql/*.sql 2>/dev/null | sort); do for sql_file in $(ls -1 data/sql/*.sql 2>/dev/null | sort); do
fname=$(basename "$sql_file") fname=$(basename "$sql_file")
applied=$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \ applied=$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
@ -251,18 +224,13 @@ jobs:
fi fi
done done
echo "All migrations applied." echo "All migrations applied."
# ─────────────────────────────────────────────────────────────────
docker compose -p gendesign -f docker-compose.prod.yml up -d 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 \ docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
caddy reload --config /etc/caddy/Caddyfile || true caddy reload --config /etc/caddy/Caddyfile || true
# Clean up disk — aggressive чтобы избежать накопления. # Cleanup old images
# 1. Для каждого gendesign-* repo оставляем 2 самых свежих SHA-тега
# + :latest. Docker images сортирует по Created DESC.
for repo in ghcr.io/lekss361/gendesign-backend \ for repo in ghcr.io/lekss361/gendesign-backend \
ghcr.io/lekss361/gendesign-worker \ ghcr.io/lekss361/gendesign-worker \
ghcr.io/lekss361/gendesign-frontend; do ghcr.io/lekss361/gendesign-frontend; do
@ -271,14 +239,10 @@ jobs:
| tail -n +3 \ | tail -n +3 \
| xargs -r docker rmi 2>/dev/null || true | xargs -r docker rmi 2>/dev/null || true
done done
# 2. ВСЕ unused images (даже свежие). Running контейнеры безопасны
# — их images не unused. Раньше был filter until=72h, но при
# частых деплоях оставались "fresh dangling" layers, накапливая
# до 50GB за неделю.
docker image prune -af || true docker image prune -af || true
docker builder prune -af || true docker builder prune -af || true
# Wait for backend to be healthy (up to 30 s). # Health check
for i in $(seq 1 30); do for i in $(seq 1 30); do
curl -fsS http://localhost:8000/health && break curl -fsS http://localhost:8000/health && break
sleep 1 sleep 1