# Рунбук: ручной выкат gitea → прод (liderra.ru)

**Создан:** 18.06.2026. **Источник:** живой выкат июньской дельты 18.06.2026 (память `portal-security-audit-2026-06-17`).

Постоянная процедура выката кода на боевой `liderra.ru`. Прод **не** git-чекаут — код кладётся overlay'ем. Сделан, чтобы следующая сессия не собирала пайплайн заново.

> **Почему вручную, а не GitHub Actions.** Старый `.github/workflows/deploy.yml` **мёртв** — аккаунт GitHub `CoralMinister` suspended, Actions не запускаются. До восстановления аккаунта (или переезда CI на gitea) выкат — ручной по шагам ниже. Старый [deploy/redeploy.sh](../../../deploy/redeploy.sh) — это **серверная половина** (scp-эпоха); актуальная серверная часть — шаги 5–6 здесь (запуск от `www-data`, миграции через `postgres`).

---

## 0. Доступ и грабли оболочки (читать ПЕРВЫМ)

- **Доступ к боевому:** `ssh liderra-prod '<команда>'` (алиас в `~/.ssh/config`, через бастион gitea-VM). Приложение в `/var/www/liderra/app`.
- **PowerShell → ssh — кавычки.** В `ssh liderra-prod '...'` использовать **только внешние одинарные** кавычки, **без** внутренних двойных кавычек / круглых скобок / символа `|` — PowerShell их манглит → bash падает `syntax error near (`.
- **heredoc и `$$`.** В heredoc с **неэкранированным** разделителем bash раскрывает `$$` как PID. Для литерала писать `\$\$` либо использовать dollar-quoting `$tag$...$tag$`.
- **Привилегии на проде:**
  - `composer` / `artisan` запускать `sudo -u www-data` — `vendor/` принадлежит `www-data:www-data` 775, ssh-юзер `ubuntu` **не** в группе → иначе `composer` падает `could not create vendor/X`.
  - Миграции БД — через `sudo -u postgres psql -d liderra` — роль `crm_app_user` (`DB_USERNAME`) **не владелец** таблиц → `artisan migrate` падает `SQLSTATE 42501 must be owner of table deals`.

---

## 1. Бэкап кода (перед выкатом, обязательно)

На проде, tar приложения без тяжёлых/генерируемых каталогов:

```bash
ssh liderra-prod 'cd /var/www/liderra && sudo tar --exclude=app/node_modules --exclude=app/vendor --exclude=app/storage -czf /tmp/app-backup-$(date -u +%F).tgz app'
```

Имя примера прошлого выката: `/tmp/app-backup-2026-06-18.tgz`. Хранить до подтверждения здоровья после выката (см. шаг 7).

---

## 2. Клон свежего кода из gitea (на проде, во временный каталог)

Прод видит gitea по HTTP (с прода = HTTP 200). Самоподписанный TLS → `http.sslVerify=false`; креды в URL:

```bash
ssh liderra-prod 'rm -rf /tmp/liderra-build && git -c http.sslVerify=false clone https://liderra:Liderra_Admin_2026_Change@111.88.252.149.nip.io/liderra/portal.git /tmp/liderra-build'
```

> Это собирает выкат из ветки `main` репо `liderra/portal` в gitea (origin = мёртвый github, не используется). Проверь, что в gitea запушено то, что собираешься катить.

---

## 3. Сборка фронтенда (на проде — там есть node20/npm/composer/php)

```bash
ssh liderra-prod 'cd /tmp/liderra-build/app && npm ci --legacy-peer-deps && npm run build'
```

`--legacy-peer-deps` обязателен (peer-конфликт histoire↔vite).

---

## 4. Maintenance-режим

```bash
ssh liderra-prod 'cd /var/www/liderra/app && sudo -u www-data php artisan down'
```

---

## 5. Overlay кода через rsync (с исключениями)

Копируем собранный код поверх боевого, **исключая** окружение/состояние/зависимости. `.env`/`vendor` в репо и так gitignored — перетереть `.env` нечем, но исключения держим явно:

```bash
ssh liderra-prod 'sudo rsync -rlt --no-perms --no-owner --no-group --exclude=.env --exclude=.env.* --exclude=storage --exclude=vendor --exclude=node_modules --exclude=.git /tmp/liderra-build/app/ /var/www/liderra/app/'
```

---

## 6. Серверная часть: зависимости, миграции, кэш (от правильных ролей)

**6a. composer / optimize — от `www-data`:**

```bash
ssh liderra-prod 'cd /var/www/liderra/app && sudo -u www-data composer install --optimize-autoloader --no-interaction --no-scripts --ignore-platform-req=ext-redis'
ssh liderra-prod 'cd /var/www/liderra/app && sudo -u www-data php artisan optimize'
```

**6b. Миграции — через `postgres` superuser (НЕ `artisan migrate`).** `artisan migrate` от `crm_app_user` упрётся в `42501 must be owner`. Применять SQL миграции напрямую и **вручную** дописать строку в `migrations`:

```bash
# применить DDL миграции (пример; dollar-quoting вместо $$):
ssh liderra-prod 'sudo -u postgres psql -d liderra -c "<DDL миграции>"'
# отметить миграцию как применённую (batch — следующий номер):
ssh liderra-prod 'sudo -u postgres psql -d liderra -c "INSERT INTO migrations (migration, batch) VALUES (\x27<имя_файла_без_.php>\x27, <batch>)"'
```

> Повторный `artisan migrate` снова упрётся в привилегию — это ожидаемо, не «лечить» правами `crm_app_user`. Если миграция партиционированная — см. паттерн «pre-apply partitioned migrations via postgres superuser».

**6c. Права на storage/cache (из redeploy.sh):**

```bash
ssh liderra-prod 'cd /var/www/liderra/app && sudo chown -R ubuntu:www-data storage bootstrap/cache && sudo chmod -R 775 storage bootstrap/cache'
```

---

## 7. Подъём и smoke

```bash
ssh liderra-prod 'cd /var/www/liderra/app && sudo -u www-data php artisan up'
ssh liderra-prod 'sudo systemctl restart php8.3-fpm liderra-queue'
```

Smoke (с прода или с dev — curl только наружу, не active scan):

```bash
ssh liderra-prod 'curl -skI https://liderra.ru/'
```

Ждём `HTTP/2 200` + корректные заголовки безопасности. Дополнительно: `audit:verify-chains`, `scheduler:check-heartbeats`, `incidents:watch-failures` — clean.

---

## 8. Rollback

Если smoke красный:

```bash
ssh liderra-prod 'cd /var/www/liderra/app && sudo -u www-data php artisan down'
ssh liderra-prod 'cd /var/www/liderra && sudo tar -xzf /tmp/app-backup-<дата>.tgz'
ssh liderra-prod 'cd /var/www/liderra/app && sudo -u www-data php artisan up'
```

Миграции БД rollback'ятся отдельно (если применялись) — через `sudo -u postgres psql` обратным DDL + удаление строки из `migrations`. БД-откат планировать заранее: append-only audit-таблицы откатить нельзя.

---

## Связанное

- [deploy/redeploy.sh](../../../deploy/redeploy.sh) — серверная половина scp-эпохи (исторический контекст шага 6).
- Память `portal-security-audit-2026-06-17` — первый прогон этого пайплайна (18.06.2026, июньская дельта).
- `ПИЛОТ.md` (корень) — снимок боевого состояния; волатильное перепроверять реальной командой по ssh.
