Commit ec14e5cc authored by xuchentao's avatar xuchentao

ci: simplify deployment to single current directory

parent 4a19cdd8
Pipeline #429 passed with stage
in 16 seconds
# 嘉运网约车官网 · GitLab CI/CD 安全部署 # 嘉运网约车官网 · GitLab CI/CD 单目录部署
# 流程:幂等部署 → 配置 Nginx → 验证与清理 # push 到 main 后,将当前提交部署到 /root/jiayun/current。
# 触发:push 到 main 分支
#
# ⚠️ GitLab 版本:11.7(较旧)
# - 不使用 workflow:rules
# - 不使用 rules
# - 不使用 needs/DAG
# - 不使用 resource_group
# - 不使用 cache:policy
# 仅使用 only、stages、script、when 等基础语法。
# Runner 注册时请使用标签:jiayun-prod
variables: variables:
DEPLOY_ROOT: "/root/jiayun" DEPLOY_ROOT: "/root/jiayun"
RELEASES_DIR: "/root/jiayun/releases"
SHARED_DIR: "/root/jiayun/shared"
SHARED_ENV: "/root/jiayun/shared/.env" SHARED_ENV: "/root/jiayun/shared/.env"
APP_NAME: "jiayun" APP_NAME: "jiayun"
PORT: "8790"
HEALTHCHECK_URL: "http://127.0.0.1:8790/" HEALTHCHECK_URL: "http://127.0.0.1:8790/"
PM2_BIN: "/usr/bin/pm2" PM2_BIN: "/usr/bin/pm2"
PM2_HOME_DIR: "/root/.pm2" PM2_HOME_DIR: "/root/.pm2"
...@@ -26,14 +13,7 @@ variables: ...@@ -26,14 +13,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 +23,35 @@ deploy: ...@@ -43,124 +23,35 @@ 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 -f "$SHARED_ENV" || (echo "缺少生产配置:$SHARED_ENV" >&2 && exit 1)
test -n "$CI_PIPELINE_ID" || (echo "CI_PIPELINE_ID 缺失" && exit 1) test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行:$PM2_BIN" >&2 && 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 "$HEALTHCHECK_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"
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 共享构建锁" 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" "$HEALTHCHECK_URL" "$CI_PIPELINE_ID" node --env-file="$SHARED_ENV" scripts/run-with-site-lock.mjs -- \
echo "[deploy] 当前版本:$(readlink -f "$DEPLOY_ROOT/current")" bash scripts/deploy-current.sh \
"$DEPLOY_ROOT" \
# ── 2. 安装并验证 Nginx 配置 ───────────────────────── "$CI_PROJECT_DIR" \
# 复制 current release 中的配置,检查语法后重新加载 Nginx。 "$CI_COMMIT_SHA" \
configure_nginx: "$APP_NAME" \
stage: nginx "$PM2_BIN" \
tags: "$PM2_HOME_DIR" \
- jiayun-prod "$PM2_USE_SUDO" \
only: "$HEALTHCHECK_URL"
- main
script: echo "[nginx] 更新配置"
- echo "[nginx] 开始配置公网入口:$PUBLIC_HOST" sudo -n /usr/bin/install -o root -g root -m 0644 \
- test -f "$DEPLOY_ROOT/current/deploy/nginx/jiayun.conf" || (echo "缺少 Nginx 配置" && exit 2) "$DEPLOY_ROOT/current/deploy/nginx/jiayun.conf" \
- sudo -n /usr/bin/install -o root -g root -m 0644 "$DEPLOY_ROOT/current/deploy/nginx/jiayun.conf" /etc/nginx/conf.d/jiayun.conf /etc/nginx/conf.d/jiayun.conf
- sudo -n /usr/sbin/nginx -t sudo -n /usr/sbin/nginx -t
- sudo -n /usr/bin/systemctl reload nginx 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 配置与公网入口验证完成" curl --fail --silent --show-error --max-time 5 \
--header "Host: $PUBLIC_HOST" \
# ── 3. 验证并只保留当前 + 上一个成功版本 ───────────── http://127.0.0.1/ >/dev/null
# when: always 用于输出最终状态;生产健康检查和自动回退已经在 deploy 的共享锁内完成。
verify: echo "[deploy] 部署与公网入口验证完成"
stage: verify
tags:
- jiayun-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 "$HEALTHCHECK_URL" >/dev/null; then
echo "[verify] 服务运行正常:$HEALTHCHECK_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"
...@@ -32,7 +32,7 @@ npm start ...@@ -32,7 +32,7 @@ npm start
- 分类:`src/content/categories.json` - 分类:`src/content/categories.json`
- 上传图片:`public/uploads/` - 上传图片:`public/uploads/`
生产环境建议设置 `CMS_DATA_DIR` 指向 release 目录之外的持久化路径。后台支持资讯新建、保存草稿、预览、发布、下架、删除、图片上传和分类管理。发布操作会在共享锁内原子构建站点,失败时恢复内容快照。 生产环境建议设置 `CMS_DATA_DIR` 指向部署目录之外的持久化路径。后台支持资讯新建、保存草稿、预览、发布、下架、删除、图片上传和分类管理。发布操作会在共享锁内原子构建站点,失败时恢复内容快照。
## 必需环境变量 ## 必需环境变量
...@@ -50,4 +50,4 @@ CMS_BUILD_LOCK=/root/jiayun/shared/site-build.lock ...@@ -50,4 +50,4 @@ CMS_BUILD_LOCK=/root/jiayun/shared/site-build.lock
项目包含 canonical、Open Graph、Organization/LocalBusiness/Service/FAQ/Article 结构化数据、sitemap、robots.txt、llms.txt 与 web manifest。正式域名配置在 `astro.config.mjs` 中;修改时需同时更新 `public/robots.txt` 中的 Sitemap。 项目包含 canonical、Open Graph、Organization/LocalBusiness/Service/FAQ/Article 结构化数据、sitemap、robots.txt、llms.txt 与 web manifest。正式域名配置在 `astro.config.mjs` 中;修改时需同时更新 `public/robots.txt` 中的 Sitemap。
`scripts/` 保留了参考项目的共享构建锁、原子替换、release 部署、健康检查与自动回退机制;`deploy/nginx/jiayun.conf` 是反向代理示例。GitLab CI 使用 release 目录部署,需按真实服务器调整 Runner 标签、域名/IP 与目录 GitLab CI 将唯一的当前版本部署到 `/root/jiayun/current`,用户内容与生产环境配置保存在 `/root/jiayun/shared`。部署流程为替换当前代码、安装依赖、构建、重启与健康检查,不创建或清理历史发布目录,也不提供自动回退。`deploy/nginx/jiayun.conf` 是反向代理配置
#!/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,24 @@ ...@@ -2,21 +2,24 @@
set -Eeuo pipefail set -Eeuo pipefail
deploy_root=${1:?缺少部署根目录} deploy_root=${1:?缺少部署根目录}
release_dir=${2:?缺少 release 目录} source_dir=${2:?缺少源码目录}
app_name=${3:?缺少 PM2 应用名} commit_sha=${3:?缺少提交 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:-8790}/} pm2_use_sudo=${7:-1}
pipeline_id=${8:?缺少 CI pipeline ID} healthcheck_url=${8:-http://127.0.0.1:${CMS_PORT:-8790}/}
case "$deploy_root" in case "$deploy_root" in
/*) ;; /*) ;;
*) echo "部署根目录必须是绝对路径" >&2; exit 2 ;; *) echo "部署根目录必须是绝对路径" >&2; exit 2 ;;
esac esac
case "$release_dir" in case "$source_dir" in
"$deploy_root"/releases/*) ;; /*) ;;
*) echo "release 目录必须位于 $deploy_root/releases/ 下" >&2; exit 2 ;; *) echo "源码目录必须是绝对路径" >&2; exit 2 ;;
esac
case "$commit_sha" in
*[!0-9A-Fa-f]*|'') echo "提交 SHA 不合法" >&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 +36,21 @@ case "$pm2_use_sudo" in ...@@ -33,20 +36,21 @@ 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_dir="$deploy_root/shared"
shared_env="$shared_dir/.env" shared_env="$shared_dir/.env"
current_link="$deploy_root/current" current_dir="$deploy_root/current"
if [ "${JIAYUN_SITE_LOCK_HELD:-}" != "1" ]; then if [ "${JIAYUN_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 [ ! -d "$source_dir/.git" ] || [ ! -f "$source_dir/package-lock.json" ]; then
echo "release 目录不完整:$release_dir" >&2 echo "源码目录不完整:$source_dir" >&2
exit 2
fi
if ! git -C "$source_dir" cat-file -e "$commit_sha^{commit}"; then
echo "源码目录中不存在提交:$commit_sha" >&2
exit 2 exit 2
fi fi
if [ ! -f "$shared_env" ]; then if [ ! -f "$shared_env" ]; then
...@@ -61,16 +65,8 @@ case "${CMS_BUILD_LOCK:-}" in ...@@ -61,16 +65,8 @@ 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
echo "$current_link 必须是软链接,拒绝覆盖真实目录" >&2
exit 2
fi
command -v curl >/dev/null command -v curl >/dev/null
test -x "$pm2_bin" test -x "$pm2_bin"
if [ ! -f "$release_dir/.pipeline-id" ] || [ "$(cat "$release_dir/.pipeline-id")" != "$pipeline_id" ]; then
echo "release 的 pipeline 标识不匹配" >&2
exit 2
fi
pm2_command() { pm2_command() {
if [ "$pm2_use_sudo" = "1" ]; then if [ "$pm2_use_sudo" = "1" ]; then
...@@ -80,70 +76,38 @@ pm2_command() { ...@@ -80,70 +76,38 @@ pm2_command() {
fi fi
} }
# 只删除并重建 APP_NAME 对应的一个进程;不执行 pm2 kill、delete all、restart all 或 pm2 save。 echo "[deploy] 停止当前进程"
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
pm2_command start "$current_link/server.mjs" \
--name "$app_name" \
--cwd "$current_link" \
--node-args="--env-file=$shared_env"
}
previous_target=""
if [ -L "$current_link" ]; then
previous_target=$(readlink -f "$current_link")
fi fi
# GitLab 11.7 没有 resource_group。若较新的 pipeline 已上线,拒绝旧 pipeline 覆盖它。 # 旧部署的 current 是软链接;单目录部署改为同名实体目录。
if [ -n "$previous_target" ] && [ -f "$previous_target/.pipeline-id" ]; then if [ -L "$current_dir" ]; then
current_pipeline_id=$(cat "$previous_target/.pipeline-id") rm -- "$current_dir"
case "$current_pipeline_id" in elif [ -e "$current_dir" ] && [ ! -d "$current_dir" ]; then
*[!0-9]*|'') echo "当前版本的 pipeline 标识不合法" >&2; exit 2 ;; echo "$current_dir 必须是目录或旧版软链接" >&2
esac exit 2
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 mkdir -p "$current_dir"
rollback() { echo "[deploy] 替换当前版本文件"
exit_code=$? find "$current_dir" -depth -mindepth 1 -delete
trap - ERR git -C "$source_dir" archive "$commit_sha" | tar -x -C "$current_dir"
if [ "$switched" = "1" ]; then printf '%s\n' "$commit_sha" > "$current_dir/.commit-sha"
echo "新版本部署失败,正在恢复上一版本……" >&2 ln -s "$shared_env" "$current_dir/.env"
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" cd "$current_dir"
echo "[deploy] 使用共享用户内容执行最终构建" echo "[deploy] 安装依赖"
npm run build:inside-lock npm ci
next_link="$deploy_root/.current-next-$$" echo "[deploy] 构建站点"
ln -s "$release_dir" "$next_link" npm run build:inside-lock
mv -Tf "$next_link" "$current_link"
switched=1
start_current_app echo "[deploy] 启动项目"
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
...@@ -158,12 +122,7 @@ done ...@@ -158,12 +122,7 @@ 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 "部署成功:$current_dir"
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"
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