Commit f8a89584 authored by xuchentao's avatar xuchentao

ci: simplify to single-directory deployment

parent 658c58a4
Pipeline #430 passed with stage
in 15 seconds
# 塑美俏官网 · GitLab CI/CD 安全部署 # 塑美俏官网 · GitLab CI/CD 单目录覆盖部署
# 流程:幂等部署 → 配置 Nginx → 验证与清理 # 流程:停服 → 清空 current → 导出提交 → npm ci → 构建 → 启动与健康检查 → Nginx
# 触发:push 到 main 分支 # GitLab 11.7:仅使用 stages、only、tags、script 等基础语法。
#
# ⚠️ GitLab 版本:11.7(较旧)
# - 不使用 workflow:rules
# - 不使用 rules
# - 不使用 needs/DAG
# - 不使用 resource_group
# - 不使用 cache:policy
# 仅使用 only、stages、script、when 等基础语法。
# Runner 注册时请使用标签:sumeiqiao-prod
variables: variables:
DEPLOY_ROOT: "/root/sumeiqiao" DEPLOY_ROOT: "/root/sumeiqiao"
RELEASES_DIR: "/root/sumeiqiao/releases"
SHARED_DIR: "/root/sumeiqiao/shared" SHARED_DIR: "/root/sumeiqiao/shared"
SHARED_ENV: "/root/sumeiqiao/shared/.env" SHARED_ENV: "/root/sumeiqiao/shared/.env"
APP_NAME: "sumeiqiao" APP_NAME: "sumeiqiao"
PORT: "8789"
SITE_URL: "http://127.0.0.1:8789/" SITE_URL: "http://127.0.0.1:8789/"
PM2_BIN: "/usr/bin/pm2" PM2_BIN: "/usr/bin/pm2"
PM2_HOME_DIR: "/root/.pm2" PM2_HOME_DIR: "/root/.pm2"
...@@ -26,14 +15,7 @@ variables: ...@@ -26,14 +15,7 @@ variables:
stages: stages:
- deploy - deploy
- nginx
- verify
# CI_PIPELINE_ID 保证不同 pipeline 不会写同一个 release。同一 deploy job 被重试时,
# 会重新导出相同提交并执行 npm ci;若该 pipeline 已上线,则直接成功退出。
# ── 1. 幂等部署 ──────────────────────────────────────
# 创建 release、安装依赖和部署在同一个 job 中完成,不依赖跨 job 保留 node_modules。
deploy: deploy:
stage: deploy stage: deploy
tags: tags:
...@@ -43,124 +25,16 @@ deploy: ...@@ -43,124 +25,16 @@ deploy:
script: script:
- | - |
set -Eeuo pipefail set -Eeuo pipefail
RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
echo "[deploy] 提交:$CI_COMMIT_SHA" echo "[deploy] 提交:$CI_COMMIT_SHA"
echo "[deploy] 目标 Release:$RELEASE_DIR" test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" >&2 && exit 1)
test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" && exit 1) test -d "$DEPLOY_ROOT" || (echo "部署根目录不存在:$DEPLOY_ROOT" >&2 && exit 1)
test -n "$CI_PIPELINE_ID" || (echo "CI_PIPELINE_ID 缺失" && exit 1) test -d "$SHARED_DIR" || (echo "持久化目录不存在:$SHARED_DIR" >&2 && exit 1)
case "$CI_PIPELINE_ID" in test -f "$SHARED_ENV" || (echo "缺少生产配置:$SHARED_ENV" >&2 && exit 1)
*[!0-9]*) echo "CI_PIPELINE_ID 必须是数字" >&2; exit 2 ;; test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行:$PM2_BIN" >&2 && exit 1)
esac
test -f "$SHARED_ENV" || (echo "缺少生产配置:$SHARED_ENV" && exit 1)
test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行:$PM2_BIN" && exit 1)
case "$RELEASE_DIR" in
"$RELEASES_DIR"/*) ;;
*) echo "release 路径越界" >&2; exit 2 ;;
esac
current_target=$(readlink -f "$DEPLOY_ROOT/current" 2>/dev/null || true)
if [ "$current_target" = "$RELEASE_DIR" ] && [ -f "$RELEASE_DIR/.deploy-success" ]; then
if [ -d "$RELEASE_DIR/node_modules" ] && curl --fail --silent --show-error --max-time 5 "$SITE_URL" >/dev/null; then
echo "[deploy] 该 pipeline 已部署且服务正常,无需重复执行"
exit 0
fi
echo "本次 release 已上线但内容不完整或服务异常,拒绝原地修改" >&2
exit 2
fi
if [ "$current_target" = "$RELEASE_DIR" ]; then
echo "current 已指向未标记成功的本次 release,拒绝原地修改" >&2
exit 2
fi
if [ -n "$current_target" ] && [ -f "$current_target/.pipeline-id" ]; then
current_pipeline_id=$(cat "$current_target/.pipeline-id")
case "$current_pipeline_id" in
*[!0-9]*|'') echo "当前版本的 pipeline 标识不合法" >&2; exit 2 ;;
esac
if [ "$current_pipeline_id" -gt "$CI_PIPELINE_ID" ]; then
echo "较新的 pipeline $current_pipeline_id 已部署,拒绝重试旧 pipeline $CI_PIPELINE_ID" >&2
exit 3
fi
fi
test ! -L "$RELEASE_DIR" || (echo "release 目录不能是软链接" && exit 2)
mkdir -p "$RELEASES_DIR" "$SHARED_DIR" "$RELEASE_DIR"
if [ -f "$RELEASE_DIR/.commit-sha" ] && [ "$(cat "$RELEASE_DIR/.commit-sha")" != "$CI_COMMIT_SHA" ]; then
echo "release 中的提交标识不匹配" >&2
exit 2
fi
if [ -f "$RELEASE_DIR/.pipeline-id" ] && [ "$(cat "$RELEASE_DIR/.pipeline-id")" != "$CI_PIPELINE_ID" ]; then
echo "release 中的 pipeline 标识不匹配" >&2
exit 2
fi
if [ -e "$RELEASE_DIR/.env" ] && [ ! -L "$RELEASE_DIR/.env" ]; then
echo "release 中的 .env 必须是软链接" >&2
exit 2
fi
git config --global --add safe.directory "$CI_PROJECT_DIR" git config --global --add safe.directory "$CI_PROJECT_DIR"
git archive "$CI_COMMIT_SHA" | tar -x -C "$RELEASE_DIR" echo "[deploy] 等待共享部署/构建锁"
printf '%s\n' "$CI_COMMIT_SHA" > "$RELEASE_DIR/.commit-sha" node --env-file="$SHARED_ENV" "$CI_PROJECT_DIR/scripts/run-with-site-lock.mjs" -- \
printf '%s\n' "$CI_PIPELINE_ID" > "$RELEASE_DIR/.pipeline-id" bash "$CI_PROJECT_DIR/scripts/deploy-current.sh" \
ln -sfn "$SHARED_ENV" "$RELEASE_DIR/.env" "$DEPLOY_ROOT" "$CI_PROJECT_DIR" "$CI_COMMIT_SHA" \
"$APP_NAME" "$PM2_BIN" "$PM2_HOME_DIR" "$PM2_USE_SUDO" "$SITE_URL" "$PUBLIC_HOST"
cd "$RELEASE_DIR"
echo "[deploy] 安装依赖"
npm ci
du -sh node_modules
echo "[deploy] 等待 CMS 共享构建锁"
node --env-file="$SHARED_ENV" scripts/run-with-site-lock.mjs -- bash scripts/deploy-release.sh "$DEPLOY_ROOT" "$RELEASE_DIR" "$APP_NAME" "$PM2_BIN" "$PM2_HOME_DIR" "$PM2_USE_SUDO" "$SITE_URL" "$CI_PIPELINE_ID"
echo "[deploy] 当前版本:$(readlink -f "$DEPLOY_ROOT/current")"
# ── 2. 安装并验证 Nginx 配置 ─────────────────────────
# 复制 current release 中的配置,检查语法后重新加载 Nginx。
configure_nginx:
stage: nginx
tags:
- sumeiqiao-prod
only:
- main
script:
- echo "[nginx] 开始配置公网入口:$PUBLIC_HOST"
- test -f "$DEPLOY_ROOT/current/deploy/nginx/sumeiqiao.conf" || (echo "缺少 Nginx 配置" && exit 2)
- sudo -n /usr/bin/install -o root -g root -m 0644 "$DEPLOY_ROOT/current/deploy/nginx/sumeiqiao.conf" /etc/nginx/conf.d/sumeiqiao.conf
- sudo -n /usr/sbin/nginx -t
- sudo -n /usr/bin/systemctl reload nginx
- 'curl --fail --silent --show-error --max-time 5 --header "Host: $PUBLIC_HOST" http://127.0.0.1/ >/dev/null'
- echo "[nginx] Nginx 配置与公网入口验证完成"
# ── 3. 验证并只保留当前 + 上一个成功版本 ─────────────
# when: always 用于输出最终状态;生产健康检查和自动回退已经在 deploy 的共享锁内完成。
verify:
stage: verify
tags:
- sumeiqiao-prod
only:
- main
when: always
script:
- export RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
- echo "[verify] 本次 Release:$RELEASE_DIR"
- echo "[verify] 当前指向:$(readlink -f "$DEPLOY_ROOT/current" 2>/dev/null || echo 未部署)"
- |
verify_status=0
current_target=$(readlink -f "$DEPLOY_ROOT/current" 2>/dev/null || true)
if [ "$current_target" = "$RELEASE_DIR" ]; then
if curl --fail --silent --show-error --max-time 5 "$SITE_URL" >/dev/null; then
echo "[verify] 服务运行正常:$SITE_URL"
else
echo "[verify] 服务无响应,输出本项目最近日志"
sudo -n env PM2_HOME="$PM2_HOME_DIR" "$PM2_BIN" logs "$APP_NAME" --lines 30 --nostream || true
verify_status=1
fi
else
echo "[verify] 本次版本未部署成功,线上 current 保持在其他版本"
fi
echo "[verify] PM2 当前运行项目:"
if ! sudo -n env PM2_HOME="$PM2_HOME_DIR" "$PM2_BIN" list; then
echo "[verify] PM2 项目列表读取失败"
verify_status=1
fi
bash "$CI_PROJECT_DIR/scripts/cleanup-releases.sh" "$DEPLOY_ROOT" "$RELEASE_DIR"
exit "$verify_status"
...@@ -39,7 +39,7 @@ npm start ...@@ -39,7 +39,7 @@ npm start
- `CMS_SECRET`:后台会话签名密钥 - `CMS_SECRET`:后台会话签名密钥
- `CMS_API_KEY`:外部程序调用管理 API 时使用的 Bearer Token - `CMS_API_KEY`:外部程序调用管理 API 时使用的 Bearer Token
- `CMS_DATA_DIR`:生产环境的用户数据目录,建议设置为仓库外的绝对路径 - `CMS_DATA_DIR`:生产环境的用户数据目录,建议设置为仓库外的绝对路径
- `CMS_BUILD_LOCK`:代码部署与文章发布共用的锁文件,生产环境各个 release 必须配置成同一路径 - `CMS_BUILD_LOCK`:代码部署与文章发布共用的锁文件,生产环境必须放在 `shared/`
## 后台 API ## 后台 API
...@@ -87,7 +87,7 @@ npm start ...@@ -87,7 +87,7 @@ npm start
文章状态分为“新建未发布”“修改未发布”和“已上线”。文章网址、摘要、作者及 SEO 时间信息均由程序自动生成,编辑人员只需填写标题、分类和正文。 文章状态分为“新建未发布”“修改未发布”和“已上线”。文章网址、摘要、作者及 SEO 时间信息均由程序自动生成,编辑人员只需填写标题、分类和正文。
## 两条部署流水线 ## 内容发布与代码部署
### 用户提交文章 ### 用户提交文章
...@@ -106,17 +106,24 @@ CMS_DATA_DIR=/root/sumeiqiao/shared/cms-data ...@@ -106,17 +106,24 @@ CMS_DATA_DIR=/root/sumeiqiao/shared/cms-data
CMS_BUILD_LOCK=/root/sumeiqiao/shared/site-build.lock CMS_BUILD_LOCK=/root/sumeiqiao/shared/site-build.lock
``` ```
CI/CD 应在新的 release 目录部署代码,不要在正在运行的目录执行 `git clean`。仓库中的 `.gitlab-ci.yml` 兼容 GitLab 11.7,分为幂等部署、配置 Nginx、验证三个阶段。创建 release、执行 `npm ci`、生产构建和切换都在同一个 deploy job 中完成,不依赖不同 job 之间保留 `node_modules`。同一 pipeline 重试时会重建缺失的依赖;如果该 release 已成功上线,则直接作为成功处理。最终生产构建、切换 current 软链接、重启 PM2 和健康检查在同一把共享锁内完成。应用健康检查通过后才复制 Nginx 配置、检查语法并 reload。健康检查失败时会自动切回上一版本。 生产服务器采用单目录覆盖部署,不保留历史发布版本:
```bash ```text
node --env-file=/root/sumeiqiao/shared/.env scripts/run-with-site-lock.mjs -- bash scripts/deploy-release.sh /root/sumeiqiao /root/sumeiqiao/releases/<release> sumeiqiao /usr/bin/pm2 /root/.pm2 1 http://127.0.0.1:8789/ /root/sumeiqiao/
├── current/ # 唯一运行版本,实体目录
└── shared/ # 环境变量、CMS 数据、上传文件和构建锁等持久化内容
``` ```
`.gitlab-ci.yml` 兼容 GitLab 11.7,仅包含一个 deploy stage。部署在共享锁内依次停止本项目 PM2 进程、清空 `current`、用 `git archive` 导出当前提交、关联 `shared/.env`、执行 `npm ci` 和构建、启动 PM2、进行本机健康检查,最后安装并检查 Nginx 配置、reload 并验证入口。
单目录覆盖部署不提供自动回滚,安装依赖和构建期间服务会短暂停机。如果部署失败,修复问题后重新运行 pipeline。
### GitLab CI/CD 配置 ### GitLab CI/CD 配置
默认假设 GitLab Shell Runner 与生产服务位于同一台服务器,并使用以下路径: 默认假设 GitLab Shell Runner 与生产服务位于同一台服务器,并使用以下路径:
- 部署根目录:`/root/sumeiqiao` - 部署根目录:`/root/sumeiqiao`
- 唯一运行目录:`/root/sumeiqiao/current`(实体目录)
- 持久化配置:`/root/sumeiqiao/shared/.env` - 持久化配置:`/root/sumeiqiao/shared/.env`
- PM2 应用名:`sumeiqiao` - PM2 应用名:`sumeiqiao`
- 服务端口:`8789` - 服务端口:`8789`
...@@ -125,7 +132,7 @@ node --env-file=/root/sumeiqiao/shared/.env scripts/run-with-site-lock.mjs -- ba ...@@ -125,7 +132,7 @@ node --env-file=/root/sumeiqiao/shared/.env scripts/run-with-site-lock.mjs -- ba
如果服务器实际路径或 PM2 应用名不同,只修改 `.gitlab-ci.yml` 顶部变量即可。脚本只会对 `APP_NAME` 指定的一个 PM2 进程执行 `describe``delete``start`,不会停止、重启或保存其他 PM2 项目。 如果服务器实际路径或 PM2 应用名不同,只修改 `.gitlab-ci.yml` 顶部变量即可。脚本只会对 `APP_NAME` 指定的一个 PM2 进程执行 `describe``delete``start`,不会停止、重启或保存其他 PM2 项目。
服务器需提前创建 `shared/.env`,至少配置 `CMS_DATA_DIR``CMS_BUILD_LOCK``CMS_PASSWORD``CMS_SECRET``CMS_API_KEY``CMS_PORT`其中建议使用: 服务器需提前创建实体目录 `shared/` 和其中的 `.env`,至少配置 `CMS_DATA_DIR``CMS_BUILD_LOCK``CMS_PASSWORD``CMS_SECRET``CMS_API_KEY``CMS_PORT`。持久化路径必须位于 `shared/` 下,其中建议使用:
```env ```env
CMS_DATA_DIR=/root/sumeiqiao/shared/cms-data CMS_DATA_DIR=/root/sumeiqiao/shared/cms-data
...@@ -152,8 +159,10 @@ gitlab-runner ALL=(root) NOPASSWD: SUMEIQIAO_NGINX ...@@ -152,8 +159,10 @@ gitlab-runner ALL=(root) NOPASSWD: SUMEIQIAO_NGINX
保存后执行 `sudo visudo -cf /etc/sudoers.d/sumeiqiao-runner` 检查语法。CI 只会从 `/root/sumeiqiao/current` 读取本项目配置,并只会更新 `/etc/nginx/conf.d/sumeiqiao.conf` 保存后执行 `sudo visudo -cf /etc/sudoers.d/sumeiqiao-runner` 检查语法。CI 只会从 `/root/sumeiqiao/current` 读取本项目配置,并只会更新 `/etc/nginx/conf.d/sumeiqiao.conf`
多个 GitLab pipeline 即使同时进入部署阶段,也会由服务器共享锁依次执行;部署持锁时,文章后台不会排队受理发布操作,而会明确提示编辑人员稍后重试。 生产 `.env`、CMS 文章与分类数据、用户上传文件、构建锁及其他需要保留的业务数据都不能写入 `current`。当前应用通过 `CMS_DATA_DIR` 直接读取 CMS 数据与上传文件,通过 `CMS_BUILD_LOCK` 使用共享锁;`current/.env` 只是指向 `shared/.env` 的软链接。
多个 GitLab pipeline 即使同时进入部署阶段,也会由服务器共享锁依次执行。部署持锁时,文章后台不会排队受理发布操作,而会明确提示编辑人员稍后重试。
验证阶段会输出 PM2 当前运行的全部项目,并只保留当前版本和上一个成功版本。清理脚本不会删除其他 pipeline 尚未完成的 release;本 pipeline 失败产生的目录会在自己的验证阶段清理。即使失败目录已被清理,重试 deploy job 也会重新创建 release 和依赖。若 Runner 被强制终止导致验证阶段未执行,可能留下一个未完成目录,但不会影响线上版本,可在确认没有对应 pipeline 运行后手动删除。 首次使用新流程时,如果 `current` 是旧 release 的软链接,部署脚本会先停止本项目进程,只删除该软链接,再创建同名实体目录;不会删除软链接指向的旧版本目录。旧的 `/root/sumeiqiao/releases` 不由部署脚本读取或清理,必须由人工确认后手动删除。
普通本地构建或不包含 release 切换的 CI 可以直接运行 `npm run build`,它会自行取得同一把锁。这样两条流水线不会同时读取或覆盖文章数据,也不会同时替换静态网站。 普通本地构建可以直接运行 `npm run build`,它会自行取得同一把锁。这样内容发布和代码部署不会同时读取或覆盖文章数据,也不会同时替换静态网站。
#!/usr/bin/env bash
set -Eeuo pipefail
deploy_root=${1:?缺少部署根目录}
pipeline_release=${2:-}
case "$deploy_root" in
/*) ;;
*) echo "部署根目录必须是绝对路径" >&2; exit 2 ;;
esac
deploy_root=$(readlink -f "$deploy_root")
releases_dir="$deploy_root/releases"
current_link="$deploy_root/current"
previous_marker="$deploy_root/shared/previous-release"
if [ -n "$pipeline_release" ]; then
pipeline_release=$(readlink -f "$pipeline_release")
case "$pipeline_release" in
"$releases_dir"/*) ;;
*) echo "本 pipeline 的 release 路径越界" >&2; exit 2 ;;
esac
fi
if [ ! -d "$releases_dir" ]; then
echo "[cleanup] Releases 目录不存在,无需清理"
exit 0
fi
current_target=$(readlink -f "$current_link" 2>/dev/null || true)
previous_target=""
if [ -f "$previous_marker" ]; then
previous_target=$(sed -n '1p' "$previous_marker")
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
previous_target=$(readlink -f "$previous_target")
fi
fi
echo "[cleanup] 保留当前版本:${current_target:-}"
echo "[cleanup] 保留上一成功版本:${previous_target:-}"
for candidate in "$releases_dir"/*; do
[ -e "$candidate" ] || continue
if [ -L "$candidate" ] || [ ! -d "$candidate" ]; then
echo "[cleanup] 跳过非普通目录:$candidate"
continue
fi
resolved=$(readlink -f "$candidate")
case "$resolved" in
"$releases_dir"/*) ;;
*) echo "[cleanup] 跳过越界路径:$candidate"; continue ;;
esac
if [ "$resolved" = "$current_target" ] || { [ -n "$previous_target" ] && [ "$resolved" = "$previous_target" ]; }; then
echo "[cleanup] 保留:$resolved"
continue
fi
if [ "$resolved" = "$pipeline_release" ] || [ -f "$resolved/.deploy-success" ]; then
echo "[cleanup] 删除旧 Release:$resolved"
rm -rf -- "$resolved"
else
echo "[cleanup] 跳过未完成 Release(可能属于其他 pipeline):$resolved"
fi
done
echo "[cleanup] 清理完成,当前 Releases:"
find "$releases_dir" -mindepth 1 -maxdepth 1 -type d -print | sort
...@@ -2,21 +2,33 @@ ...@@ -2,21 +2,33 @@
set -Eeuo pipefail set -Eeuo pipefail
deploy_root=${1:?缺少部署根目录} deploy_root=${1:?缺少部署根目录}
release_dir=${2:?缺少 release 目录} source_dir=${2:?缺少 Git 工作目录}
app_name=${3:?缺少 PM2 应用名} commit_sha=${3:?缺少 Git commit SHA}
pm2_bin=${4:?缺少 PM2 可执行文件路径} app_name=${4:?缺少 PM2 应用名}
pm2_home=${5:?缺少 PM2_HOME} pm2_bin=${5:?缺少 PM2 可执行文件路径}
pm2_use_sudo=${6:-1} pm2_home=${6:?缺少 PM2_HOME}
healthcheck_url=${7:-http://127.0.0.1:${CMS_PORT:-8789}/} pm2_use_sudo=${7:-1}
pipeline_id=${8:?缺少 CI pipeline ID} healthcheck_url=${8:-http://127.0.0.1:${CMS_PORT:-8789}/}
public_host=${9:?缺少 Nginx 入口 Host}
deploy_root=${deploy_root%/}
source_dir=${source_dir%/}
current_dir="$deploy_root/current"
shared_dir="$deploy_root/shared"
shared_env="$shared_dir/.env"
nginx_source="$current_dir/deploy/nginx/sumeiqiao.conf"
nginx_target="/etc/nginx/conf.d/sumeiqiao.conf"
case "$deploy_root" in case "$deploy_root" in
/root/?*) ;;
*) echo "部署根目录必须是 /root 下的项目目录" >&2; exit 2 ;;
esac
case "$source_dir" in
/*) ;; /*) ;;
*) echo "部署根目录必须是绝对路径" >&2; exit 2 ;; *) echo "Git 工作目录必须是绝对路径" >&2; exit 2 ;;
esac esac
case "$release_dir" in case "$commit_sha" in
"$deploy_root"/releases/*) ;; *[!0-9a-f]*|'') echo "Git commit SHA 不合法" >&2; exit 2 ;;
*) echo "release 目录必须位于 $deploy_root/releases/ 下" >&2; exit 2 ;;
esac esac
case "$app_name" in case "$app_name" in
*[!A-Za-z0-9_.-]*|'') echo "PM2 应用名包含不安全字符" >&2; exit 2 ;; *[!A-Za-z0-9_.-]*|'') echo "PM2 应用名包含不安全字符" >&2; exit 2 ;;
...@@ -33,20 +45,17 @@ case "$pm2_use_sudo" in ...@@ -33,20 +45,17 @@ case "$pm2_use_sudo" in
0|1) ;; 0|1) ;;
*) echo "PM2_USE_SUDO 只能是 0 或 1" >&2; exit 2 ;; *) echo "PM2_USE_SUDO 只能是 0 或 1" >&2; exit 2 ;;
esac esac
case "$pipeline_id" in
*[!0-9]*|'') echo "CI pipeline ID 必须是数字" >&2; exit 2 ;;
esac
shared_dir="$deploy_root/shared"
shared_env="$shared_dir/.env"
current_link="$deploy_root/current"
if [ "${SUMEIQIAO_SITE_LOCK_HELD:-}" != "1" ]; then if [ "${SUMEIQIAO_SITE_LOCK_HELD:-}" != "1" ]; then
echo "部署必须通过 run-with-site-lock.mjs 执行" >&2 echo "部署必须通过 run-with-site-lock.mjs 执行" >&2
exit 2 exit 2
fi fi
if [ ! -d "$release_dir" ] || [ ! -f "$release_dir/package-lock.json" ]; then if [ -L "$deploy_root" ] || [ ! -d "$deploy_root" ]; then
echo "release 目录不完整:$release_dir" >&2 echo "部署根目录必须是已存在的实体目录:$deploy_root" >&2
exit 2
fi
if [ -L "$shared_dir" ] || [ ! -d "$shared_dir" ]; then
echo "shared 必须是已存在的实体目录:$shared_dir" >&2
exit 2 exit 2
fi fi
if [ ! -f "$shared_env" ]; then if [ ! -f "$shared_env" ]; then
...@@ -61,16 +70,28 @@ case "${CMS_BUILD_LOCK:-}" in ...@@ -61,16 +70,28 @@ case "${CMS_BUILD_LOCK:-}" in
"$shared_dir"/*) ;; "$shared_dir"/*) ;;
*) echo "CMS_BUILD_LOCK 必须位于 $shared_dir/ 下" >&2; exit 2 ;; *) echo "CMS_BUILD_LOCK 必须位于 $shared_dir/ 下" >&2; exit 2 ;;
esac esac
if [ -e "$current_link" ] && [ ! -L "$current_link" ]; then if [ "$(git -C "$source_dir" rev-parse --is-inside-work-tree 2>/dev/null || true)" != "true" ]; then
echo "$current_link 必须是软链接,拒绝覆盖真实目录" >&2 echo "Git 工作目录不完整:$source_dir" >&2
exit 2 exit 2
fi fi
command -v curl >/dev/null if ! git -C "$source_dir" cat-file -e "$commit_sha^{commit}"; then
test -x "$pm2_bin" echo "Git 提交不存在:$commit_sha" >&2
if [ ! -f "$release_dir/.pipeline-id" ] || [ "$(cat "$release_dir/.pipeline-id")" != "$pipeline_id" ]; then exit 2
echo "release 的 pipeline 标识不匹配" >&2 fi
if git -C "$source_dir" cat-file -e "$commit_sha:.env" 2>/dev/null; then
echo "提交中不得包含生产 .env;请只使用 $shared_env" >&2
exit 2 exit 2
fi fi
for required_path in package.json package-lock.json server.mjs scripts/build-site.mjs deploy/nginx/sumeiqiao.conf; do
if ! git -C "$source_dir" cat-file -e "$commit_sha:$required_path"; then
echo "待部署提交缺少必要文件:$required_path" >&2
exit 2
fi
done
command -v curl >/dev/null
command -v npm >/dev/null
command -v tar >/dev/null
test -x "$pm2_bin"
pm2_command() { pm2_command() {
if [ "$pm2_use_sudo" = "1" ]; then if [ "$pm2_use_sudo" = "1" ]; then
...@@ -80,70 +101,45 @@ pm2_command() { ...@@ -80,70 +101,45 @@ pm2_command() {
fi fi
} }
# 只删除并重建 APP_NAME 对应的一个进程;不执行 pm2 kill、delete all、restart all 或 pm2 save。 echo "[deploy] 停止本项目 PM2 进程:$app_name"
start_current_app() { if pm2_command describe "$app_name" >/dev/null 2>&1; then
if pm2_command describe "$app_name" >/dev/null 2>&1; then pm2_command delete "$app_name"
pm2_command delete "$app_name" fi
fi
pm2_command start "$current_link/server.mjs" \
--name "$app_name" \
--cwd "$current_link" \
--node-args="--env-file=$shared_env"
}
previous_target="" if [ -L "$current_dir" ]; then
if [ -L "$current_link" ]; then old_target=$(readlink "$current_dir")
previous_target=$(readlink -f "$current_link") echo "[deploy] 首次迁移:仅删除 current 软链接(原目标保持不变):$old_target"
unlink "$current_dir"
elif [ -e "$current_dir" ] && [ ! -d "$current_dir" ]; then
echo "current 必须是目录或首次迁移用的软链接:$current_dir" >&2
exit 2
fi fi
# GitLab 11.7 没有 resource_group。若较新的 pipeline 已上线,拒绝旧 pipeline 覆盖它。 mkdir -p "$current_dir"
if [ -n "$previous_target" ] && [ -f "$previous_target/.pipeline-id" ]; then if [ -L "$current_dir" ] || [ "$(readlink -f "$current_dir")" != "$current_dir" ]; then
current_pipeline_id=$(cat "$previous_target/.pipeline-id") echo "current 必须是部署根目录下的实体目录:$current_dir" >&2
case "$current_pipeline_id" in exit 2
*[!0-9]*|'') echo "当前版本的 pipeline 标识不合法" >&2; exit 2 ;;
esac
if [ "$current_pipeline_id" -gt "$pipeline_id" ]; then
echo "较新的 pipeline $current_pipeline_id 已部署,拒绝旧 pipeline $pipeline_id 覆盖" >&2
exit 3
fi
if [ "$current_pipeline_id" -eq "$pipeline_id" ] && [ "$previous_target" = "$release_dir" ]; then
echo "该 pipeline 已经部署,无需重复切换"
exit 0
fi
fi fi
switched=0
rollback() {
exit_code=$?
trap - ERR
if [ "$switched" = "1" ]; then
echo "新版本部署失败,正在恢复上一版本……" >&2
rollback_link="$deploy_root/.current-rollback-$$"
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
ln -s "$previous_target" "$rollback_link"
mv -Tf "$rollback_link" "$current_link"
start_current_app || true
else
if pm2_command describe "$app_name" >/dev/null 2>&1; then
pm2_command delete "$app_name" || true
fi
echo "没有可恢复的上一版本,已停止本项目进程" >&2
fi
fi
exit "$exit_code"
}
trap rollback ERR
cd "$release_dir" echo "[deploy] 清空 current 实体目录"
echo "[deploy] 使用共享用户内容执行最终构建" find "$current_dir" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
npm run build:inside-lock
echo "[deploy] 导出提交 $commit_sha 到 current"
git -C "$source_dir" archive "$commit_sha" | tar -x -C "$current_dir"
ln -s "$shared_env" "$current_dir/.env"
next_link="$deploy_root/.current-next-$$" cd "$current_dir"
ln -s "$release_dir" "$next_link" echo "[deploy] 安装依赖"
mv -Tf "$next_link" "$current_link" npm ci
switched=1
start_current_app echo "[deploy] 构建应用"
npm run build:inside-lock
echo "[deploy] 启动本项目 PM2 进程"
pm2_command start "$current_dir/server.mjs" \
--name "$app_name" \
--cwd "$current_dir" \
--node-args="--env-file=$shared_env"
healthy=0 healthy=0
for attempt in $(seq 1 30); do for attempt in $(seq 1 30); do
...@@ -151,19 +147,20 @@ for attempt in $(seq 1 30); do ...@@ -151,19 +147,20 @@ for attempt in $(seq 1 30); do
healthy=1 healthy=1
break break
fi fi
echo "等待服务启动... ($attempt/30)" echo "[deploy] 等待服务启动... ($attempt/30)"
sleep 1 sleep 1
done done
if [ "$healthy" != "1" ]; then if [ "$healthy" != "1" ]; then
echo "健康检查失败:$healthcheck_url" >&2 echo "健康检查失败:$healthcheck_url" >&2
pm2_command logs "$app_name" --lines 30 --nostream || true pm2_command logs "$app_name" --lines 30 --nostream || true
false exit 1
fi fi
trap - ERR echo "[deploy] 更新并验证 Nginx"
previous_marker_tmp="$shared_dir/.previous-release-$$" test -f "$nginx_source"
printf '%s\n' "$previous_target" > "$previous_marker_tmp" sudo -n /usr/bin/install -o root -g root -m 0644 "$nginx_source" "$nginx_target"
mv -f "$previous_marker_tmp" "$shared_dir/previous-release" sudo -n /usr/sbin/nginx -t
printf '%s\n' "$pipeline_id" > "$release_dir/.deploy-success" sudo -n /usr/bin/systemctl reload nginx
echo "部署成功:$release_dir" curl --fail --silent --show-error --max-time 5 --header "Host: $public_host" http://127.0.0.1/ >/dev/null
echo "[deploy] 部署成功:$current_dir$commit_sha)"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment