From fdd705c4b47b2e7150822e0d2b9c93939af501f7 Mon Sep 17 00:00:00 2001 From: opsx Date: Fri, 30 Oct 2026 17:48:20 +0000 Subject: [PATCH] =?UTF-8?q?build(release):=20release.sh=20=E5=8A=A0?= =?UTF-8?q?=E7=8B=AC=E5=8D=A0=E6=9E=84=E5=BB=BA=E9=94=81=20+=20push=20?= =?UTF-8?q?=E9=87=8D=E8=AF=95=EF=BC=8C=E6=9D=9C=E7=BB=9D=20latest=20?= =?UTF-8?q?=E8=A2=AB=E5=B9=B6=E5=8F=91=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit === 问题 === 本项目所有清单都指向 `latest`(k8s deployment / docker-compose / prod compose), 两个构建并发时**谁后完成谁生效**,另一个的推送等于白做。 本次实际踩到:一次被中断的构建后来完成,把 latest 推回了旧镜像 64fb231, 而当前正确版本是 a16978d2;若此时 Pod 重启就会跑回旧代码, 表现是"改了但没生效",从页面完全看不出来。 === 修复 1:flock 独占锁 === LOCK_FILE=${RELEASE_LOCK_FILE:-/var/lock/lokistack-release.lock} exec 9>"$LOCK_FILE"; flock -n 9 || { 中文提示 + exit 1 } - 锁跟随文件描述符,进程退出(含 Ctrl-C / set -e 中断)自动释放, 不需要手工清理僵尸锁文件 - 失败提示给出:锁路径、查看占用命令(fuser/lsof)、以及"无需手工删锁"的说明 - 支持 RELEASE_LOCK_FILE 换锁路径(多仓库并行发布时用) - 新增 --dry-run:只做前置检查(docker 是否可用、锁能否获取),不构建不推送, 用于验证脚本本身和排查"为什么发布失败" === 修复 2:push 重试 === Docker Hub 偶发 502,实测导致过"v0.1 推成功、latest 失败 → latest 指旧镜像"。 新增 push_with_retry():每个 tag 最多 3 次,间隔 10 秒,全失败才 exit 1。 三个 tag(latest / v0.1 / 不可变 tag)都走它。 === 实测 === 测试1 无人占用 : 🔒 获取锁成功,退出码 0 测试2 另一进程持有锁 : ❌ 明确拒绝并给出排查命令,退出码 1 测试3 占用进程退出后 : ✅ 自动恢复可用,退出码 0 bash -n 语法检查通过 --- scripts/release.sh | 65 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index bbe777b..f9f01e9 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -23,6 +23,14 @@ # bash scripts/release.sh # 构建 + 推三个 tag + 滚动重启(拉 latest) # bash scripts/release.sh --no-deploy # 只构建 + 推送,不碰集群 # bash scripts/release.sh --pin # 额外把 deployment.yaml 钉到不可变 tag(可选) +# bash scripts/release.sh --dry-run # 只检查锁与前置条件,不构建不推送 +# +# 并发保护(重要): +# 本项目所有环境都拉 latest,两个构建同时跑会互相覆盖 latest: +# 后完成的那次生效,先完成的那次白推。曾出现"中断的构建后来完成, +# 把 latest 指回旧代码",表现为"改了但没生效",极难排查。 +# 因此脚本用 flock 加独占锁,同一时刻只允许一个发布任务。 +# flock 在进程退出时自动释放,不会留下需要手工清理的僵尸锁。 # ============================================================ set -euo pipefail @@ -32,6 +40,43 @@ IMAGE_REPO="${IMAGE_REPO:-lghost/lokistack}" VERSION="${VERSION:-2.0.0}" MODE="${1:-}" +# ── 独占锁:同一时刻只允许一个发布任务 ──────────────────────────────── +# 为什么必须加:所有清单都指向 latest,两个构建并发时谁后完成谁生效, +# 另一个的推送等于白做;更糟的是完成顺序不可控,可能把 latest 指向更旧的 +# 代码。这是"改了但没生效"类问题的根源之一。 +# flock 跟随文件描述符,进程退出(含 Ctrl-C / set -e 中断)自动释放。 +LOCK_FILE="${RELEASE_LOCK_FILE:-/var/lock/lokistack-release.lock}" +mkdir -p "$(dirname "${LOCK_FILE}")" 2>/dev/null || true +if ! exec 9>"${LOCK_FILE}"; then + echo "❌ 无法创建锁文件 ${LOCK_FILE}(权限不足?)" + exit 1 +fi +if ! flock -n 9; then + echo "❌ 已有发布任务正在进行,本次不启动。" + echo " 锁文件 : ${LOCK_FILE}" + echo " 查看占用 : fuser -v ${LOCK_FILE} 2>/dev/null || lsof ${LOCK_FILE}" + echo " 无需手工删锁文件:持有进程退出时 flock 自动释放。" + exit 1 +fi +echo "🔒 已获取发布锁:${LOCK_FILE}" + +echo "==========================================" +echo " 前置检查" +echo "==========================================" +echo " 仓库 : ${IMAGE_REPO}" +if ! command -v docker >/dev/null 2>&1; then + echo "❌ 未找到 docker 命令" + exit 1 +fi +echo " docker : $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo '未知')" + +if [ "${MODE}" = "--dry-run" ]; then + echo "" + echo "(--dry-run) 只做前置检查:不构建、不推送、不动集群。" + echo " → 锁获取正常。" + exit 0 +fi + SHORT_SHA=$(git rev-parse --short=8 HEAD 2>/dev/null || echo "nogit") DIRTY="" if ! git diff --quiet 2>/dev/null; then @@ -53,9 +98,23 @@ echo "✅ 构建完成 ${IMAGE_ID}" echo "" echo "推送三个 tag(latest 与 v0.1 必须同 digest,否则环境间会版本漂移)..." -docker push "${IMAGE_REPO}:latest" | tail -1 -docker push "${IMAGE_REPO}:v0.1" | tail -1 -docker push "${IMAGE_REPO}:${IMMUTABLE_TAG}" | tail -1 +# Docker Hub 偶发 502(实测踩过:v0.1 推成功、latest 失败 → +# latest 指着旧镜像,Pod 重启后跑的是旧代码)。所以每次 push 都重试。 +push_with_retry() { + local tag="$1" attempt + for attempt in 1 2 3; do + if docker push "${IMAGE_REPO}:${tag}" 2>&1 | tail -1; then + return 0 + fi + echo " ⚠️ push ${tag} 第 ${attempt} 次失败,10 秒后重试..." + sleep 10 + done + echo "❌ push ${tag} 连续 3 次失败(镜像仓库异常?)" + return 1 +} +push_with_retry latest +push_with_retry v0.1 +push_with_retry "${IMMUTABLE_TAG}" # ── 校验三个 tag 指向同一个镜像 ── echo ""