fix(ci): switch deploy to .forgejo/workflows + shell GHCR login #192
1 changed files with 43 additions and 79 deletions
|
|
@ -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
|
||||||
Loading…
Add table
Reference in a new issue