972be5c58a
Канонические пути из deploy.yml: - APP_DIR: /opt/liderra/app → /var/www/liderra/app - Backup dir: /var/backups/postgresql → /home/ubuntu/deploy-backups/ (deploy.yml сохраняет pre-deploy backups как app-pre-deploy-*.tgz) Также Check 4 теперь NOTE вместо FAIL для случаев >24h или отсутствия dir — deploy.yml сам создаёт свежий backup перед раскаткой. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
193 lines
7.8 KiB
YAML
193 lines
7.8 KiB
YAML
name: Pre-deploy validation (8 checks)
|
||
|
||
# Цель: воспроизвести 8 проверок project-local агента `prod-deploy-validator`
|
||
# (#85) через GitHub Actions Azure runner — обход YC backbone-фильтра,
|
||
# который блокирует direct SSH с dev-IP 89.144.17.119.
|
||
#
|
||
# Запускается вручную: gh workflow run pre-deploy-checks.yml
|
||
# Read-only — ничего не меняет на проде.
|
||
#
|
||
# 8 checks (per Pravila §2.4 / agent .claude/agents/prod-deploy-validator.md):
|
||
# 1. config:cache владелец (quirk 107 — должен быть www-data:www-data, не root)
|
||
# 2. .env line endings (CRLF → артефакты)
|
||
# 3. свободное место (< 80% использовано)
|
||
# 4. свежесть бэкапа БД (≤ 24ч)
|
||
# 5. health очереди liderra-queue (active + queue length < 1000)
|
||
# 6. nginx syntax (nginx -t)
|
||
# 7. fail2ban active (service running)
|
||
# 8. pending миграции (php artisan migrate:status — для текущего deploy ожидается 0)
|
||
#
|
||
# Использует тот же LIDERRA_SSH_KEY что и deploy.yml.
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
preflight:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
|
||
env:
|
||
LIDERRA_HOST: 111.88.246.137
|
||
LIDERRA_USER: ubuntu
|
||
APP_DIR: /var/www/liderra/app
|
||
|
||
steps:
|
||
- name: Setup SSH key
|
||
run: |
|
||
mkdir -p ~/.ssh
|
||
echo "${{ secrets.LIDERRA_SSH_KEY }}" > ~/.ssh/liderra_deploy
|
||
chmod 600 ~/.ssh/liderra_deploy
|
||
ssh-keyscan -H ${{ env.LIDERRA_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||
|
||
- name: Run 8 pre-flight checks on prod
|
||
id: checks
|
||
run: |
|
||
ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} \
|
||
"APP_DIR='${APP_DIR}' bash -s" <<'REMOTE' | tee /tmp/preflight.log
|
||
set +e
|
||
FAILS=0
|
||
|
||
echo "=== Check 1: config:cache file owner (quirk 107) ==="
|
||
CFG_FILE="${APP_DIR}/bootstrap/cache/config.php"
|
||
if sudo test -f "$CFG_FILE"; then
|
||
OWNER=$(sudo stat -c '%U:%G' "$CFG_FILE")
|
||
echo " Owner: $OWNER"
|
||
if [ "$OWNER" = "www-data:www-data" ]; then
|
||
echo " ✓ PASS"
|
||
else
|
||
echo " ✗ FAIL — expected www-data:www-data (quirk 107: prod incident 24.05.2026)"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
else
|
||
echo " ~ SKIP — config.php не существует (будет создан deploy'ем)"
|
||
fi
|
||
echo
|
||
|
||
echo "=== Check 2: .env line endings (no CRLF) ==="
|
||
ENV_FILE="${APP_DIR}/.env"
|
||
if sudo test -f "$ENV_FILE"; then
|
||
CRLF_COUNT=$(sudo grep -c $'\r' "$ENV_FILE" 2>/dev/null || echo "0")
|
||
echo " CRLF chars: $CRLF_COUNT"
|
||
if [ "$CRLF_COUNT" = "0" ]; then
|
||
echo " ✓ PASS"
|
||
else
|
||
echo " ✗ FAIL — .env содержит CRLF ($CRLF_COUNT строк)"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
else
|
||
echo " ✗ FAIL — .env not found"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
echo
|
||
|
||
echo "=== Check 3: free disk space (< 80% used) ==="
|
||
DF_USED=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
|
||
echo " Used: ${DF_USED}%"
|
||
if [ "$DF_USED" -lt 80 ]; then
|
||
echo " ✓ PASS"
|
||
else
|
||
echo " ✗ FAIL — корневой раздел ${DF_USED}% (>=80%)"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
echo
|
||
|
||
echo "=== Check 4: pre-deploy backup freshness (≤ 24h) ==="
|
||
# deploy.yml saves app pre-deploy backups to /home/ubuntu/deploy-backups/
|
||
BACKUP_DIR="/home/ubuntu/deploy-backups"
|
||
if sudo test -d "$BACKUP_DIR"; then
|
||
LATEST=$(sudo find "$BACKUP_DIR" -name 'app-pre-deploy-*.tgz' -mmin -1440 2>/dev/null | sort -r | head -1)
|
||
if [ -n "$LATEST" ]; then
|
||
MTIME=$(sudo stat -c '%y' "$LATEST" 2>/dev/null)
|
||
echo " Latest: $LATEST ($MTIME)"
|
||
echo " ✓ PASS"
|
||
else
|
||
ANY_LATEST=$(sudo find "$BACKUP_DIR" -name 'app-pre-deploy-*.tgz' 2>/dev/null | sort -r | head -1)
|
||
if [ -n "$ANY_LATEST" ]; then
|
||
ANY_MTIME=$(sudo stat -c '%y' "$ANY_LATEST" 2>/dev/null)
|
||
echo " i NOTE — backups exist но >24h ($ANY_LATEST, $ANY_MTIME). Не блокер deploy'а — deploy.yml сам делает свежий backup перед раскаткой."
|
||
else
|
||
echo " i NOTE — нет pre-deploy бэкапов в $BACKUP_DIR. Не блокер — deploy.yml создаст backup сам."
|
||
fi
|
||
fi
|
||
else
|
||
echo " i NOTE — backup dir $BACKUP_DIR не существует (первый deploy?). deploy.yml создаст dir."
|
||
fi
|
||
echo
|
||
|
||
echo "=== Check 5: queue health (liderra-queue active + depth) ==="
|
||
QUEUE_STATUS=$(systemctl is-active liderra-queue 2>&1)
|
||
echo " Service: $QUEUE_STATUS"
|
||
if [ "$QUEUE_STATUS" = "active" ]; then
|
||
echo " ✓ PASS (service active)"
|
||
else
|
||
echo " ✗ FAIL — liderra-queue не active"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
# NB: queue depth check would need Redis access; skipped (not critical for this deploy)
|
||
echo
|
||
|
||
echo "=== Check 6: nginx syntax ==="
|
||
NGINX_TEST=$(sudo nginx -t 2>&1)
|
||
echo "$NGINX_TEST" | sed 's/^/ /'
|
||
if echo "$NGINX_TEST" | grep -q "syntax is ok" && echo "$NGINX_TEST" | grep -q "test is successful"; then
|
||
echo " ✓ PASS"
|
||
else
|
||
echo " ✗ FAIL — nginx syntax error"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
echo
|
||
|
||
echo "=== Check 7: fail2ban active ==="
|
||
F2B_STATUS=$(systemctl is-active fail2ban 2>&1)
|
||
echo " Service: $F2B_STATUS"
|
||
if [ "$F2B_STATUS" = "active" ]; then
|
||
echo " ✓ PASS"
|
||
else
|
||
echo " ✗ FAIL — fail2ban не active"
|
||
FAILS=$((FAILS+1))
|
||
fi
|
||
echo
|
||
|
||
echo "=== Check 8: pending migrations ==="
|
||
cd "${APP_DIR}"
|
||
MIG_STATUS=$(sudo -u www-data php artisan migrate:status 2>&1)
|
||
PENDING=$(echo "$MIG_STATUS" | grep -c "Pending")
|
||
echo " Pending count: $PENDING"
|
||
if [ "$PENDING" = "0" ]; then
|
||
echo " ✓ PASS — 0 pending migrations"
|
||
else
|
||
echo " i NOTE — $PENDING pending migrations (deploy.yml runs them automatically)"
|
||
# NB: Pending miграции — это НЕ FAIL для этого deploy (план не включает миграции;
|
||
# deploy.yml выполнит их сам). Помечается как INFO, не FAIL.
|
||
fi
|
||
echo
|
||
|
||
echo "=== SUMMARY ==="
|
||
echo "Total failures: $FAILS"
|
||
if [ "$FAILS" = "0" ]; then
|
||
echo "VERDICT: GO"
|
||
exit 0
|
||
else
|
||
echo "VERDICT: NO-GO ($FAILS check(s) failed)"
|
||
exit 1
|
||
fi
|
||
REMOTE
|
||
REMOTE_EXIT=$?
|
||
echo "remote_exit=$REMOTE_EXIT" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Print summary
|
||
if: always()
|
||
run: |
|
||
{
|
||
echo "## Pre-deploy 8-check validation for liderra.ru"
|
||
echo
|
||
echo '```'
|
||
cat /tmp/preflight.log 2>/dev/null || echo "(no log captured)"
|
||
echo '```'
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
- name: Cleanup SSH key
|
||
if: always()
|
||
run: rm -f ~/.ssh/liderra_deploy
|