214 lines
7.5 KiB
Bash
Executable File
214 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -Eeuo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
|
PROD_COMPOSE="${ROOT_DIR}/compose.prod.yaml"
|
|
DEV_COMPOSE="${ROOT_DIR}/compose.yaml"
|
|
ENV_FILE="${ROOT_DIR}/.env"
|
|
LEGACY_WAS_RUNNING=0
|
|
PROD_COMPOSE_ARGS=(-f "$PROD_COMPOSE")
|
|
SSH_TUNNEL=0
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
|
|
}
|
|
|
|
fail() {
|
|
printf '[error] %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
env_value() {
|
|
local key="$1" line value
|
|
line="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)"
|
|
value="${line#*=}"
|
|
case "$value" in
|
|
\"*\") value="${value:1:${#value}-2}" ;;
|
|
\'*\') value="${value:1:${#value}-2}" ;;
|
|
esac
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
configure_compose() {
|
|
local auth_host characters_host arg
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--ssh-tunnel) SSH_TUNNEL=1 ;;
|
|
*) fail "Неизвестный параметр: $arg (доступен --ssh-tunnel)" ;;
|
|
esac
|
|
done
|
|
auth_host="$(env_value VMANGOS_AUTH_DB_HOST)"
|
|
characters_host="$(env_value VMANGOS_CHARACTERS_DB_HOST)"
|
|
if [[ "$auth_host" == core-db-tunnel || "$characters_host" == core-db-tunnel ]]; then
|
|
SSH_TUNNEL=1
|
|
fi
|
|
if (( SSH_TUNNEL )); then
|
|
[[ -f "${ROOT_DIR}/compose.prod.ssh-tunnel.yaml" ]] || fail "Не найден compose.prod.ssh-tunnel.yaml"
|
|
if [[ -n "$(env_value VMANGOS_AUTH_DB_URL)" || -n "$(env_value VMANGOS_CHARACTERS_DB_URL)" ]]; then
|
|
fail "Для SSH-туннеля уберите VMANGOS_AUTH_DB_URL и VMANGOS_CHARACTERS_DB_URL; используйте отдельные настройки БД"
|
|
fi
|
|
PROD_COMPOSE_ARGS+=(-f "${ROOT_DIR}/compose.prod.ssh-tunnel.yaml")
|
|
log "БД ядра: SSH-туннель core-db-tunnel:3306"
|
|
fi
|
|
prod config --quiet
|
|
}
|
|
|
|
prod() {
|
|
docker compose "${PROD_COMPOSE_ARGS[@]}" "$@"
|
|
}
|
|
|
|
legacy() {
|
|
docker compose -f "$DEV_COMPOSE" "$@"
|
|
}
|
|
|
|
wait_for_health() {
|
|
local service="$1"
|
|
local container_id
|
|
local status
|
|
|
|
container_id="$(prod ps -q "$service")"
|
|
[[ -n "$container_id" ]] || fail "Контейнер ${service} не создан"
|
|
|
|
for _ in $(seq 1 45); do
|
|
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")"
|
|
if [[ "$status" == healthy ]]; then
|
|
return 0
|
|
fi
|
|
if [[ "$status" == exited || "$status" == dead ]]; then
|
|
docker logs --tail 100 "$container_id" >&2 || true
|
|
fail "Контейнер ${service} завершился со статусом ${status}"
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
docker logs --tail 100 "$container_id" >&2 || true
|
|
fail "Контейнер ${service} не перешёл в healthy"
|
|
}
|
|
|
|
rollback_legacy() {
|
|
log "Production web не прошёл проверку, выполняю откат"
|
|
prod stop web app >/dev/null 2>&1 || true
|
|
if (( LEGACY_WAS_RUNNING )); then
|
|
legacy start laravel.test >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
verify_frontend_assets() {
|
|
local app_port="$1"
|
|
local app_manifest web_manifest assets asset_path content_type
|
|
|
|
app_manifest="$(prod exec -T app sha256sum /var/www/html/public/build/manifest.json)" || return 1
|
|
web_manifest="$(prod exec -T web sha256sum /var/www/html/public/build/manifest.json)" || return 1
|
|
if [[ "$app_manifest" != "$web_manifest" ]]; then
|
|
log "Сборки frontend в app и web не совпадают"
|
|
return 1
|
|
fi
|
|
|
|
assets="$(prod exec -T app php -r '
|
|
$manifest = json_decode(file_get_contents("public/build/manifest.json"), true, 512, JSON_THROW_ON_ERROR);
|
|
$entry = $manifest["resources/js/app.js"] ?? [];
|
|
if (! isset($entry["file"])) {
|
|
fwrite(STDERR, "Missing JavaScript entry in Vite manifest\n");
|
|
exit(1);
|
|
}
|
|
foreach (array_merge([$entry["file"]], $entry["css"] ?? []) as $asset) {
|
|
echo "/build/".$asset."\n";
|
|
}
|
|
')" || return 1
|
|
|
|
while IFS= read -r asset_path; do
|
|
content_type="$(curl --fail --silent --show-error --max-time 15 \
|
|
--output /dev/null --write-out '%{content_type}' \
|
|
"http://127.0.0.1:${app_port}${asset_path}")" || return 1
|
|
case "${asset_path}:${content_type}" in
|
|
*.js:application/javascript*|*.js:text/javascript*|*.css:text/css*) ;;
|
|
*)
|
|
log "Некорректный Content-Type для ${asset_path}: ${content_type}"
|
|
return 1
|
|
;;
|
|
esac
|
|
done <<< "$assets"
|
|
}
|
|
|
|
main() {
|
|
command -v docker >/dev/null 2>&1 || fail "Не найден Docker"
|
|
docker compose version >/dev/null 2>&1 || fail "Не найден Docker Compose Plugin"
|
|
[[ -f "$ENV_FILE" ]] || fail "Не найден ${ENV_FILE}"
|
|
|
|
cd "$ROOT_DIR"
|
|
configure_compose "$@"
|
|
|
|
local app_uid app_gid app_port legacy_id
|
|
app_uid="$(env_value WWWUSER)"
|
|
app_gid="$(env_value WWWGROUP)"
|
|
app_port="$(env_value APP_PORT)"
|
|
app_uid="${app_uid:-1000}"
|
|
app_gid="${app_gid:-1000}"
|
|
app_port="${app_port:-8080}"
|
|
|
|
mkdir -p storage/app/public storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs
|
|
if [[ "$(id -u)" -eq 0 ]]; then
|
|
chown -R "${app_uid}:${app_gid}" storage
|
|
fi
|
|
chmod -R ug+rwX storage
|
|
|
|
log "Собираю production-образы"
|
|
prod build app web
|
|
if (( SSH_TUNNEL )); then
|
|
prod build core-db-tunnel
|
|
fi
|
|
|
|
log "Запускаю MySQL сайта"
|
|
prod up -d mysql
|
|
wait_for_health mysql
|
|
|
|
if (( SSH_TUNNEL )); then
|
|
log "Запускаю SSH-туннель к БД ядра"
|
|
prod up -d --no-deps core-db-tunnel
|
|
wait_for_health core-db-tunnel
|
|
fi
|
|
|
|
log "Запускаю новый PHP-FPM"
|
|
prod up -d --no-deps app
|
|
wait_for_health app
|
|
|
|
log "Проверяю доступ приложения к обеим БД ядра"
|
|
prod exec -T --user "${app_uid}:${app_gid}" app php artisan config:clear
|
|
prod exec -T --user "${app_uid}:${app_gid}" app php artisan tinker --execute='foreach (["vmangos_auth", "vmangos_characters"] as $name) { DB::connection($name)->select("SELECT 1"); echo $name." OK\n"; }'
|
|
|
|
log "Выполняю миграции и прогреваю кэш"
|
|
prod exec -T --user "${app_uid}:${app_gid}" app php artisan migrate --force
|
|
prod exec -T --user "${app_uid}:${app_gid}" app php artisan optimize
|
|
|
|
legacy_id="$(legacy ps -q laravel.test 2>/dev/null || true)"
|
|
if [[ -n "$legacy_id" ]] && [[ "$(docker inspect -f '{{.State.Running}}' "$legacy_id")" == true ]]; then
|
|
LEGACY_WAS_RUNNING=1
|
|
log "Останавливаю legacy Sail-контейнер"
|
|
legacy stop laravel.test
|
|
fi
|
|
|
|
log "Переключаю nginx на production web-контейнер"
|
|
if ! prod up -d --no-deps web || ! wait_for_health web; then
|
|
rollback_legacy
|
|
exit 1
|
|
fi
|
|
|
|
if ! curl --fail --silent --max-time 15 "http://127.0.0.1:${app_port}/" >/dev/null; then
|
|
rollback_legacy
|
|
fail "HTTP-проверка production web завершилась ошибкой"
|
|
fi
|
|
|
|
log "Проверяю соответствие сборок frontend и доступность JavaScript/CSS"
|
|
if ! verify_frontend_assets "$app_port"; then
|
|
rollback_legacy
|
|
fail "Проверка frontend завершилась ошибкой"
|
|
fi
|
|
|
|
log "Production-деплой завершён: app и web healthy"
|
|
prod ps app web
|
|
}
|
|
|
|
main "$@"
|