pbls/build.sh

242 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# =============================================================================
# pbls 应用一键部署脚本PBL Agent OS Phase 0/1
# 覆盖 web-application-spec 的 8 步集成清单:
# 1) 拉取代码(应用 + 23 个模块仓)
# 2) 建虚拟环境 + pip install -r requirements.txt
# 3) 建库建表mariadb 方言 DDL幂等
# 4) wwwroot 软链(前端覆盖层 / bricks 静态资源)
# 5) 枚举种子注入pbl_appcodes seed幂等8 组 74 项)
# 6) i18n 语言包编译
# 7) 服务配置systemd unit + 端口 9300 三方一致性校验)
# 8) 启动 + 健康检查
# 用法: bash build.sh [test|prod] (默认 test
# =============================================================================
set -euo pipefail
ENV_NAME="${1:-test}"
APP_NAME="pbls"
APP_PORT="9300"
WS_ROOT="${WS_ROOT:-/d/pipeline/workspaces/0/sdlc_general}"
APP_DIR="${WS_ROOT}/apps/${APP_NAME}"
MOD_DIR="${WS_ROOT}/modules"
PROJ_DIR="${WS_ROOT}/projects/${APP_NAME}"
VENV_DIR="${APP_DIR}/.venv"
WWWROOT="${APP_DIR}/wwwroot"
LOG_DIR="${APP_DIR}/logs"
GIT_BASE="${GIT_BASE:-https://git.example.internal/sdlc}"
GIT_BRANCH="${GIT_BRANCH:-main}"
DB_HOST="${DB_HOST:-127.0.0.1}"
DB_PORT="${DB_PORT:-3306}"
DB_USER="${DB_USER:-pbls}"
DB_PASS="${DB_PASS:-pbls@2026}"
DB_NAME="${DB_NAME:-pbls}"
# 23 个模块load_order与 app/pbls.py、pbls_spec.json 一致)
MODULES=(
appbase rbac pybricks
pbl_common
world scene entity scense scense_runtime script_engine kdb world_snapshot
pbl_appcodes pbl_blueprint pbl_validation pbl_compiler pbl_agent_runtime
pbl_evidence pbl_assessment pbl_kdb_ext pbl_domain_ext pbl_scense_ext
pbl_runtime_ext pbl_template pbl_analytics pbl_governance
)
log() { echo "[build.sh][$(date '+%F %T')] $*"; }
fail() { echo "[build.sh][FATAL] $*" >&2; exit 1; }
log "===== 开始部署 ${APP_NAME} (env=${ENV_NAME}, port=${APP_PORT}) ====="
# -----------------------------------------------------------------------------
# 步骤 0环境与项目 env 文件校验(端口三方一致性硬门禁)
# -----------------------------------------------------------------------------
ENV_FILE="${PROJ_DIR}/env/${ENV_NAME}.json"
[ -f "${ENV_FILE}" ] || fail "缺少项目环境文件:${ENV_FILE}"
command -v python3 >/dev/null 2>&1 || fail "未安装 python3"
command -v git >/dev/null 2>&1 || fail "未安装 git"
# 三方端口一致性conf/config.json == projects/pbls/env/{env}.json == 入口约定 9300
CONF_PORT=$(python3 -c "import json;print(json.load(open('${APP_DIR}/conf/config.json'))['website']['port'])")
ENV_PORT=$(python3 -c "import json;print(json.load(open('${ENV_FILE}'))['website']['port'])")
[ "${CONF_PORT}" = "${APP_PORT}" ] || fail "conf/config.json 端口 ${CONF_PORT} != ${APP_PORT}"
[ "${ENV_PORT}" = "${APP_PORT}" ] || fail "env/${ENV_NAME}.json 端口 ${ENV_PORT} != ${APP_PORT}"
log "步骤0 OK端口三方一致 = ${APP_PORT}"
# -----------------------------------------------------------------------------
# 步骤 1拉取代码应用仓 + 模块仓;已存在则 fetch/reset 到目标分支)
# -----------------------------------------------------------------------------
mkdir -p "${MOD_DIR}" "${LOG_DIR}"
clone_or_update() {
local url="$1" dest="$2"
if [ -d "${dest}/.git" ]; then
log " 更新已存在仓库:${dest}"
git -C "${dest}" fetch --all --prune --quiet || true
git -C "${dest}" checkout "${GIT_BRANCH}" --quiet 2>/dev/null || true
git -C "${dest}" reset --hard "origin/${GIT_BRANCH}" --quiet 2>/dev/null || true
else
log " 克隆:${url} -> ${dest}"
git clone --depth 1 -b "${GIT_BRANCH}" "${url}" "${dest}" || \
log " [WARN] 克隆失败(离线/无权限),使用工作空间已有代码:${dest}"
fi
}
clone_or_update "${GIT_BASE}/${APP_NAME}_app.git" "${APP_DIR}"
for m in "${MODULES[@]}"; do
[ -d "${MOD_DIR}/${m}" ] && continue
clone_or_update "${GIT_BASE}/${m}.git" "${MOD_DIR}/${m}"
done
log "步骤1 OK代码就绪应用 + ${#MODULES[@]} 个模块)"
# -----------------------------------------------------------------------------
# 步骤 2虚拟环境 + 依赖安装
# -----------------------------------------------------------------------------
if [ ! -x "${VENV_DIR}/bin/python" ]; then
python3 -m venv "${VENV_DIR}" || fail "创建 venv 失败"
fi
# shellcheck disable=SC1091
source "${VENV_DIR}/bin/activate"
python -m pip install --upgrade pip setuptools wheel --quiet || true
pip install -r "${APP_DIR}/requirements.txt" --quiet || fail "pip install 失败"
# 模块以源码路径方式挂载PYTHONPATH 注入,避免逐个打包发布)
export PYTHONPATH="${APP_DIR}/app:${MOD_DIR}:${PYTHONPATH:-}"
log "步骤2 OK依赖安装完成PYTHONPATH=${PYTHONPATH}"
# -----------------------------------------------------------------------------
# 步骤 3建库建表mariadb 方言DDL 幂等CREATE TABLE IF NOT EXISTS
# -----------------------------------------------------------------------------
if command -v mysql >/dev/null 2>&1; then
mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
-e "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" \
|| fail "建库失败"
for ddl in "${APP_DIR}"/scripts/ddl/*.sql; do
[ -f "${ddl}" ] || continue
log " 执行 DDL${ddl}"
mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" < "${ddl}" \
|| fail "DDL 执行失败:${ddl}"
done
else
log " [WARN] 未检测到 mysql 客户端,改用应用内建表(模块 tables.py 自动 CREATE IF NOT EXISTS"
fi
log "步骤3 OK36 表 DDL 就绪BIGINT AUTO_INCREMENT 主键 / varchar(32) 编码 / 无 FK / 无 ENUM / 无 TIMESTAMP"
# -----------------------------------------------------------------------------
# 步骤 4wwwroot 软链(前端覆盖层 + bricks 静态资源)
# -----------------------------------------------------------------------------
mkdir -p "${WWWROOT}"
# bricks / sage 前端静态资源(存在才链,缺失不阻断)
for src in "${MOD_DIR}/pybricks/wwwroot" "${MOD_DIR}/appbase/wwwroot" "${MOD_DIR}/rbac/wwwroot"; do
[ -d "${src}" ] || continue
base=$(basename "$(dirname "${src}")")
ln -sfn "${src}" "${WWWROOT}/${base}" && log " 软链:${WWWROOT}/${base} -> ${src}"
done
# PBL 前端覆盖层pbl_scense_ext / pbl_overlay
for ov in pbl_scense_ext pbl_domain_ext; do
[ -d "${MOD_DIR}/${ov}/wwwroot" ] && ln -sfn "${MOD_DIR}/${ov}/wwwroot" "${WWWROOT}/${ov}"
done
[ -d "${APP_DIR}/wwwroot/pbl_overlay" ] && log " 覆盖层:${WWWROOT}/pbl_overlay应用自带"
log "步骤4 OKwwwroot 软链完成"
# -----------------------------------------------------------------------------
# 步骤 5枚举种子注入pbl_appcodes幂等8 组 74 项)
# -----------------------------------------------------------------------------
python - <<'PY' || fail "枚举种子注入失败"
import os, sys
sys.path.insert(0, os.environ.get('MOD_DIR', 'modules'))
from pbl_appcodes.seed import seed_all
res = seed_all(dbname=os.environ.get('DB_NAME', 'pbls'), idempotent=True)
print('[seed] groups=%s items=%s inserted=%s skipped=%s' % (
res.get('groups'), res.get('items'), res.get('inserted'), res.get('skipped')))
assert res.get('groups') == 8 and res.get('items') == 74, '枚举组/项数量不符(应 8 组 74 项)'
PY
log "步骤5 OK枚举种子幂等注入完成"
# -----------------------------------------------------------------------------
# 步骤 6i18n 语言包编译
# -----------------------------------------------------------------------------
if [ -d "${APP_DIR}/i18n" ]; then
python - <<'PY' || log " [WARN] i18n 编译异常,继续"
import json, os, glob
root = 'i18n'
out = {}
for f in glob.glob(os.path.join(root, '**', '*.json'), recursive=True):
lang = os.path.basename(os.path.dirname(f))
out.setdefault(lang, {}).update(json.load(open(f, encoding='utf-8')))
os.makedirs('conf', exist_ok=True)
json.dump(out, open('conf/i18n.compiled.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print('[i18n] langs=%s keys=%s' % (list(out), sum(len(v) for v in out.values())))
PY
else
log " [INFO] 无 i18n 目录,跳过语言包编译(默认 zh_CN"
fi
log "步骤6 OKi18n 处理完成"
# -----------------------------------------------------------------------------
# 步骤 7服务配置systemd unit无 root 时降级为 nohup 启动脚本)
# -----------------------------------------------------------------------------
UNIT_DIR="${APP_DIR}/deploy"
mkdir -p "${UNIT_DIR}"
cat > "${UNIT_DIR}/${APP_NAME}.service" <<UNIT
[Unit]
Description=PBL Agent OS (${APP_NAME}) - env ${ENV_NAME}
After=network.target mysql.service
[Service]
Type=simple
User=${SUDO_USER:-pipeline}
WorkingDirectory=${APP_DIR}
Environment=PBLS_ENV=${ENV_NAME}
Environment=PBLS_DBNAME=${DB_NAME}
Environment=PYTHONPATH=${APP_DIR}/app:${MOD_DIR}
ExecStart=${VENV_DIR}/bin/python ${APP_DIR}/app/${APP_NAME}.py
Restart=always
RestartSec=5
StandardOutput=append:${LOG_DIR}/${APP_NAME}.out
StandardError=append:${LOG_DIR}/${APP_NAME}.err
[Install]
WantedBy=multi-user.target
UNIT
cat > "${UNIT_DIR}/start.sh" <<START
#!/usr/bin/env bash
set -euo pipefail
cd "${APP_DIR}"
export PBLS_ENV="${ENV_NAME}" PBLS_DBNAME="${DB_NAME}"
export PYTHONPATH="${APP_DIR}/app:${MOD_DIR}"
nohup "${VENV_DIR}/bin/python" "${APP_DIR}/app/${APP_NAME}.py" \\
>> "${LOG_DIR}/${APP_NAME}.out" 2>> "${LOG_DIR}/${APP_NAME}.err" &
echo \$! > "${APP_DIR}/${APP_NAME}.pid"
echo "started pid=\$(cat ${APP_DIR}/${APP_NAME}.pid) port=${APP_PORT}"
START
chmod +x "${UNIT_DIR}/start.sh"
log "步骤7 OK服务配置生成${UNIT_DIR}/${APP_NAME}.service + start.sh"
# -----------------------------------------------------------------------------
# 步骤 8启动 + 健康检查
# -----------------------------------------------------------------------------
if [ "${SKIP_START:-0}" = "1" ]; then
log "步骤8 SKIPSKIP_START=1仅完成构建产物"
else
bash "${UNIT_DIR}/start.sh" || fail "启动失败"
ok=0
for i in $(seq 1 30); do
sleep 2
if python - <<PY
import socket,sys
s=socket.socket(); s.settimeout(2)
sys.exit(0 if s.connect_ex(('127.0.0.1', ${APP_PORT}))==0 else 1)
PY
then ok=1; break; fi
done
[ "${ok}" = "1" ] || fail "健康检查失败:端口 ${APP_PORT} 未监听"
log "步骤8 OK${APP_NAME} 已监听 ${APP_PORT}"
fi
log "===== 部署完成http://127.0.0.1:${APP_PORT}/ env=${ENV_NAME}====="