- backend/db.py:db() 的 DB_PASSWORD 改用 _require_env 强制读取,删除弱默认兜底 - backend/db.py:新增 _convert_placeholders 状态机替换 ? 占位符(跳过字符串字面量),并校验占位符/参数数量,替换原 sql.replace 全局替换脆弱点 - backend/flask_app.py:secret_key 改用 _require_env 强制读取,删除可预测弱默认,防止 session 伪造 - .gitea/workflows/deploy.yml:新增 2.5 步,发布时若线上 shared/.env 缺 DB_PASSWORD/SECRET_KEY 则从 CI secrets 自动补齐(已存在则保留),确保 P0 改动上线后可正常启动
114 lines
4.1 KiB
YAML
114 lines
4.1 KiB
YAML
name: Deploy
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: prod-deploy
|
|
env:
|
|
DEPLOY_BASE: /opt/opc-manager
|
|
REPO_URL: https://qiukai:${{ secrets.DEPLOY_TOKEN }}@git.qiukai.me/qiukai/opc-manager.git
|
|
SERVICE_NAME: opc-manager
|
|
steps:
|
|
- name: Clone and deploy
|
|
run: |
|
|
set -e
|
|
RELEASE_ID="${{ github.sha }}"
|
|
RELEASE_DIR="${DEPLOY_BASE}/releases/${RELEASE_ID}"
|
|
CLONE_DIR="/tmp/opc-deploy-${RELEASE_ID}"
|
|
|
|
echo "=== 1. Clone repository ==="
|
|
rm -rf "${CLONE_DIR}"
|
|
git clone --depth 1 --branch main "${REPO_URL}" "${CLONE_DIR}"
|
|
|
|
echo "=== 2. Prepare release directory ==="
|
|
rm -rf "${RELEASE_DIR}"
|
|
mkdir -p "${RELEASE_DIR}"
|
|
|
|
# Copy repo content to release dir (exclude .git, .env, venv, data)
|
|
rsync -a --exclude='.git' \
|
|
--exclude='.env' \
|
|
--exclude='.env.local' \
|
|
--exclude='.venv' \
|
|
--exclude='data/uploads' \
|
|
--exclude='data/opc.sqlite' \
|
|
--exclude='__pycache__' \
|
|
--exclude='.gitea' \
|
|
"${CLONE_DIR}/" "${RELEASE_DIR}/"
|
|
|
|
echo "=== 2.5 Ensure .env secrets ==="
|
|
# .env 不随 git 走,线上凭据由服务器本地 shared/.env 提供。
|
|
# 若缺失则从 CI secrets 自动补齐;已存在的变量保留,避免覆盖线上配置/导致 session 失效。
|
|
mkdir -p "${DEPLOY_BASE}/shared"
|
|
SHARED_ENV="${DEPLOY_BASE}/shared/.env"
|
|
touch "${SHARED_ENV}"
|
|
ensure_env_var() {
|
|
key="$1"; val="$2"
|
|
if [ -z "$val" ]; then
|
|
echo " ! ${key} 未提供(CI secret 为空),跳过写入"
|
|
return
|
|
fi
|
|
if ! grep -q "^${key}=" "${SHARED_ENV}"; then
|
|
echo "${key}=${val}" >> "${SHARED_ENV}"
|
|
echo " + 已写入 ${key}"
|
|
else
|
|
echo " = ${key} 已存在,保留原值"
|
|
fi
|
|
}
|
|
ensure_env_var "DB_PASSWORD" "${{ secrets.OPC_DB_PASSWORD }}"
|
|
ensure_env_var "SECRET_KEY" "${{ secrets.OPC_SECRET_KEY }}"
|
|
|
|
echo "=== 3. Link shared resources ==="
|
|
mkdir -p "${RELEASE_DIR}/data"
|
|
# .env from shared dir (not in git)
|
|
ln -sfn "${DEPLOY_BASE}/shared/.env" "${RELEASE_DIR}/.env"
|
|
# uploads directory from shared (persist across releases)
|
|
mkdir -p "${DEPLOY_BASE}/shared/uploads"
|
|
ln -sfn "${DEPLOY_BASE}/shared/uploads" "${RELEASE_DIR}/data/uploads"
|
|
|
|
echo "=== 4. Setup Python venv ==="
|
|
cd "${RELEASE_DIR}"
|
|
python3 -m venv .venv
|
|
. .venv/bin/activate
|
|
pip install --no-cache-dir -r requirements.txt
|
|
|
|
echo "=== 5. Restart service ==="
|
|
# Update WorkingDirectory in service via symlink approach
|
|
# The systemd service points to /opt/opc-manager/current
|
|
ln -sfn "${RELEASE_DIR}" "${DEPLOY_BASE}/current"
|
|
systemctl restart "${SERVICE_NAME}"
|
|
sleep 3
|
|
|
|
echo "=== 6. Health check ==="
|
|
for i in 1 2 3 4 5; do
|
|
if curl -fsS http://127.0.0.1:5177/api/health >/dev/null 2>&1; then
|
|
echo "Health check passed"
|
|
break
|
|
fi
|
|
echo "Attempt $i: waiting for service..."
|
|
sleep 2
|
|
done
|
|
|
|
# Final verify
|
|
if ! curl -fsS http://127.0.0.1:5177/api/health >/dev/null 2>&1; then
|
|
echo "ERROR: Health check failed after 5 attempts"
|
|
echo "Rolling back to previous release..."
|
|
PREV=$(ls -t "${DEPLOY_BASE}/releases" | sed -n '2p')
|
|
if [ -n "${PREV}" ]; then
|
|
ln -sfn "${DEPLOY_BASE}/releases/${PREV}" "${DEPLOY_BASE}/current"
|
|
systemctl restart "${SERVICE_NAME}"
|
|
echo "Rolled back to ${PREV}"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== 7. Cleanup old releases ==="
|
|
ls -dt "${DEPLOY_BASE}"/releases/*/ | tail -n +6 | xargs -r rm -rf
|
|
|
|
echo "=== 8. Cleanup temp ==="
|
|
rm -rf "${CLONE_DIR}"
|
|
|
|
echo "=== Deploy complete: ${RELEASE_ID} ==="
|