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
8a0972f4
Commit
8a0972f4
authored
Jul 23, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
ci: make release deployment idempotent
parent
0f249b2a
Pipeline
#410
failed with stages
in 4 seconds
Changes
2
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
85 additions
and
59 deletions
+85
-59
.gitlab-ci.yml
.gitlab-ci.yml
+83
-57
CMS_README.md
CMS_README.md
+2
-2
No files found.
.gitlab-ci.yml
View file @
8a0972f4
# 塑美俏官网 · GitLab CI/CD 安全部署
# 流程:
准备 release → 安装依赖 → 安全
部署 → 配置 Nginx → 验证与清理
# 流程:
幂等
部署 → 配置 Nginx → 验证与清理
# 触发:push 到 main 分支
#
# ⚠️ GitLab 版本:11.7(较旧)
...
...
@@ -25,58 +25,15 @@ variables:
PUBLIC_HOST
:
"
101.126.10.129"
stages
:
-
prepare
-
install
-
deploy
-
nginx
-
verify
#
所有 job 使用同一目录。CI_PIPELINE_ID 保证不同 pipeline 不会写同一个 release。
#
不使用 CI_JOB_ID,因为同一 pipeline 的不同 job 必须共享 release
。
#
CI_PIPELINE_ID 保证不同 pipeline 不会写同一个 release。同一 deploy job 被重试时,
#
会重新导出相同提交并执行 npm ci;若该 pipeline 已上线,则直接成功退出
。
# ── 1. 准备独立 release ───────────────────────────────
prepare
:
stage
:
prepare
tags
:
-
sumeiqiao-prod
only
:
-
main
script
:
-
export RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
-
echo "[prepare] 提交:$CI_COMMIT_SHA"
-
echo "[prepare] Release:$RELEASE_DIR"
-
git config --global --add safe.directory "$CI_PROJECT_DIR"
-
test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" && exit 1)
-
test -n "$CI_PIPELINE_ID" || (echo "CI_PIPELINE_ID 缺失" && exit 1)
-
test -f "$SHARED_ENV" || (echo "缺少生产配置:$SHARED_ENV" && exit 1)
-
mkdir -p "$RELEASES_DIR" "$SHARED_DIR" "$RELEASE_DIR"
# GitLab Runner 已检出本次提交;直接导出受 Git 管理的文件,不修改正在运行的 current。
-
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"
-
test -L "$RELEASE_DIR/.env" || ln -s "$SHARED_ENV" "$RELEASE_DIR/.env"
-
echo "[prepare] Release 准备完成"
-
ls -la "$RELEASE_DIR" | sed -n '1,30p'
# ── 2. 安装依赖 ──────────────────────────────────────
# release 尚未启用,安装失败不会影响线上服务,也不需要占用 CMS 构建锁。
install
:
stage
:
install
tags
:
-
sumeiqiao-prod
only
:
-
main
script
:
-
export RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
-
echo "[install] 开始安装依赖:$RELEASE_DIR"
-
cd "$RELEASE_DIR"
-
npm ci
-
echo "[install] 依赖安装完成"
-
du -sh node_modules
# ── 3. 安全部署 ──────────────────────────────────────
# 共享锁内重新读取最新文章并构建,然后原子切换 current、仅重启本项目 PM2、健康检查。
# 常规失败会在锁内自动恢复上一成功版本。
# ── 1. 幂等部署 ──────────────────────────────────────
# 创建 release、安装依赖和部署在同一个 job 中完成,不依赖跨 job 保留 node_modules。
deploy
:
stage
:
deploy
tags
:
...
...
@@ -84,15 +41,79 @@ deploy:
only
:
-
main
script
:
-
export RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
-
echo "[deploy] 等待 CMS 共享构建锁"
-
echo "[deploy] 目标 Release:$RELEASE_DIR"
-
test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行:$PM2_BIN" && exit 1)
-
cd "$RELEASE_DIR"
-
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")"
-
|
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
# ── 4. 安装并验证 Nginx 配置 ─────────────────────────
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 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
...
...
@@ -109,7 +130,7 @@ configure_nginx:
-
'
curl
--fail
--silent
--show-error
--max-time
5
--header
"Host:
$PUBLIC_HOST"
http://127.0.0.1/
>/dev/null'
-
echo "[nginx] Nginx 配置与公网入口验证完成"
# ──
5
. 验证并只保留当前 + 上一个成功版本 ─────────────
# ──
3
. 验证并只保留当前 + 上一个成功版本 ─────────────
# when: always 用于输出最终状态;生产健康检查和自动回退已经在 deploy 的共享锁内完成。
verify
:
stage
:
verify
...
...
@@ -136,5 +157,10 @@ verify:
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"
CMS_README.md
View file @
8a0972f4
...
...
@@ -106,7 +106,7 @@ 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;
最终生产构建、切换 current 软链接、重启 PM2 和健康检查在同一把共享锁内完成。应用健康检查通过后才复制 Nginx 配置、检查语法并 reload。健康检查失败时会自动切回上一版本。
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/
...
...
@@ -154,6 +154,6 @@ gitlab-runner ALL=(root) NOPASSWD: SUMEIQIAO_NGINX
多个 GitLab pipeline 即使同时进入部署阶段,也会由服务器共享锁依次执行;部署持锁时,文章后台不会排队受理发布操作,而会明确提示编辑人员稍后重试。
验证阶段
只保留当前版本和上一个成功版本。清理脚本不会删除其他 pipeline 尚未完成的 release;本 pipeline 失败产生的目录会在自己的验证阶段清理
。若 Runner 被强制终止导致验证阶段未执行,可能留下一个未完成目录,但不会影响线上版本,可在确认没有对应 pipeline 运行后手动删除。
验证阶段
会输出 PM2 当前运行的全部项目,并只保留当前版本和上一个成功版本。清理脚本不会删除其他 pipeline 尚未完成的 release;本 pipeline 失败产生的目录会在自己的验证阶段清理。即使失败目录已被清理,重试 deploy job 也会重新创建 release 和依赖
。若 Runner 被强制终止导致验证阶段未执行,可能留下一个未完成目录,但不会影响线上版本,可在确认没有对应 pipeline 运行后手动删除。
普通本地构建或不包含 release 切换的 CI 可以直接运行
`npm run build`
,它会自行取得同一把锁。这样两条流水线不会同时读取或覆盖文章数据,也不会同时替换静态网站。
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