feat: add syj variant automated deploy script
One-shot release pipeline for syj-shop variant: admin build -> rsync to 8.140.50.89 -> remote .env switch + cache clear + Swoole reload -> healthcheck with auto-rollback on failure. Includes operations manual under docs/project-syj/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
docs/project-syj/deploy.md
Normal file
126
docs/project-syj/deploy.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# syj 商城变体发布手册
|
||||
|
||||
一键脚本:`pro_v3.5.1/deploy/release-syj.sh`
|
||||
覆盖范围:后端 PHP + admin 前端(dist)+ `.env` 切换 + Swoole 平滑 reload + 健康检查 + 失败自动回滚。
|
||||
**不覆盖**:`view/uniapp_v2/`(HBuilderX 手动)、`view/uniapp/`(本次不发布)、composer 依赖、数据库迁移。
|
||||
|
||||
---
|
||||
|
||||
## 1. 首次配置
|
||||
|
||||
### 1.1 本机
|
||||
- 安装 `rsync`、`ssh`、Node 16+(admin 构建)
|
||||
- 配置 SSH 免密:`ssh-copy-id -p 22 root@8.140.50.89`
|
||||
- 验证:`ssh root@8.140.50.89 "ls /www/wwwroot/syj.fsgx.cn"`
|
||||
- 脚本参数集中在 `pro_v3.5.1/deploy/syj.conf`,按实际环境修改
|
||||
|
||||
### 1.2 服务器(首次部署后)
|
||||
- 路径:`/www/wwwroot/syj.fsgx.cn/`(CRMEB 项目根,对应仓库的 `pro_v3.5.1/`)
|
||||
- PHP 8.0 + Swoole 4.x 已装好;`config/swoole.php` 建议显式设置 `pid_file => runtime/swoole/swoole.pid`
|
||||
- Swoole 进程通过 systemd 或 screen 长驻。systemd 单元示例:
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/syj-swoole.service
|
||||
[Unit]
|
||||
Description=syj-shop Swoole HTTP/WS
|
||||
After=network.target mysql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/www/wwwroot/syj.fsgx.cn
|
||||
ExecStart=/usr/bin/php think swoole
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
User=www
|
||||
Group=www
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
`systemctl daemon-reload && systemctl enable --now syj-swoole`
|
||||
|
||||
- Nginx 反代关键片段(端口 443 → Swoole 20199):
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:20199;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
location /admin/ {
|
||||
alias /www/wwwroot/syj.fsgx.cn/view/admin/dist/;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 `.env-syj` 字段含义
|
||||
- `[DATABASE]` HOSTNAME=8.140.50.89 → 与 web 同机的 MySQL
|
||||
- `[REDIS]` HOSTNAME=8.140.50.89 → 同机 Redis
|
||||
- `APP_DEBUG = true` ⚠️ **生产建议改为 false**(脚本不强制改,避免吞掉用户配置)
|
||||
|
||||
---
|
||||
|
||||
## 2. 日常发布
|
||||
|
||||
```bash
|
||||
cd /Users/mac/scott2026/huangjingfen/pro_v3.5.1
|
||||
bash deploy/release-syj.sh
|
||||
```
|
||||
|
||||
阶段日志说明:
|
||||
1. `Pre-flight checks` — 校验分支、`.env-syj` 存在、远端目录可达
|
||||
2. `Build admin` — `view/admin` 下 `npm run build`,产物 `view/admin/dist/`
|
||||
3. `Release tag: 20260510-093015-79436c01` — 时间戳 + git short SHA
|
||||
4. `rsync ->` — 推送代码(带 `--backup-dir` 把被覆盖的旧文件备份到远端 `/www/wwwroot/syj.fsgx.cn-bak/<tag>/`)
|
||||
5. `Remote: switch .env, php think clear, Swoole reload` — 远端拷贝 `.env-syj` → `.env`、清缓存、`kill -USR1 master`
|
||||
6. `Healthcheck` — 串行 curl `https://syj.fsgx.cn/api/version` 与 `/admin/`,每个 URL 重试 5 次
|
||||
7. 成功:写 `deploy/.last-release` 并清理超出 `KEEP_BACKUPS=10` 的旧备份
|
||||
|
||||
可选标志:
|
||||
- `--dry-run` 只打印 rsync 改动,不动远端
|
||||
- `--skip-build` 复用已有 `view/admin/dist/`
|
||||
- `--rollback <tag>` 回滚到指定备份(见下)
|
||||
|
||||
---
|
||||
|
||||
## 3. 回滚
|
||||
|
||||
### 3.1 自动回滚
|
||||
健康检查失败时脚本自动执行:用 `<tag>` 备份覆盖远端代码 → 清缓存 → reload → 再次健康检查。仍失败则脚本退出 2,备份目录保留供人工排查。
|
||||
|
||||
### 3.2 手动回滚
|
||||
查看远端备份:
|
||||
|
||||
```bash
|
||||
ssh root@8.140.50.89 'ls -1t /www/wwwroot/syj.fsgx.cn-bak | head -10'
|
||||
```
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
bash deploy/release-syj.sh --rollback 20260510-093015-79436c01
|
||||
```
|
||||
|
||||
`.env` 不在回滚范围内(`rsync --exclude='.env'`),避免误覆盖运行中的环境配置。
|
||||
|
||||
---
|
||||
|
||||
## 4. 故障排查
|
||||
|
||||
| 现象 | 排查 |
|
||||
|------|------|
|
||||
| admin 502 | Swoole 是否在跑:`systemctl status syj-swoole` 或 `ps aux \| grep "think swoole"` |
|
||||
| `/api/version` 不是 200 | 看远端 `runtime/log/<日期>/cli.log`、Swoole 是否成功 reload |
|
||||
| reload 后代码未生效 | 检查 `runtime/swoole/swoole.pid` 是否过期;删除后让 Swoole 重启重生 |
|
||||
| `.env` 错配(连不上 DB) | `cat /www/wwwroot/syj.fsgx.cn/.env` 对比 `.env-syj` |
|
||||
| 健康检查超时 | 调大 `HEALTH_INTERVAL` / `HEALTH_RETRY`(在 `syj.conf`) |
|
||||
| `public/uploads/` 被清掉 | `rsync-exclude.txt` 有 `public/uploads/`,不会被同步;若已被覆盖,从 `<tag>` 备份恢复 |
|
||||
| 分支不对脚本仍要继续 | 输入 `y` 二次确认;建议先 `git checkout syj-bypass-auth` |
|
||||
|
||||
远端 Swoole 日志:`/www/wwwroot/syj.fsgx.cn/runtime/swoole/swoole.log`
|
||||
本地最近一次发布的 tag:`pro_v3.5.1/deploy/.last-release`
|
||||
211
pro_v3.5.1/deploy/release-syj.sh
Executable file
211
pro_v3.5.1/deploy/release-syj.sh
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env bash
|
||||
# syj 商城变体一键发布脚本
|
||||
# 用法:
|
||||
# bash deploy/release-syj.sh # 正常发布
|
||||
# bash deploy/release-syj.sh --dry-run # 干跑(不动远端)
|
||||
# bash deploy/release-syj.sh --rollback <ts-sha> # 手动回滚到指定备份
|
||||
# bash deploy/release-syj.sh --skip-build # 跳过 admin 构建(复用 dist/)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CONF_FILE="$SCRIPT_DIR/syj.conf"
|
||||
EXCLUDE_FILE="$SCRIPT_DIR/rsync-exclude.txt"
|
||||
LAST_RELEASE_FILE="$SCRIPT_DIR/.last-release"
|
||||
|
||||
[ -f "$CONF_FILE" ] || { echo "ERROR: $CONF_FILE not found"; exit 1; }
|
||||
[ -f "$EXCLUDE_FILE" ] || { echo "ERROR: $EXCLUDE_FILE not found"; exit 1; }
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONF_FILE"
|
||||
|
||||
DRY_RUN=0
|
||||
SKIP_BUILD=0
|
||||
ROLLBACK_TAG=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--rollback) ROLLBACK_TAG="${2:-}"; shift 2 ;;
|
||||
-h|--help) sed -n '2,8p' "$0"; exit 0 ;;
|
||||
*) echo "Unknown arg: $1"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
_ts() { date +%H:%M:%S; }
|
||||
log() { printf "\033[1;36m[%s]\033[0m %s\n" "$(_ts)" "$*"; }
|
||||
warn() { printf "\033[1;33m[%s] WARN\033[0m %s\n" "$(_ts)" "$*"; }
|
||||
err() { printf "\033[1;31m[%s] ERR\033[0m %s\n" "$(_ts)" "$*" >&2; }
|
||||
|
||||
ssh_run() { ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" "$@"; }
|
||||
ssh_pipe() { ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" bash -se; }
|
||||
|
||||
check_health() {
|
||||
local url code i
|
||||
for url in "${HEALTH_URLS[@]}"; do
|
||||
for ((i=1; i<=HEALTH_RETRY; i++)); do
|
||||
code=$(curl -ksS -o /dev/null -w "%{http_code}" --max-time 10 "$url" || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
log " ✓ $url -> 200"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$HEALTH_RETRY" ]; then
|
||||
err " ✗ $url -> $code (after $HEALTH_RETRY tries)"
|
||||
return 1
|
||||
fi
|
||||
sleep "$HEALTH_INTERVAL"
|
||||
done
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
remote_clear_and_reload() {
|
||||
REMOTE_ROOT="$REMOTE_ROOT" ENV_SOURCE_FILE="$ENV_SOURCE_FILE" \
|
||||
ssh_pipe <<'EOF'
|
||||
set -euo pipefail
|
||||
cd "$REMOTE_ROOT"
|
||||
if [ ! -f "$ENV_SOURCE_FILE" ]; then
|
||||
echo "ERR: $ENV_SOURCE_FILE missing on remote" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp -p "$ENV_SOURCE_FILE" .env
|
||||
php think clear
|
||||
PIDFILE=runtime/swoole/swoole.pid
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
PID=$(cat "$PIDFILE")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
kill -USR1 "$PID"
|
||||
echo "Swoole reload via pidfile (master=$PID)"
|
||||
else
|
||||
echo "WARN: stale pidfile, falling back to ps"
|
||||
PID=$(ps -eo pid,ppid,command | awk '/php think swoole/ && $2==1 {print $1; exit}')
|
||||
[ -n "$PID" ] && kill -USR1 "$PID" && echo "Swoole reload via ps (master=$PID)"
|
||||
fi
|
||||
else
|
||||
PID=$(ps -eo pid,ppid,command | awk '/php think swoole/ && $2==1 {print $1; exit}')
|
||||
if [ -n "$PID" ]; then
|
||||
kill -USR1 "$PID"
|
||||
echo "Swoole reload via ps (master=$PID)"
|
||||
else
|
||||
echo "WARN: Swoole master not found; nothing reloaded"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
}
|
||||
|
||||
prune_old_backups() {
|
||||
REMOTE_BAK="$REMOTE_BAK" KEEP_BACKUPS="$KEEP_BACKUPS" ssh_pipe <<'EOF'
|
||||
set -euo pipefail
|
||||
mkdir -p "$REMOTE_BAK"
|
||||
cd "$REMOTE_BAK"
|
||||
ls -1t 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r d; do
|
||||
[ -n "$d" ] && rm -rf -- "$d" && echo "Pruned $REMOTE_BAK/$d"
|
||||
done
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---- ROLLBACK MODE ----
|
||||
if [ -n "$ROLLBACK_TAG" ]; then
|
||||
log "ROLLBACK to $ROLLBACK_TAG"
|
||||
REMOTE_ROOT="$REMOTE_ROOT" REMOTE_BAK="$REMOTE_BAK" TAG="$ROLLBACK_TAG" \
|
||||
ssh_pipe <<'EOF'
|
||||
set -euo pipefail
|
||||
SRC="$REMOTE_BAK/$TAG"
|
||||
[ -d "$SRC" ] || { echo "Backup $SRC not found"; exit 1; }
|
||||
rsync -a --exclude='.env' "$SRC/" "$REMOTE_ROOT/"
|
||||
echo "Rolled back files from $SRC"
|
||||
EOF
|
||||
remote_clear_and_reload
|
||||
sleep "$HEALTH_INTERVAL"
|
||||
check_health || { err "Healthcheck failed after rollback"; exit 1; }
|
||||
log "Rollback OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- PRE-FLIGHT ----
|
||||
log "Pre-flight checks"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
CURRENT_BRANCH=$(git -C "$PROJECT_ROOT/.." rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?")
|
||||
if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then
|
||||
warn "Current branch is '$CURRENT_BRANCH', expected '$EXPECTED_BRANCH'"
|
||||
if [ "$DRY_RUN" -ne 1 ]; then
|
||||
read -r -p "Continue anyway? [y/N] " ans
|
||||
[[ "$ans" =~ ^[Yy]$ ]] || { err "Aborted"; exit 1; }
|
||||
fi
|
||||
fi
|
||||
|
||||
[ -f "$PROJECT_ROOT/$ENV_SOURCE_FILE" ] || { err "$ENV_SOURCE_FILE missing locally"; exit 1; }
|
||||
grep -q "syj.fsgx.cn" "$PROJECT_ROOT/view/admin/.env.production" || warn "view/admin/.env.production not pointing to syj.fsgx.cn"
|
||||
|
||||
ssh_run "test -d '$REMOTE_ROOT'" || { err "Remote root $REMOTE_ROOT does not exist"; exit 1; }
|
||||
|
||||
# ---- BUILD ADMIN ----
|
||||
if [ "$SKIP_BUILD" -eq 1 ]; then
|
||||
log "Skip admin build (--skip-build)"
|
||||
else
|
||||
log "Build admin"
|
||||
(
|
||||
cd "$PROJECT_ROOT/view/admin"
|
||||
if [ ! -d node_modules ]; then
|
||||
npm ci --silent
|
||||
fi
|
||||
npm run build
|
||||
)
|
||||
fi
|
||||
[ -d "$PROJECT_ROOT/view/admin/dist" ] || { err "view/admin/dist missing; build failed?"; exit 1; }
|
||||
|
||||
# ---- VERSION ----
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
SHA=$(git -C "$PROJECT_ROOT/.." rev-parse --short HEAD 2>/dev/null || echo "nogit")
|
||||
TAG="${TS}-${SHA}"
|
||||
log "Release tag: $TAG"
|
||||
|
||||
# ---- RSYNC ----
|
||||
RSYNC_OPTS=(-az --delete --human-readable
|
||||
--exclude-from="$EXCLUDE_FILE"
|
||||
--backup --backup-dir="$REMOTE_BAK/$TAG"
|
||||
-e "ssh -p $SSH_PORT")
|
||||
[ "$DRY_RUN" -eq 1 ] && RSYNC_OPTS+=(--dry-run --itemize-changes)
|
||||
|
||||
log "rsync -> ${SSH_USER}@${SSH_HOST}:$REMOTE_ROOT (backup: $REMOTE_BAK/$TAG)"
|
||||
ssh_run "mkdir -p '$REMOTE_BAK/$TAG'"
|
||||
rsync "${RSYNC_OPTS[@]}" "$PROJECT_ROOT/" "${SSH_USER}@${SSH_HOST}:$REMOTE_ROOT/"
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "Dry-run done. No remote changes applied."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- REMOTE: env switch + clear + reload ----
|
||||
log "Remote: switch .env, php think clear, Swoole reload"
|
||||
remote_clear_and_reload
|
||||
|
||||
# ---- HEALTHCHECK ----
|
||||
sleep "$HEALTH_INTERVAL"
|
||||
log "Healthcheck"
|
||||
if check_health; then
|
||||
echo "$TAG" > "$LAST_RELEASE_FILE"
|
||||
log "Released $TAG ✓"
|
||||
prune_old_backups || warn "Prune step failed (non-fatal)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- AUTO ROLLBACK ----
|
||||
err "Healthcheck failed. Auto-rolling back from $REMOTE_BAK/$TAG"
|
||||
REMOTE_ROOT="$REMOTE_ROOT" REMOTE_BAK="$REMOTE_BAK" TAG="$TAG" \
|
||||
ssh_pipe <<'EOF'
|
||||
set -euo pipefail
|
||||
rsync -a --exclude='.env' "$REMOTE_BAK/$TAG/" "$REMOTE_ROOT/"
|
||||
EOF
|
||||
remote_clear_and_reload
|
||||
sleep "$HEALTH_INTERVAL"
|
||||
if check_health; then
|
||||
err "Rolled back to previous state. Release $TAG aborted."
|
||||
exit 1
|
||||
else
|
||||
err "Rollback healthcheck STILL failing. Manual intervention required."
|
||||
err "Backup at: ${SSH_USER}@${SSH_HOST}:$REMOTE_BAK/$TAG"
|
||||
exit 2
|
||||
fi
|
||||
26
pro_v3.5.1/deploy/rsync-exclude.txt
Normal file
26
pro_v3.5.1/deploy/rsync-exclude.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
.git/
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
runtime/
|
||||
backup/
|
||||
tests/
|
||||
node_modules/
|
||||
public/uploads/
|
||||
.env
|
||||
.env-fsgx
|
||||
.env-huangjinfen
|
||||
view/uniapp/
|
||||
view/uniapp_v2/
|
||||
view/admin/node_modules/
|
||||
view/admin/src/
|
||||
view/admin/public/
|
||||
view/admin/.env*
|
||||
view/admin/package*.json
|
||||
view/admin/babel.config.js
|
||||
view/admin/vue.config.js
|
||||
view/admin/jest.config.js
|
||||
view/admin/alias.config.js
|
||||
view/admin/tests/
|
||||
view/admin/dist/.DS_Store
|
||||
deploy/.last-release
|
||||
27
pro_v3.5.1/deploy/syj.conf
Normal file
27
pro_v3.5.1/deploy/syj.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
# syj 商城变体发布参数(被 release-syj.sh source)
|
||||
# 修改后立即生效,不需要重新构建
|
||||
|
||||
SSH_USER=root
|
||||
SSH_HOST=8.140.50.89
|
||||
SSH_PORT=22
|
||||
REMOTE_ROOT=/www/wwwroot/syj.fsgx.cn
|
||||
REMOTE_BAK=/www/wwwroot/syj.fsgx.cn-bak
|
||||
|
||||
# 健康检查 URL,任一非 200 即触发回滚
|
||||
HEALTH_URLS=(
|
||||
"https://syj.fsgx.cn/api/version"
|
||||
"https://syj.fsgx.cn/admin/"
|
||||
)
|
||||
|
||||
# 备份保留数量(远端 REMOTE_BAK 下保留最近 N 份)
|
||||
KEEP_BACKUPS=10
|
||||
|
||||
# 期望分支;不一致时脚本会要求二次确认
|
||||
EXPECTED_BRANCH=syj-bypass-auth
|
||||
|
||||
# 远端 .env 取自该文件(位于 pro_v3.5.1/ 根)
|
||||
ENV_SOURCE_FILE=.env-syj
|
||||
|
||||
# 健康检查重试
|
||||
HEALTH_RETRY=5
|
||||
HEALTH_INTERVAL=3
|
||||
Reference in New Issue
Block a user