#!/usr/bin/env bash # ============================================================ # release.sh — 构建 + 推送(latest / v0.1 / 不可变 tag 三合一) # ============================================================ # 本项目的发布策略:**清单与容器启动统一使用 latest**。 # kubernetes/deployment.yaml → lghost/lokistack:latest + imagePullPolicy: Always # docker-compose.yml → lghost/lokistack:latest # docker-compose.prod.yml → lghost/lokistack:${IMAGE_TAG:-latest} # # 所以每次发版必须让 latest 与 v0.1 **同时**指向新镜像,否则会出现 # "k8s 拉到新版、docker 起还是旧版"这种版本漂移。 # # 本脚本另外额外打一个不可变 tag(<版本>-<日期>-<短commit>): # - 它不写进任何清单,只用于事后追溯/回滚(仓库里能查到那一刻的确切镜像) # - 需要"固定版本部署"的生产环境可以手动改成这个 tag # # 为什么不在脚本里改 yml 的 tag: # 上一版脚本会把 deployment.yaml 的 image 改成不可变 tag 并把 pullPolicy # 改成 IfNotPresent,结果与"统一用 latest"的约定相反,还容易忘记提交 # 导致下次 apply 退回旧 tag。改成默认不动清单。 # # 用法: # bash scripts/release.sh # 构建 + 推三个 tag + 滚动重启(拉 latest) # bash scripts/release.sh --no-deploy # 只构建 + 推送,不碰集群 # bash scripts/release.sh --pin # 额外把 deployment.yaml 钉到不可变 tag(可选) # ============================================================ set -euo pipefail cd "$(dirname "$0")/.." IMAGE_REPO="${IMAGE_REPO:-lghost/lokistack}" VERSION="${VERSION:-2.0.0}" MODE="${1:-}" SHORT_SHA=$(git rev-parse --short=8 HEAD 2>/dev/null || echo "nogit") DIRTY="" if ! git diff --quiet 2>/dev/null; then DIRTY="-dirty" echo "⚠️ 工作区有未提交改动,不可变 tag 会带 -dirty(发布前建议先提交)" fi IMMUTABLE_TAG="${VERSION}-$(date +%Y%m%d)-${SHORT_SHA}${DIRTY}" echo "==========================================" echo " 构建: ${IMAGE_REPO} → latest / v0.1 / ${IMMUTABLE_TAG}" echo "==========================================" docker build \ -t "${IMAGE_REPO}:latest" \ -t "${IMAGE_REPO}:v0.1" \ -t "${IMAGE_REPO}:${IMMUTABLE_TAG}" . IMAGE_ID=$(docker images --format '{{.ID}}' "${IMAGE_REPO}:latest" | head -1) 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 # ── 校验三个 tag 指向同一个镜像 ── echo "" echo "校验远端 digest 一致性..." digest_of() { curl -s "https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/$1" 2>/dev/null \ | python3 -c 'import sys,json;print(json.load(sys.stdin).get("digest","?"))' 2>/dev/null \ || echo "?" } D_LATEST=$(digest_of latest) D_V01=$(digest_of v0.1) D_IMM=$(digest_of "${IMMUTABLE_TAG}") echo " latest : ${D_LATEST:0:19}" echo " v0.1 : ${D_V01:0:19}" echo " ${IMMUTABLE_TAG}: ${D_IMM:0:19}" if [ "${D_LATEST:0:19}" != "${D_V01:0:19}" ] || [ "${D_LATEST:0:19}" != "${D_IMM:0:19}" ]; then echo "❌ 三个 tag digest 不一致,先别继续部署(可能是并发构建覆盖了 latest)" exit 1 fi echo " → 一致 ✅" if [ "${MODE}" = "--pin" ]; then echo "" echo "(--pin)把 kubernetes/deployment.yaml 钉到 ${IMMUTABLE_TAG}..." sed -i "s|image: ${IMAGE_REPO}:.*|image: ${IMAGE_REPO}:${IMMUTABLE_TAG}|" kubernetes/deployment.yaml sed -i "s|imagePullPolicy: Always|imagePullPolicy: IfNotPresent|" kubernetes/deployment.yaml echo "⚠️ 记得提交 deployment.yaml,否则下次 apply 会退回 latest" fi if [ "${MODE}" != "--no-deploy" ]; then export KUBECONFIG="${KUBECONFIG:-/root/.kube/config}" echo "" echo "应用清单(image: latest + Always → 强制重拉)..." kubectl apply -f kubernetes/deployment.yaml # 清单内容没变时 apply 不会触发滚动,这里显式重启一次 kubectl -n log rollout restart deploy/lokistack kubectl -n log rollout status deploy/lokistack --timeout=300s POD_DIGEST=$(kubectl -n log get po -l app=lokistack \ -o jsonpath='{.items[0].status.containerStatuses[0].imageID}' 2>/dev/null | sed 's/.*@//') echo "" echo "==========================================" echo " 部署完成,核对 Pod 与远端 latest:" echo " 远端 latest : ${D_LATEST:0:19}" echo " Pod 运行中 : ${POD_DIGEST:0:19}" if [ "${POD_DIGEST:0:19}" = "${D_LATEST:0:19}" ]; then echo " → 一致 ✅" else echo " → 不一致 ❌(Pod 可能还在拉取,或节点镜像缓存异常)" fi echo "==========================================" fi echo "" echo "回滚方法(latest 策略下靠不可变 tag 追溯):" echo " docker pull ${IMAGE_REPO}:<上次的 ${VERSION}-日期-commit>" echo " docker tag ${IMAGE_REPO}:<该 tag> ${IMAGE_REPO}:latest && docker push ${IMAGE_REPO}:latest" echo " kubectl -n log rollout restart deploy/lokistack"