2026-03-09 13:00:34 +08:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
|
BLUE='\033[0;34m'
|
|
|
|
|
|
NC='\033[0m'
|
|
|
|
|
|
|
|
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
|
VENV_DIR="${ROOT_DIR}/.venv"
|
|
|
|
|
|
PYTHON_BIN="${VENV_DIR}/bin/python"
|
|
|
|
|
|
UVICORN_BIN="${VENV_DIR}/bin/uvicorn"
|
|
|
|
|
|
CELERY_BIN="${VENV_DIR}/bin/celery"
|
|
|
|
|
|
|
|
|
|
|
|
API_HOST="${API_HOST:-0.0.0.0}"
|
|
|
|
|
|
API_PORT="${API_PORT:-8000}"
|
|
|
|
|
|
CELERY_POOL="${CELERY_POOL:-solo}"
|
|
|
|
|
|
SKIP_INSTALL="${SKIP_INSTALL:-0}"
|
|
|
|
|
|
SHUTDOWN_TIMEOUT="${SHUTDOWN_TIMEOUT:-12}"
|
|
|
|
|
|
|
|
|
|
|
|
API_PID=""
|
|
|
|
|
|
CELERY_PID=""
|
|
|
|
|
|
CLEANED_UP=0
|
|
|
|
|
|
INFRA_STARTED=0
|
|
|
|
|
|
|
|
|
|
|
|
print_header() {
|
|
|
|
|
|
echo -e "\n${BLUE}========================================${NC}"
|
|
|
|
|
|
echo -e "${BLUE}$1${NC}"
|
|
|
|
|
|
echo -e "${BLUE}========================================${NC}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print_ok() {
|
|
|
|
|
|
echo -e "${GREEN}✓ $1${NC}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print_warn() {
|
|
|
|
|
|
echo -e "${YELLOW}⚠ $1${NC}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print_err() {
|
|
|
|
|
|
echo -e "${RED}✗ $1${NC}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
is_pid_alive() {
|
|
|
|
|
|
local pid="$1"
|
|
|
|
|
|
[[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wait_pid_exit() {
|
|
|
|
|
|
local pid="$1"
|
|
|
|
|
|
local timeout="$2"
|
|
|
|
|
|
local waited=0
|
|
|
|
|
|
|
|
|
|
|
|
while is_pid_alive "${pid}"; do
|
|
|
|
|
|
if (( waited >= timeout )); then
|
|
|
|
|
|
return 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
waited=$((waited + 1))
|
|
|
|
|
|
done
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
kill_children_term() {
|
|
|
|
|
|
local pid="$1"
|
|
|
|
|
|
local children
|
|
|
|
|
|
|
|
|
|
|
|
children="$(pgrep -P "${pid}" 2>/dev/null || true)"
|
|
|
|
|
|
if [[ -n "${children}" ]]; then
|
|
|
|
|
|
# 先递归处理子进程,避免 reloader/server 残留
|
|
|
|
|
|
while IFS= read -r child_pid; do
|
|
|
|
|
|
[[ -z "${child_pid}" ]] && continue
|
|
|
|
|
|
kill_children_term "${child_pid}"
|
|
|
|
|
|
kill -TERM "${child_pid}" 2>/dev/null || true
|
|
|
|
|
|
done <<< "${children}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop_process_gracefully() {
|
|
|
|
|
|
local name="$1"
|
|
|
|
|
|
local pid="$2"
|
|
|
|
|
|
local timeout="${3:-10}"
|
|
|
|
|
|
|
|
|
|
|
|
if ! is_pid_alive "${pid}"; then
|
|
|
|
|
|
print_ok "${name} 已退出"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
print_warn "正在停止 ${name}(PID: ${pid})..."
|
|
|
|
|
|
kill_children_term "${pid}"
|
|
|
|
|
|
kill -TERM "${pid}" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
|
|
if wait_pid_exit "${pid}" "${timeout}"; then
|
|
|
|
|
|
print_ok "${name} 已停止"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
print_warn "${name} 在 ${timeout}s 内未退出,准备强制结束"
|
|
|
|
|
|
kill -KILL "${pid}" 2>/dev/null || true
|
|
|
|
|
|
wait_pid_exit "${pid}" 3 || true
|
|
|
|
|
|
print_ok "${name} 已强制结束"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
|
if [[ "${CLEANED_UP}" == "1" ]]; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
CLEANED_UP=1
|
|
|
|
|
|
|
|
|
|
|
|
print_header "正在关闭开发环境"
|
|
|
|
|
|
|
|
|
|
|
|
if is_pid_alive "${API_PID}"; then
|
|
|
|
|
|
stop_process_gracefully "FastAPI" "${API_PID}" "${SHUTDOWN_TIMEOUT}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if is_pid_alive "${CELERY_PID}"; then
|
|
|
|
|
|
stop_process_gracefully "Celery" "${CELERY_PID}" "${SHUTDOWN_TIMEOUT}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "${INFRA_STARTED}" == "1" ]]; then
|
|
|
|
|
|
print_warn "正在停止 PostgreSQL / Redis 容器..."
|
|
|
|
|
|
(
|
|
|
|
|
|
cd "${ROOT_DIR}" && docker compose -f docker-compose.dev.yml stop
|
|
|
|
|
|
) >/dev/null 2>&1 || true
|
|
|
|
|
|
print_ok "PostgreSQL/Redis 容器已停止"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
require_cmd() {
|
|
|
|
|
|
local cmd="$1"
|
|
|
|
|
|
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
|
|
|
|
|
print_err "未找到命令: ${cmd}"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
start_infra() {
|
|
|
|
|
|
print_header "启动 PostgreSQL 和 Redis"
|
|
|
|
|
|
cd "${ROOT_DIR}"
|
|
|
|
|
|
docker compose -f docker-compose.dev.yml up -d
|
|
|
|
|
|
INFRA_STARTED=1
|
|
|
|
|
|
print_ok "基础设施已就绪"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 15:15:35 +08:00
|
|
|
|
# Docker 刚启动时 Postgres 可能尚未接受连接,立即跑 Alembic 会误报失败
|
|
|
|
|
|
wait_postgres_ready() {
|
|
|
|
|
|
local retries=30
|
|
|
|
|
|
local i=0
|
|
|
|
|
|
print_header "等待 PostgreSQL 就绪"
|
|
|
|
|
|
cd "${ROOT_DIR}"
|
|
|
|
|
|
while (( i < retries )); do
|
|
|
|
|
|
if docker compose -f docker-compose.dev.yml exec -T postgres \
|
|
|
|
|
|
pg_isready -U postgres >/dev/null 2>&1; then
|
|
|
|
|
|
print_ok "PostgreSQL 已就绪"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
i=$((i + 1))
|
|
|
|
|
|
done
|
|
|
|
|
|
print_warn "PostgreSQL 在 ${retries}s 内未就绪,迁移可能失败"
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 10:34:09 +08:00
|
|
|
|
get_effective_database_url() {
|
|
|
|
|
|
if [[ -n "${DATABASE_URL:-}" ]]; then
|
|
|
|
|
|
printf '%s\n' "${DATABASE_URL}"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ -f "${ROOT_DIR}/.env" ]]; then
|
|
|
|
|
|
local line
|
|
|
|
|
|
line="$(sed -n 's/^DATABASE_URL=//p' "${ROOT_DIR}/.env" | sed -n '1p')"
|
|
|
|
|
|
line="${line%\"}"
|
|
|
|
|
|
line="${line#\"}"
|
|
|
|
|
|
line="${line%\'}"
|
|
|
|
|
|
line="${line#\'}"
|
|
|
|
|
|
if [[ -n "${line}" ]]; then
|
|
|
|
|
|
printf '%s\n' "${line}"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
warn_database_url_host_pitfall() {
|
|
|
|
|
|
local database_url
|
|
|
|
|
|
local host
|
|
|
|
|
|
|
|
|
|
|
|
if ! database_url="$(get_effective_database_url)"; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "${database_url}" =~ @([^:/?#]+) ]]; then
|
|
|
|
|
|
host="${BASH_REMATCH[1]}"
|
|
|
|
|
|
case "${host}" in
|
|
|
|
|
|
postgres|db|postgres-dev|postgresql)
|
|
|
|
|
|
print_warn "检测到 DATABASE_URL 主机为 ${host};在宿主机执行 Alembic/uvicorn 时通常应使用 localhost"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print_alembic_failure_hint() {
|
|
|
|
|
|
local log_file="$1"
|
|
|
|
|
|
local log_output
|
|
|
|
|
|
|
|
|
|
|
|
log_output="$(sed -n '1,200p' "${log_file}")"
|
|
|
|
|
|
if [[ "${log_output}" == *'could not translate host name "postgres"'* ]] || [[ "${log_output}" == *"Name or service not known"* ]]; then
|
|
|
|
|
|
print_warn "看起来 DATABASE_URL 指向了容器内主机名;在宿主机运行时请改用 localhost:5432"
|
|
|
|
|
|
elif [[ "${log_output}" == *"Connection refused"* ]] || [[ "${log_output}" == *"could not connect to server"* ]]; then
|
|
|
|
|
|
print_warn "PostgreSQL 连接被拒绝;请确认容器已启动且 DATABASE_URL 与 docker-compose.dev.yml 暴露端口一致"
|
|
|
|
|
|
elif [[ "${log_output}" == *"password authentication failed"* ]]; then
|
|
|
|
|
|
print_warn "PostgreSQL 用户名或密码不匹配;请核对 .env.development 中的 DATABASE_URL"
|
|
|
|
|
|
elif [[ "${log_output}" == *"No such file or directory"* ]] || [[ "${log_output}" == *"can't open file"* ]]; then
|
|
|
|
|
|
print_warn "Alembic 依赖的文件或工作目录可能不正确;请确认在 api/ 目录运行脚本"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
is_port_listening() {
|
|
|
|
|
|
local port="$1"
|
|
|
|
|
|
|
|
|
|
|
|
if command -v lsof >/dev/null 2>&1; then
|
|
|
|
|
|
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
|
|
|
|
|
return $?
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ -x "${PYTHON_BIN}" ]]; then
|
|
|
|
|
|
"${PYTHON_BIN}" - "${port}" <<'PY' >/dev/null 2>&1
|
|
|
|
|
|
import socket
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
sock = socket.socket()
|
|
|
|
|
|
sock.settimeout(0.2)
|
|
|
|
|
|
try:
|
|
|
|
|
|
sock.connect(("127.0.0.1", int(sys.argv[1])))
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
raise SystemExit(1)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
sock.close()
|
|
|
|
|
|
raise SystemExit(0)
|
|
|
|
|
|
PY
|
|
|
|
|
|
return $?
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wait_for_tcp_listener() {
|
|
|
|
|
|
local pid="$1"
|
|
|
|
|
|
local port="$2"
|
|
|
|
|
|
local timeout="${3:-8}"
|
|
|
|
|
|
local waited=0
|
|
|
|
|
|
|
|
|
|
|
|
while (( waited < timeout )); do
|
|
|
|
|
|
if is_port_listening "${port}"; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if ! is_pid_alive "${pid}"; then
|
|
|
|
|
|
return 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
waited=$((waited + 1))
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
return 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ensure_background_process_alive() {
|
|
|
|
|
|
local name="$1"
|
|
|
|
|
|
local pid="$2"
|
|
|
|
|
|
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
if ! is_pid_alive "${pid}"; then
|
|
|
|
|
|
print_err "${name} 启动后立即退出,请查看上方日志"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 13:00:34 +08:00
|
|
|
|
ensure_venv() {
|
|
|
|
|
|
print_header "检查 Python 虚拟环境"
|
|
|
|
|
|
|
|
|
|
|
|
if [[ ! -d "${VENV_DIR}" ]]; then
|
|
|
|
|
|
print_warn ".venv 不存在,正在创建"
|
|
|
|
|
|
uv venv "${VENV_DIR}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "${SKIP_INSTALL}" != "1" ]]; then
|
|
|
|
|
|
print_header "安装 Python 依赖"
|
2026-03-18 17:18:23 +08:00
|
|
|
|
uv sync
|
2026-03-09 13:00:34 +08:00
|
|
|
|
print_ok "依赖安装完成"
|
|
|
|
|
|
else
|
|
|
|
|
|
print_warn "已跳过依赖安装 (SKIP_INSTALL=1)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 13:54:41 +08:00
|
|
|
|
# 本地约定:以 .env.development 为真源;每次一键启动都从 .env.development 覆盖 .env,供 pydantic Settings(env_file=".env") 读取。
|
|
|
|
|
|
# 请勿仅在 .env 里改密钥而不同步回 .env.development,否则下次启动会被覆盖。
|
2026-03-23 13:21:07 +08:00
|
|
|
|
ensure_dotenv_from_development() {
|
|
|
|
|
|
print_header "准备本地 .env"
|
|
|
|
|
|
if [[ -f "${ROOT_DIR}/.env.development" ]]; then
|
|
|
|
|
|
cp "${ROOT_DIR}/.env.development" "${ROOT_DIR}/.env"
|
2026-03-23 13:54:41 +08:00
|
|
|
|
print_ok "已从 .env.development 同步为 .env"
|
2026-03-23 13:21:07 +08:00
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
print_warn "未找到 .env.development,无法自动生成 .env"
|
|
|
|
|
|
print_warn "请执行: cp api/.env.example api/.env.development 后按说明填写,再运行 ./development.sh"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 13:00:34 +08:00
|
|
|
|
check_env_file() {
|
|
|
|
|
|
print_header "检查环境变量文件"
|
|
|
|
|
|
if [[ ! -f "${ROOT_DIR}/.env" ]]; then
|
|
|
|
|
|
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
|
|
|
|
|
else
|
|
|
|
|
|
print_ok "检测到 .env"
|
2026-04-07 10:34:09 +08:00
|
|
|
|
warn_database_url_host_pitfall
|
2026-03-09 13:00:34 +08:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
run_migrations() {
|
|
|
|
|
|
print_header "执行数据库迁移"
|
|
|
|
|
|
cd "${ROOT_DIR}"
|
2026-04-07 10:34:09 +08:00
|
|
|
|
local log_file
|
|
|
|
|
|
log_file="$(mktemp -t life-echo-alembic.XXXXXX.log)"
|
|
|
|
|
|
|
|
|
|
|
|
if uv run alembic upgrade head >"${log_file}" 2>&1; then
|
2026-03-18 17:18:23 +08:00
|
|
|
|
print_ok "Alembic 迁移已就绪"
|
2026-04-07 10:34:09 +08:00
|
|
|
|
rm -f "${log_file}"
|
2026-03-18 17:18:23 +08:00
|
|
|
|
else
|
|
|
|
|
|
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
2026-04-07 10:34:09 +08:00
|
|
|
|
print_alembic_failure_hint "${log_file}"
|
|
|
|
|
|
print_warn "Alembic 输出(最近 40 行):"
|
|
|
|
|
|
tail -n 40 "${log_file}"
|
|
|
|
|
|
rm -f "${log_file}"
|
2026-03-18 17:18:23 +08:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 13:00:34 +08:00
|
|
|
|
start_services() {
|
|
|
|
|
|
print_header "启动 FastAPI 和 Celery"
|
|
|
|
|
|
cd "${ROOT_DIR}"
|
|
|
|
|
|
|
2026-04-07 10:34:09 +08:00
|
|
|
|
if is_port_listening "${API_PORT}"; then
|
|
|
|
|
|
print_err "端口 ${API_PORT} 已被占用,无法启动新的 Uvicorn。"
|
|
|
|
|
|
print_err "请先结束占用进程,例如: lsof -nP -iTCP:${API_PORT} -sTCP:LISTEN"
|
|
|
|
|
|
exit 1
|
2026-03-20 15:15:35 +08:00
|
|
|
|
fi
|
|
|
|
|
|
|
2026-03-22 16:45:57 +08:00
|
|
|
|
# 迁移由 main.py 在启动时执行;排除 alembic 目录与 alembic.ini,避免编辑迁移时触发整进程重载
|
|
|
|
|
|
"${UVICORN_BIN}" main:app --reload \
|
|
|
|
|
|
--reload-exclude 'alembic/**' \
|
|
|
|
|
|
--reload-exclude 'alembic.ini' \
|
|
|
|
|
|
--host "${API_HOST}" --port "${API_PORT}" &
|
2026-03-09 13:00:34 +08:00
|
|
|
|
API_PID=$!
|
2026-04-07 10:34:09 +08:00
|
|
|
|
local api_start_status=0
|
|
|
|
|
|
if wait_for_tcp_listener "${API_PID}" "${API_PORT}" 8; then
|
|
|
|
|
|
api_start_status=0
|
|
|
|
|
|
else
|
|
|
|
|
|
api_start_status=$?
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
case "${api_start_status}" in
|
|
|
|
|
|
0)
|
|
|
|
|
|
print_ok "FastAPI 已启动 (PID: ${API_PID})"
|
|
|
|
|
|
;;
|
|
|
|
|
|
1)
|
|
|
|
|
|
print_err "FastAPI 启动失败,进程已退出;请查看上方 Uvicorn 日志"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
print_err "FastAPI 进程仍存活,但端口 ${API_PORT} 未在预期时间内开始监听"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
2026-03-09 13:00:34 +08:00
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
2026-03-09 13:00:34 +08:00
|
|
|
|
CELERY_PID=$!
|
2026-04-07 10:34:09 +08:00
|
|
|
|
ensure_background_process_alive "Celery" "${CELERY_PID}"
|
2026-03-09 13:00:34 +08:00
|
|
|
|
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
|
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
|
echo -e "${GREEN}开发环境启动完成${NC}"
|
|
|
|
|
|
echo "API 文档: http://localhost:${API_PORT}/docs"
|
|
|
|
|
|
echo "健康检查: http://localhost:${API_PORT}/health"
|
|
|
|
|
|
echo "按 Ctrl+C 停止所有进程"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main() {
|
|
|
|
|
|
print_header "Life Echo 开发环境一键启动"
|
|
|
|
|
|
|
|
|
|
|
|
require_cmd "docker"
|
|
|
|
|
|
require_cmd "uv"
|
|
|
|
|
|
|
|
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
|
|
|
|
|
|
|
|
start_infra
|
2026-03-20 15:15:35 +08:00
|
|
|
|
wait_postgres_ready || true
|
2026-03-09 13:00:34 +08:00
|
|
|
|
ensure_venv
|
2026-03-23 13:21:07 +08:00
|
|
|
|
ensure_dotenv_from_development
|
2026-03-09 13:00:34 +08:00
|
|
|
|
check_env_file
|
2026-03-18 17:18:23 +08:00
|
|
|
|
run_migrations
|
2026-03-09 13:00:34 +08:00
|
|
|
|
start_services
|
|
|
|
|
|
|
|
|
|
|
|
wait "${API_PID}" "${CELERY_PID}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main "$@"
|