Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
S
sumeiqiao
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
sumeiqiao
Commits
f8a89584
Commit
f8a89584
authored
Jul 31, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
ci: simplify to single-directory deployment
parent
658c58a4
Pipeline
#430
passed with stage
in 15 seconds
Changes
4
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
197 additions
and
213 deletions
+197
-213
.gitlab-ci.yml
.gitlab-ci.yml
+13
-139
CMS_README.md
CMS_README.md
+18
-9
cleanup-releases.sh
scripts/cleanup-releases.sh
+0
-65
deploy-current.sh
scripts/deploy-current.sh
+166
-0
No files found.
.gitlab-ci.yml
View file @
f8a89584
# 塑美俏官网 · GitLab CI/CD 安全部署
# 流程:幂等部署 → 配置 Nginx → 验证与清理
# 触发:push 到 main 分支
#
# ⚠️ GitLab 版本:11.7(较旧)
# - 不使用 workflow:rules
# - 不使用 rules
# - 不使用 needs/DAG
# - 不使用 resource_group
# - 不使用 cache:policy
# 仅使用 only、stages、script、when 等基础语法。
# Runner 注册时请使用标签:sumeiqiao-prod
# 塑美俏官网 · GitLab CI/CD 单目录覆盖部署
# 流程:停服 → 清空 current → 导出提交 → npm ci → 构建 → 启动与健康检查 → Nginx
# GitLab 11.7:仅使用 stages、only、tags、script 等基础语法。
variables
:
DEPLOY_ROOT
:
"
/root/sumeiqiao"
RELEASES_DIR
:
"
/root/sumeiqiao/releases"
SHARED_DIR
:
"
/root/sumeiqiao/shared"
SHARED_ENV
:
"
/root/sumeiqiao/shared/.env"
APP_NAME
:
"
sumeiqiao"
PORT
:
"
8789"
SITE_URL
:
"
http://127.0.0.1:8789/"
PM2_BIN
:
"
/usr/bin/pm2"
PM2_HOME_DIR
:
"
/root/.pm2"
...
...
@@ -26,14 +15,7 @@ variables:
stages
:
-
deploy
-
nginx
-
verify
# CI_PIPELINE_ID 保证不同 pipeline 不会写同一个 release。同一 deploy job 被重试时,
# 会重新导出相同提交并执行 npm ci;若该 pipeline 已上线,则直接成功退出。
# ── 1. 幂等部署 ──────────────────────────────────────
# 创建 release、安装依赖和部署在同一个 job 中完成,不依赖跨 job 保留 node_modules。
deploy
:
stage
:
deploy
tags
:
...
...
@@ -43,124 +25,16 @@ deploy:
script
:
-
|
set -Eeuo pipefail
RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
echo "[deploy] 提交:$CI_COMMIT_SHA"
echo "[deploy] 目标 Release:$RELEASE_DIR"
test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" && exit 1)
test -n "$CI_PIPELINE_ID" || (echo "CI_PIPELINE_ID 缺失" && exit 1)
case "$CI_PIPELINE_ID" in
*[!0-9]*) echo "CI_PIPELINE_ID 必须是数字" >&2; exit 2 ;;
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
test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" >&2 && exit 1)
test -d "$DEPLOY_ROOT" || (echo "部署根目录不存在:$DEPLOY_ROOT" >&2 && exit 1)
test -d "$SHARED_DIR" || (echo "持久化目录不存在:$SHARED_DIR" >&2 && exit 1)
test -f "$SHARED_ENV" || (echo "缺少生产配置:$SHARED_ENV" >&2 && exit 1)
test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行:$PM2_BIN" >&2 && exit 1)
git config --global --add safe.directory "$CI_PROJECT_DIR"
git archive "$CI_COMMIT_SHA" | tar -x -C "$RELEASE_DIR"
printf '%s\n' "$CI_COMMIT_SHA" > "$RELEASE_DIR/.commit-sha"
printf '%s\n' "$CI_PIPELINE_ID" > "$RELEASE_DIR/.pipeline-id"
ln -sfn "$SHARED_ENV" "$RELEASE_DIR/.env"
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"
echo "[deploy] 等待共享部署/构建锁"
node --env-file="$SHARED_ENV" "$CI_PROJECT_DIR/scripts/run-with-site-lock.mjs" -- \
bash "$CI_PROJECT_DIR/scripts/deploy-current.sh" \
"$DEPLOY_ROOT" "$CI_PROJECT_DIR" "$CI_COMMIT_SHA" \
"$APP_NAME" "$PM2_BIN" "$PM2_HOME_DIR" "$PM2_USE_SUDO" "$SITE_URL" "$PUBLIC_HOST"
CMS_README.md
View file @
f8a89584
...
...
@@ -39,7 +39,7 @@ npm start
-
`CMS_SECRET`
:后台会话签名密钥
-
`CMS_API_KEY`
:外部程序调用管理 API 时使用的 Bearer Token
-
`CMS_DATA_DIR`
:生产环境的用户数据目录,建议设置为仓库外的绝对路径
-
`CMS_BUILD_LOCK`
:代码部署与文章发布共用的锁文件,生产环境
各个 release 必须配置成同一路径
-
`CMS_BUILD_LOCK`
:代码部署与文章发布共用的锁文件,生产环境
必须放在
`shared/`
中
## 后台 API
...
...
@@ -87,7 +87,7 @@ npm start
文章状态分为“新建未发布”“修改未发布”和“已上线”。文章网址、摘要、作者及 SEO 时间信息均由程序自动生成,编辑人员只需填写标题、分类和正文。
##
两条部署流水线
##
内容发布与代码部署
### 用户提交文章
...
...
@@ -106,17 +106,24 @@ CMS_DATA_DIR=/root/sumeiqiao/shared/cms-data
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
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/
```
text
/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 Shell Runner 与生产服务位于同一台服务器,并使用以下路径:
-
部署根目录:
`/root/sumeiqiao`
-
唯一运行目录:
`/root/sumeiqiao/current`
(实体目录)
-
持久化配置:
`/root/sumeiqiao/shared/.env`
-
PM2 应用名:
`sumeiqiao`
-
服务端口:
`8789`
...
...
@@ -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 项目。
服务器需提前创建
`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
CMS_DATA_DIR=/root/sumeiqiao/shared/cms-data
...
...
@@ -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`
。
多个 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`
,它会自行取得同一把锁。这样内容发布和代码部署
不会同时读取或覆盖文章数据,也不会同时替换静态网站。
scripts/cleanup-releases.sh
deleted
100644 → 0
View file @
658c58a4
#!/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
scripts/deploy-
release
.sh
→
scripts/deploy-
current
.sh
100644 → 100755
View file @
f8a89584
...
...
@@ -2,21 +2,33 @@
set
-Eeuo
pipefail
deploy_root
=
${
1
:?缺少部署根目录
}
release_dir
=
${
2
:?缺少
release 目录
}
app_name
=
${
3
:?缺少
PM2 应用名
}
pm2_bin
=
${
4
:?缺少
PM2 可执行文件路径
}
pm2_home
=
${
5
:?缺少
PM2_HOME
}
pm2_use_sudo
=
${
6
:-
1
}
healthcheck_url
=
${
7
:-
http
://127.0.0.1:
${
CMS_PORT
:-
8789
}
/
}
pipeline_id
=
${
8
:?缺少
CI pipeline ID
}
source_dir
=
${
2
:?缺少
Git 工作目录
}
commit_sha
=
${
3
:?缺少
Git commit SHA
}
app_name
=
${
4
:?缺少
PM2 应用名
}
pm2_bin
=
${
5
:?缺少
PM2 可执行文件路径
}
pm2_home
=
${
6
:?缺少
PM2_HOME
}
pm2_use_sudo
=
${
7
:-
1
}
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
/root/?
*
)
;;
*
)
echo
"部署根目录必须是 /root 下的项目目录"
>
&2
;
exit
2
;;
esac
case
"
$source_dir
"
in
/
*
)
;;
*
)
echo
"
部署根
目录必须是绝对路径"
>
&2
;
exit
2
;;
*
)
echo
"
Git 工作
目录必须是绝对路径"
>
&2
;
exit
2
;;
esac
case
"
$release_dir
"
in
"
$deploy_root
"
/releases/
*
)
;;
*
)
echo
"release 目录必须位于
$deploy_root
/releases/ 下"
>
&2
;
exit
2
;;
case
"
$commit_sha
"
in
*
[!
0-9a-f]
*
|
''
)
echo
"Git commit SHA 不合法"
>
&2
;
exit
2
;;
esac
case
"
$app_name
"
in
*
[!
A-Za-z0-9_.-]
*
|
''
)
echo
"PM2 应用名包含不安全字符"
>
&2
;
exit
2
;;
...
...
@@ -33,20 +45,17 @@ case "$pm2_use_sudo" in
0|1
)
;;
*
)
echo
"PM2_USE_SUDO 只能是 0 或 1"
>
&2
;
exit
2
;;
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
echo
"部署必须通过 run-with-site-lock.mjs 执行"
>
&2
exit
2
fi
if
[
!
-d
"
$release_dir
"
]
||
[
!
-f
"
$release_dir
/package-lock.json"
]
;
then
echo
"release 目录不完整:
$release_dir
"
>
&2
if
[
-L
"
$deploy_root
"
]
||
[
!
-d
"
$deploy_root
"
]
;
then
echo
"部署根目录必须是已存在的实体目录:
$deploy_root
"
>
&2
exit
2
fi
if
[
-L
"
$shared_dir
"
]
||
[
!
-d
"
$shared_dir
"
]
;
then
echo
"shared 必须是已存在的实体目录:
$shared_dir
"
>
&2
exit
2
fi
if
[
!
-f
"
$shared_env
"
]
;
then
...
...
@@ -61,16 +70,28 @@ case "${CMS_BUILD_LOCK:-}" in
"
$shared_dir
"
/
*
)
;;
*
)
echo
"CMS_BUILD_LOCK 必须位于
$shared_dir
/ 下"
>
&2
;
exit
2
;;
esac
if
[
-e
"
$current_link
"
]
&&
[
!
-L
"
$current_link
"
]
;
then
echo
"
$current_link
必须是软链接,拒绝覆盖真实目录
"
>
&2
if
[
"
$(
git
-C
"
$source_dir
"
rev-parse
--is-inside-work-tree
2>/dev/null
||
true
)
"
!=
"true
"
]
;
then
echo
"
Git 工作目录不完整:
$source_dir
"
>
&2
exit
2
fi
command
-v
curl
>
/dev/null
test
-x
"
$pm2_bin
"
if
[
!
-f
"
$release_dir
/.pipeline-id"
]
||
[
"
$(
cat
"
$release_dir
/.pipeline-id"
)
"
!=
"
$pipeline_id
"
]
;
then
echo
"release 的 pipeline 标识不匹配"
>
&2
if
!
git
-C
"
$source_dir
"
cat-file
-e
"
$commit_sha
^{commit}"
;
then
echo
"Git 提交不存在:
$commit_sha
"
>
&2
exit
2
fi
if
git
-C
"
$source_dir
"
cat-file
-e
"
$commit_sha
:.env"
2>/dev/null
;
then
echo
"提交中不得包含生产 .env;请只使用
$shared_env
"
>
&2
exit
2
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
()
{
if
[
"
$pm2_use_sudo
"
=
"1"
]
;
then
...
...
@@ -80,70 +101,45 @@ pm2_command() {
fi
}
# 只删除并重建 APP_NAME 对应的一个进程;不执行 pm2 kill、delete all、restart all 或 pm2 save。
start_current_app
()
{
if
pm2_command describe
"
$app_name
"
>
/dev/null 2>&1
;
then
pm2_command delete
"
$app_name
"
fi
pm2_command start
"
$current_link
/server.mjs"
\
--name
"
$app_name
"
\
--cwd
"
$current_link
"
\
--node-args
=
"--env-file=
$shared_env
"
}
echo
"[deploy] 停止本项目 PM2 进程:
$app_name
"
if
pm2_command describe
"
$app_name
"
>
/dev/null 2>&1
;
then
pm2_command delete
"
$app_name
"
fi
previous_target
=
""
if
[
-L
"
$current_link
"
]
;
then
previous_target
=
$(
readlink
-f
"
$current_link
"
)
if
[
-L
"
$current_dir
"
]
;
then
old_target
=
$(
readlink
"
$current_dir
"
)
echo
"[deploy] 首次迁移:仅删除 current 软链接(原目标保持不变):
$old_target
"
unlink
"
$current_dir
"
elif
[
-e
"
$current_dir
"
]
&&
[
!
-d
"
$current_dir
"
]
;
then
echo
"current 必须是目录或首次迁移用的软链接:
$current_dir
"
>
&2
exit
2
fi
# GitLab 11.7 没有 resource_group。若较新的 pipeline 已上线,拒绝旧 pipeline 覆盖它。
if
[
-n
"
$previous_target
"
]
&&
[
-f
"
$previous_target
/.pipeline-id"
]
;
then
current_pipeline_id
=
$(
cat
"
$previous_target
/.pipeline-id"
)
case
"
$current_pipeline_id
"
in
*
[!
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
mkdir
-p
"
$current_dir
"
if
[
-L
"
$current_dir
"
]
||
[
"
$(
readlink
-f
"
$current_dir
"
)
"
!=
"
$current_dir
"
]
;
then
echo
"current 必须是部署根目录下的实体目录:
$current_dir
"
>
&2
exit
2
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] 使用共享用户内容执行最终构建"
npm run build:inside-lock
echo
"[deploy] 清空 current 实体目录"
find
"
$current_dir
"
-mindepth
1
-maxdepth
1
-exec
rm
-rf
--
{}
+
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-
$$
"
ln
-s
"
$release_dir
"
"
$next_link
"
mv
-Tf
"
$next_link
"
"
$current_link
"
switched
=
1
cd
"
$current_dir
"
echo
"[deploy] 安装依赖"
npm ci
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
for
attempt
in
$(
seq
1 30
)
;
do
...
...
@@ -151,19 +147,20 @@ for attempt in $(seq 1 30); do
healthy
=
1
break
fi
echo
"等待服务启动... (
$attempt
/30)"
echo
"
[deploy]
等待服务启动... (
$attempt
/30)"
sleep
1
done
if
[
"
$healthy
"
!=
"1"
]
;
then
echo
"健康检查失败:
$healthcheck_url
"
>
&2
pm2_command logs
"
$app_name
"
--lines
30
--nostream
||
true
false
exit
1
fi
trap
- ERR
previous_marker_tmp
=
"
$shared_dir
/.previous-release-
$$
"
printf
'%s\n'
"
$previous_target
"
>
"
$previous_marker_tmp
"
mv
-f
"
$previous_marker_tmp
"
"
$shared_dir
/previous-release"
printf
'%s\n'
"
$pipeline_id
"
>
"
$release_dir
/.deploy-success"
echo
"部署成功:
$release_dir
"
echo
"[deploy] 更新并验证 Nginx"
test
-f
"
$nginx_source
"
sudo
-n
/usr/bin/install
-o
root
-g
root
-m
0644
"
$nginx_source
"
"
$nginx_target
"
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
"[deploy] 部署成功:
$current_dir
(
$commit_sha
)"
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment