chore(deploy): document auto restart flow
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# 自动构建与自动重启流程
|
||||||
|
|
||||||
|
本文档说明当前项目通过 Git remote + `post-receive` hook 实现自动构建、自动重启的流程。核心文件为 [.githooks/post-receive](../.githooks/post-receive)。
|
||||||
|
|
||||||
|
## 部署入口
|
||||||
|
|
||||||
|
当前涉及两个服务器 remote:
|
||||||
|
|
||||||
|
| 环境 | Git remote | 推送分支 | 服务端端口 | 部署脚本 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 测试环境 | `test` | `test` | `9135` | `scripts/deploy/deploy_web_test.sh` |
|
||||||
|
| 生产环境 | `production` | `main` | `9185` | `scripts/deploy/deploy_web.sh` |
|
||||||
|
|
||||||
|
当前 remote 配置示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test root@43.106.13.130:/root/cozsweet-repos/test
|
||||||
|
production root@43.106.13.130:/root/cozsweet-repos/main
|
||||||
|
```
|
||||||
|
|
||||||
|
常用发布入口:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 发布测试环境
|
||||||
|
./scripts/release/pre_release_web.sh
|
||||||
|
|
||||||
|
# 发布生产环境
|
||||||
|
./scripts/release/release_web.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以只执行推送脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 推送 test remote 的 test 分支
|
||||||
|
./scripts/deploy/deploy_web_test.sh
|
||||||
|
|
||||||
|
# 推送 production remote 的 main 分支
|
||||||
|
./scripts/deploy/deploy_web.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地发布流程
|
||||||
|
|
||||||
|
测试环境发布流程:
|
||||||
|
|
||||||
|
1. 进入 test worktree:`cozsweet-frontend-nextjs.worktrees/cozsweet-nextjs-test`
|
||||||
|
2. 确认当前分支。
|
||||||
|
3. 执行 `git rebase dev`,把 `dev` 的最新代码变基到 `test` 分支。
|
||||||
|
4. 复制测试环境图标到 `public/`。
|
||||||
|
5. 复制 `env-example/.env.local.example` 到 `.env.local`。
|
||||||
|
6. 执行 `scripts/deploy/deploy_web_test.sh`。
|
||||||
|
7. `deploy_web_test.sh` 推送 `test` remote 的 `test` 分支。
|
||||||
|
8. 推送成功后尝试清除 Cloudflare CDN 缓存。
|
||||||
|
|
||||||
|
生产环境发布流程:
|
||||||
|
|
||||||
|
1. 进入 main worktree:`cozsweet-frontend-nextjs.worktrees/cozsweet-nextjs-main`
|
||||||
|
2. 确认当前分支。
|
||||||
|
3. 执行 `git rebase dev`,把 `dev` 的最新代码变基到 `main` 分支。
|
||||||
|
4. 复制生产环境图标到 `public/`。
|
||||||
|
5. 复制 `env-example/.env.production.example` 到 `.env.production`。
|
||||||
|
6. 执行 `scripts/deploy/deploy_web.sh`。
|
||||||
|
7. `deploy_web.sh` 强制推送 `production` remote 的 `main` 分支。
|
||||||
|
8. 推送成功后尝试清除 Cloudflare CDN 缓存。
|
||||||
|
|
||||||
|
## 服务器端 post-receive 流程
|
||||||
|
|
||||||
|
服务器收到 push 后,`post-receive` hook 会自动执行以下步骤:
|
||||||
|
|
||||||
|
1. 初始化 Node / pnpm 环境。
|
||||||
|
2. 解析当前仓库目录,进入服务端工作树。
|
||||||
|
3. 初始化日志文件:`logs/post-receive.log`。
|
||||||
|
4. 覆盖写入本次执行元信息,包括时间、分支、commit、cwd。
|
||||||
|
5. 执行 `git reset --hard HEAD`,让服务端工作树同步到刚推送的最新 commit。
|
||||||
|
6. 根据当前分支复制环境变量文件。
|
||||||
|
7. 根据当前分支选择 Next.js 启动端口。
|
||||||
|
8. 执行 `pnpm run build:deploy`。
|
||||||
|
9. 如果构建失败,写入失败日志并中止,不会重启服务。
|
||||||
|
10. 如果构建成功,查找并强杀当前端口上的旧 Next.js 进程。
|
||||||
|
11. 等待端口释放,并重试确认。
|
||||||
|
12. 端口释放成功后,执行 `nohup pnpm run start` 后台启动新服务。
|
||||||
|
|
||||||
|
## 分支、环境变量、端口映射
|
||||||
|
|
||||||
|
`post-receive` 当前按服务端工作树所在分支判断环境:
|
||||||
|
|
||||||
|
| 服务端分支 | 环境变量复制 | 启动端口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `main` | `env-example/.env.production.example` → `.env.production` | `9185` |
|
||||||
|
| `test` | `env-example/.env.local.example` → `.env.local` | `9135` |
|
||||||
|
| `dev` | `env-example/.env.development.example` → `.env.local` | `9135` |
|
||||||
|
| 其他分支 | 跳过 env 复制 | `9135` |
|
||||||
|
|
||||||
|
本地生产发布脚本和服务器端 `post-receive` 已统一使用 `.env.production`。Next.js 在生产构建时会读取 `.env.production`;测试和开发发布仍使用 `.env.local`。
|
||||||
|
|
||||||
|
本地部署脚本清除 Cloudflare CDN 缓存时,也会按当前分支读取 env:`main` 优先读取 `.env.production`,其他分支优先读取 `.env.local`。如果优先文件不存在或缺少 `CF_ZONE_ID` / `CF_API_TOKEN`,会尝试读取另一个文件作为兜底。
|
||||||
|
|
||||||
|
## 自动重启机制
|
||||||
|
|
||||||
|
重启由 `post-receive` 中的端口进程管理实现:
|
||||||
|
|
||||||
|
1. 使用 `ss -tulnp` 查询占用目标端口的进程。
|
||||||
|
2. 如果找到旧进程,执行 `kill -9` 强制停止。
|
||||||
|
3. 默认等待 `3` 秒。
|
||||||
|
4. 最多重试 `3` 次确认端口已经释放。
|
||||||
|
5. 如果端口仍未释放,中止启动,避免新服务出现 `EADDRINUSE`。
|
||||||
|
6. 如果端口释放成功,执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT="$START_PORT" nohup pnpm run start >> "$LOG_FILE" 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
|
`package.json` 中的 `start` 脚本会优先使用传入的 `PORT`,因此测试环境固定使用 `9135`,生产环境固定使用 `9185`。
|
||||||
|
|
||||||
|
## 日志与排查
|
||||||
|
|
||||||
|
服务器端日志文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
logs/post-receive.log
|
||||||
|
```
|
||||||
|
|
||||||
|
当前日志策略是每次 `post-receive` 执行时覆盖旧日志,而不是追加。
|
||||||
|
|
||||||
|
常见排查命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看最近一次部署日志
|
||||||
|
tail -n 200 logs/post-receive.log
|
||||||
|
|
||||||
|
# 查看端口占用
|
||||||
|
ss -tulnp | grep ':9135'
|
||||||
|
ss -tulnp | grep ':9185'
|
||||||
|
|
||||||
|
# 查看当前分支和 commit
|
||||||
|
git branch --show-current
|
||||||
|
git rev-parse --short HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
构建失败时,日志中会出现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
build EXIT_CODE=<非 0>
|
||||||
|
ABORTED ... (build failed, start skipped)
|
||||||
|
```
|
||||||
|
|
||||||
|
这种情况下不会停止旧服务,也不会启动新服务。
|
||||||
|
|
||||||
|
端口释放失败时,日志中会出现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
stop FAILED: port <端口> still occupied
|
||||||
|
```
|
||||||
|
|
||||||
|
这种情况下不会启动新服务,需要人工检查占用该端口的进程。
|
||||||
|
|
||||||
|
## 关键约束
|
||||||
|
|
||||||
|
1. `scripts/deploy/*` 只负责推送代码,不负责本地构建和本地启动。
|
||||||
|
2. 自动构建和自动重启只发生在服务器端 `post-receive` hook。
|
||||||
|
3. `main` 分支对应生产环境,`test` 分支对应测试环境。
|
||||||
|
4. 生产推送当前使用 `git push --force production main`。
|
||||||
|
5. 构建失败时不会启动新服务,避免用旧 `.next` 跑新代码。
|
||||||
|
6. 服务启动使用 `nohup` 后台进程,不依赖 PM2 或 systemd。
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. 将服务进程交给 PM2 或 systemd 管理,提升崩溃自动恢复能力。
|
||||||
|
2. 保留最近 N 次部署日志,而不是只保留最后一次日志,方便排查历史问题。
|
||||||
|
3. 构建命令可以保留完整输出到单独日志文件,当前 `pnpm run build:deploy` 输出被丢弃,只记录退出码。
|
||||||
|
4. 可以在启动后增加健康检查,确认新服务真正可访问后再判定部署成功。
|
||||||
@@ -91,16 +91,38 @@ bump_version() {
|
|||||||
echo "$new_version"
|
echo "$new_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 清除 Cloudflare CDN 缓存
|
source_env_file() {
|
||||||
# 凭据从 .env.local 读(CF_ZONE_ID / CF_API_TOKEN)
|
local file="$1"
|
||||||
purge_cdn_cache() {
|
if [ ! -f "$file" ]; then
|
||||||
# 从 .env.local 读 CDN 凭据(Next.js 不读取,bash 直接 source)
|
return 1
|
||||||
if [ -f .env.local ]; then
|
fi
|
||||||
# shell 解析 KEY=VALUE(忽略注释 / 引号)
|
|
||||||
|
echo "读取环境变量文件: $file"
|
||||||
set -a
|
set -a
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1090
|
||||||
source .env.local 2>/dev/null || true
|
source "$file" 2>/dev/null || true
|
||||||
set +a
|
set +a
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清除 Cloudflare CDN 缓存
|
||||||
|
# 凭据按当前分支读取:
|
||||||
|
# main → .env.production
|
||||||
|
# 其他 → .env.local
|
||||||
|
purge_cdn_cache() {
|
||||||
|
local current=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||||
|
local primary_env=".env.local"
|
||||||
|
local fallback_env=".env.production"
|
||||||
|
|
||||||
|
if [ "$current" = "main" ]; then
|
||||||
|
primary_env=".env.production"
|
||||||
|
fallback_env=".env.local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
source_env_file "$primary_env" || true
|
||||||
|
|
||||||
|
if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ]; then
|
||||||
|
source_env_file "$fallback_env" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ]; then
|
if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ]; then
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ cp -f "$WORKTREE_PATH/icons/production/favicon.ico" "$WORKTREE_PATH/public/favic
|
|||||||
cp -f "$WORKTREE_PATH/icons/production/icons/Icon-192.png" "$WORKTREE_PATH/public/images/icons/Icon-192.png"
|
cp -f "$WORKTREE_PATH/icons/production/icons/Icon-192.png" "$WORKTREE_PATH/public/images/icons/Icon-192.png"
|
||||||
cp -f "$WORKTREE_PATH/icons/production/icons/Icon-512.png" "$WORKTREE_PATH/public/images/icons/Icon-512.png"
|
cp -f "$WORKTREE_PATH/icons/production/icons/Icon-512.png" "$WORKTREE_PATH/public/images/icons/Icon-512.png"
|
||||||
|
|
||||||
# 准备生产环境变量($WORKTREE_PATH/env-example/.env.production.example → $WORKTREE_PATH/.env.local)
|
# 准备生产环境变量($WORKTREE_PATH/env-example/.env.production.example → $WORKTREE_PATH/.env.production)
|
||||||
echo "=== 准备生产环境变量(env-example/.env.production.example → .env.local) ==="
|
echo "=== 准备生产环境变量(env-example/.env.production.example → .env.production) ==="
|
||||||
cp -f "$WORKTREE_PATH/env-example/.env.production.example" "$WORKTREE_PATH/.env.local"
|
cp -f "$WORKTREE_PATH/env-example/.env.production.example" "$WORKTREE_PATH/.env.production"
|
||||||
|
|
||||||
echo "=== 执行构建部署脚本 ==="
|
echo "=== 执行构建部署脚本 ==="
|
||||||
./scripts/deploy/deploy_web.sh
|
./scripts/deploy/deploy_web.sh
|
||||||
|
|||||||
Reference in New Issue
Block a user