Ctrl K
ComunidadGuías…

release.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# =============================================================================
# nowen 发布脚本(纯交互式 · 仅 Docker:amd64 / arm64 / multi)
#
# 直接运行:  ./release.sh
# 不需要任何命令行参数,所有选项通过菜单交互选择。
#
# 流程:
#   1. 选模式:发布模式 / 仅本地构建
#   2. 选架构:amd64 / arm64 / multi
#   3. [发布模式] 自动聚合本地+GitHub+DockerHub 历史版本,建议下一版本号
#   4. [构建模式] 选输出方式:load 到本机 / 导出 tar / push 到自定义 registry
#   5. 显示摘要 → 确认 → 开始构建/推送
# =============================================================================

set -euo pipefail

# -------------------- 配置 --------------------
DEFAULT_IMAGE_NAME="cropflre/nowen"
DEFAULT_BRANCH="main"
GITHUB_REPO_URL="https://github.com/cropflre/NOWEN"
GITHUB_REPO_SLUG="cropflre/NOWEN"
BUILDX_BUILDER="nowen-builder"
DEFAULT_TAR_OUT="nowen-arm64.tar"

# -------------------- 彩色输出 --------------------
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
    C_RED="$(tput setaf 1)"
    C_GREEN="$(tput setaf 2)"
    C_YELLOW="$(tput setaf 3)"
    C_BLUE="$(tput setaf 4)"
    C_CYAN="$(tput setaf 6)"
    C_BOLD="$(tput bold)"
    C_RESET="$(tput sgr0)"
else
    C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_CYAN=""; C_BOLD=""; C_RESET=""
fi

info()  { echo "${C_BLUE}[*]${C_RESET} $*"; }
ok()    { echo "${C_GREEN}[✓]${C_RESET} $*"; }
warn()  { echo "${C_YELLOW}[!]${C_RESET} $*" >&2; }
die()   { echo "${C_RED}[✗]${C_RESET} $*" >&2; exit 1; }
step()  { echo; echo "${C_BOLD}${C_CYAN}==== $* ====${C_RESET}"; }

# -------------------- 交互辅助 --------------------
# 必须接到 TTY,否则交互无意义
if [ ! -t 0 ]; then
    die "本脚本为交互式,请在终端中直接运行(不要用管道或 < 重定向输入)"
fi

# 选项菜单:ask_choice "提示语" 默认序号 选项1 选项2 ...
# 返回选中的序号到全局变量 CHOICE_IDX(从 1 开始)
ask_choice() {
    local title="$1"; shift
    local default_idx="$1"; shift
    local opts=( "$@" )
    local n=${#opts[@]}
    local i
    echo
    echo "${C_BOLD}${title}${C_RESET}"
    for ((i=0; i<n; i++)); do
        printf "  %d) %s\n" "$((i+1))" "${opts[i]}"
    done
    while :; do
        read -r -p "请输入 [1-${n}](默认 ${default_idx}): " _c
        _c="${_c:-$default_idx}"
        if echo "$_c" | grep -Eq '^[0-9]+$' && [ "$_c" -ge 1 ] && [ "$_c" -le "$n" ]; then
            CHOICE_IDX="$_c"
            return
        fi
        warn "无效输入:$_c"
    done
}

# 是否确认:ask_yes_no "提示" 默认(y/n)
# 返回值:0=yes 1=no
ask_yes_no() {
    local prompt="$1" def="${2:-n}" hint
    if [ "$def" = "y" ]; then hint="[Y/n]"; else hint="[y/N]"; fi
    local ans
    read -r -p "${prompt} ${hint} " ans
    ans="${ans:-$def}"
    case "$ans" in
        [yY]|[yY][eE][sS]) return 0 ;;
        *) return 1 ;;
    esac
}

# 输入字符串:ask_input "提示" 默认值
# 结果赋给全局变量 INPUT_VAL
ask_input() {
    local prompt="$1" def="${2:-}" ans
    if [ -n "$def" ]; then
        read -r -p "${prompt}(默认 ${def}): " ans
        INPUT_VAL="${ans:-$def}"
    else
        read -r -p "${prompt}: " ans
        INPUT_VAL="$ans"
    fi
}

run() {
    eval "$@"
}
run_argv() {
    "$@"
}

# -------------------- 前置检查 --------------------
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/Dockerfile" ]; then
    REPO_ROOT="$SCRIPT_DIR"
else
    REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
fi
cd "$REPO_ROOT"

clear 2>/dev/null || true
echo "${C_BOLD}${C_CYAN}"
echo "============================================================"
echo "         NOWEN  Docker 发布脚本(交互式)"
echo "============================================================"
echo "${C_RESET}"
info "工作目录:$REPO_ROOT"

git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "当前目录不是 git 仓库"
command -v docker >/dev/null 2>&1 || die "未安装 docker"
docker info >/dev/null 2>&1 || die "docker daemon 不可用(请启动 docker)"
[ -f "$REPO_ROOT/Dockerfile" ] || die "仓库根目录未找到 Dockerfile"

# -------------------- 第一步:选运行模式 --------------------
ask_choice "请选择运行模式:" 1 \
    "发布模式  (打 tag → push 到 Docker Hub → 同步 git tag 到 GitHub)" \
    "构建模式  (仅本地构建,不动 git,不推 Docker Hub)"
case "$CHOICE_IDX" in
    1) BUILD_ONLY=0 ;;
    2) BUILD_ONLY=1 ;;
esac

# -------------------- 第二步:选架构 --------------------
ask_choice "请选择构建架构:" 1 \
    "amd64   原生 docker build,最快(适合 x86 服务器/NAS)" \
    "arm64   buildx + QEMU 模拟(适合 ARM 板子 / 树莓派 / Apple Silicon)" \
    "multi   amd64 + arm64 一次出多架构 manifest(必须推送到 registry)"
case "$CHOICE_IDX" in
    1) ARCH="amd64" ;;
    2) ARCH="arm64" ;;
    3) ARCH="multi" ;;
esac

if [ "$ARCH" != "amd64" ]; then
    docker buildx version >/dev/null 2>&1 \
        || die "未检测到 docker buildx;arm64 / multi 模式必须使用 buildx"
fi

# -------------------- 默认值 --------------------
DO_PULL=1
DO_LATEST=1
DO_GIT_TAG=1
VERSION=""
CUSTOM_IMAGE=""
DO_TAR=0
TAR_OUT="$DEFAULT_TAR_OUT"
DO_PUSH_CUSTOM=0

# -------------------- 第三步(构建模式):选输出方式 --------------------
if [ "$BUILD_ONLY" = "1" ]; then
    if [ "$ARCH" = "multi" ]; then
        # multi 必须 push
        warn "multi 架构必须推送到 registry,自动启用 push 模式"
        DO_PUSH_CUSTOM=1
        ask_input "请输入目标镜像名(含 registry/tag,例如 registry.example.com/nowen:multi)" \
            "${DEFAULT_IMAGE_NAME}:multi"
        CUSTOM_IMAGE="$INPUT_VAL"
    else
        if [ "$ARCH" = "arm64" ]; then
            ask_choice "请选择构建产物输出方式:" 1 \
                "load   加载到本机 docker(默认,可以直接 docker run)" \
                "tar    导出为 .tar 文件,方便 scp 到 ARM 板子离线 docker load" \
                "push   推送到自定义 registry"
        else
            ask_choice "请选择构建产物输出方式:" 1 \
                "load   加载到本机 docker(默认,可以直接 docker run)" \
                "push   推送到自定义 registry"
        fi
        case "$ARCH-$CHOICE_IDX" in
            arm64-1) ;; # load
            arm64-2) DO_TAR=1 ;;
            arm64-3) DO_PUSH_CUSTOM=1 ;;
            amd64-1) ;; # load
            amd64-2) DO_PUSH_CUSTOM=1 ;;
        esac

        if [ "$DO_TAR" = "1" ]; then
            ask_input "tar 输出路径" "$DEFAULT_TAR_OUT"
            TAR_OUT="$INPUT_VAL"
            ask_input "镜像名(仅作为 tar 内 image tag)" "${DEFAULT_IMAGE_NAME}:${ARCH}"
            CUSTOM_IMAGE="$INPUT_VAL"
        elif [ "$DO_PUSH_CUSTOM" = "1" ]; then
            ask_input "目标镜像名(含 registry/tag,例如 registry.example.com/nowen:${ARCH})" \
                "${DEFAULT_IMAGE_NAME}:${ARCH}"
            CUSTOM_IMAGE="$INPUT_VAL"
        else
            ask_input "镜像名(加载到本机 docker 的 tag)" "${DEFAULT_IMAGE_NAME}:${ARCH}"
            CUSTOM_IMAGE="$INPUT_VAL"
        fi
    fi
fi

# -------------------- 第三步(发布模式):git 同步 + 版本号 --------------------
if [ "$BUILD_ONLY" != "1" ]; then
    CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
    info "当前分支:$CURRENT_BRANCH"
    if [ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]; then
        warn "当前不在 $DEFAULT_BRANCH 分支"
        ask_yes_no "确定继续?" "n" || die "已取消"
    fi

    # 工作区脏检查
    if ! git diff-index --quiet HEAD -- || ! git diff --cached --quiet; then
        warn "工作区有未提交的改动,将自动清理(git checkout -- . && git clean -fd)"
        git status --short | head -20
        if ask_yes_no "确认清理?" "y"; then
            git checkout -- .
            git clean -fd
            git reset HEAD -- . >/dev/null 2>&1 || true
            ok "工作区已自动恢复干净"
        else
            die "已取消"
        fi
    fi

    # ----- git pull 选择 -----
    ask_choice "是否执行 git pull 同步远端?" 1 \
        "执行 git pull(推荐)" \
        "跳过 git pull"
    [ "$CHOICE_IDX" = "2" ] && DO_PULL=0

    if [ "$DO_PULL" = "1" ]; then
        info "git fetch origin $CURRENT_BRANCH ..."
        git fetch origin "$CURRENT_BRANCH"

        _LR="$(git rev-list --left-right --count "HEAD...origin/$CURRENT_BRANCH" 2>/dev/null || echo "0	0")"
        _AHEAD="$(echo "$_LR" | awk '{print $1}')"
        _BEHIND="$(echo "$_LR" | awk '{print $2}')"
        info "本地相对 origin/$CURRENT_BRANCH:ahead=${_AHEAD}, behind=${_BEHIND}"

        if [ "$_AHEAD" = "0" ] && [ "$_BEHIND" = "0" ]; then
            ok "代码已是最新:$(git log -1 --pretty=format:'%h  %s')"
        elif [ "$_AHEAD" = "0" ] && [ "$_BEHIND" != "0" ]; then
            info "本地落后 ${_BEHIND} 个 commit,执行 fast-forward ..."
            git merge --ff-only "origin/$CURRENT_BRANCH"
            ok "已快进到:$(git log -1 --pretty=format:'%h  %s')"
        elif [ "$_AHEAD" != "0" ] && [ "$_BEHIND" = "0" ]; then
            warn "本地领先 ${_AHEAD} 个 commit,远端无新提交(这些 commit 会在发布时一并推送)"
        else
            warn "本地与远端 diverged:本地领先 ${_AHEAD},远端领先 ${_BEHIND}"
            ask_choice "请选择处理方式:" 1 \
                "rebase  把本地 commit 重放到远端最新(推荐)" \
                "merge   生成一个 merge commit" \
                "abort   终止"
            case "$CHOICE_IDX" in
                1)
                    if ! git rebase "origin/$CURRENT_BRANCH"; then
                        git rebase --abort >/dev/null 2>&1 || true
                        die "rebase 出现冲突,已自动 abort。请手动解决后重跑。"
                    fi ;;
                2)
                    if ! git merge --no-ff --no-edit "origin/$CURRENT_BRANCH"; then
                        git merge --abort >/dev/null 2>&1 || true
                        die "merge 出现冲突,已自动 abort。请手动解决后重跑。"
                    fi ;;
                3) die "用户选择 abort,已取消" ;;
            esac
            ok "同步完成:$(git log -1 --pretty=format:'%h  %s')"
        fi
    else
        info "已跳过 git pull"
    fi
fi

# -------------------- 版本号 / 镜像名确定 --------------------
GIT_COMMIT="$(git log -1 --pretty=format:'%h  %s')"
GIT_SHA="$(git rev-parse HEAD)"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"

if [ "$BUILD_ONLY" = "1" ]; then
    FULL_IMAGE="$CUSTOM_IMAGE"
    VERSION_TAG=""
    IMAGE_NAME=""
else
    IMAGE_NAME="$DEFAULT_IMAGE_NAME"

    normalize_tags() {
        grep -Eo '^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \
            | sed 's/^v//'
    }
    collect_local_tags() {
        git tag --list 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null | normalize_tags || true
    }
    collect_github_tags() {
        timeout 5 git ls-remote --tags --refs origin 2>/dev/null \
            | awk '{print $2}' | sed 's#^refs/tags/##' | normalize_tags || true
    }
    collect_dockerhub_tags() {
        command -v curl >/dev/null 2>&1 || return 0
        local ns="${IMAGE_NAME%%/*}" repo="${IMAGE_NAME##*/}"
        local url="https://hub.docker.com/v2/repositories/${ns}/${repo}/tags/?page_size=100"
        local page=1
        while [ -n "$url" ] && [ "$page" -le 5 ]; do
            local body
            body="$(curl -fsSL --max-time 5 "$url" 2>/dev/null)" || return 0
            echo "$body" \
                | grep -Eo '"name"[[:space:]]*:[[:space:]]*"[^"]+"' \
                | sed -E 's/.*"([^"]+)"$/\1/' \
                | normalize_tags
            url="$(echo "$body" | grep -Eo '"next"[[:space:]]*:[[:space:]]*"[^"]+"' \
                    | sed -E 's/.*"([^"]+)"$/\1/' | head -1)"
            page=$((page + 1))
        done
    }

    suggest_next_version() {
        local all latest
        all="$( { collect_local_tags; collect_github_tags; collect_dockerhub_tags; } | sort -u )"
        latest="$(echo "$all" | { grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -V | tail -1)"
        if [ -z "$latest" ]; then
            echo "0.1.0"
            return
        fi
        local major minor patch
        IFS='.' read -r major minor patch <<EOF
$latest
EOF
        patch=$((patch + 1))
        echo "${major}.${minor}.${patch}"
    }

    version_exists_anywhere() {
        local v="$1"
        { collect_local_tags; collect_github_tags; collect_dockerhub_tags; } \
            | sort -u | grep -Fxq "$v"
    }

    validate_version() {
        echo "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'
    }

    info "聚合历史版本(本地 tag / GitHub / Docker Hub)..."
    SUGGEST="$(suggest_next_version)"
    _LOCAL_MAX="$(collect_local_tags    | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true)"
    _GH_MAX="$(   collect_github_tags   | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true)"
    _DH_MAX="$(   collect_dockerhub_tags| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true)"
    info "  本地 tag 最新   : ${_LOCAL_MAX:-(无)}"
    info "  GitHub 最新    : ${_GH_MAX:-(无/不可达)}"
    info "  Docker Hub 最新 : ${_DH_MAX:-(无/不可达)}"
    info "  建议下一版本    : ${C_GREEN}${SUGGEST}${C_RESET}"

    while :; do
        echo
        echo "${C_BOLD}请输入本次发布版本号${C_RESET}(格式:1.2.3 或 v1.2.3,可带 -rc.1 等后缀)"
        ask_input "版本号" "$SUGGEST"
        VERSION="${INPUT_VAL#v}"
        if ! validate_version "$VERSION"; then
            warn "版本号格式非法:$VERSION(期望 X.Y.Z 或 X.Y.Z-rc.N)"
            continue
        fi
        VERSION_TAG="v${VERSION}"
        if git rev-parse "refs/tags/${VERSION_TAG}" >/dev/null 2>&1; then
            warn "git tag ${VERSION_TAG} 已存在(本地)"
            continue
        fi
        if version_exists_anywhere "$VERSION"; then
            warn "版本 ${VERSION_TAG} 在 本地 / GitHub / Docker Hub 中已存在,拒绝覆盖"
            continue
        fi
        break
    done

    # latest tag
    ask_choice "是否同时打 :latest 标签并推送?" 1 \
        "是(推荐,正式版本)" \
        "否(如预发布版本:rc / beta / alpha)"
    [ "$CHOICE_IDX" = "2" ] && DO_LATEST=0

    # git tag
    ask_choice "是否同步打 git tag 并推送到 GitHub?" 1 \
        "是(推荐)" \
        "否(仅推 Docker Hub)"
    [ "$CHOICE_IDX" = "2" ] && DO_GIT_TAG=0
fi

# -------------------- 同步 package.json 的 version --------------------
sync_root_pkg_version() {
    local target_version="$1"
    local pkg_file="${REPO_ROOT}/package.json"
    [ -f "$pkg_file" ] || return 0

    local current
    current="$(grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' "$pkg_file" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')"
    if [ "$current" = "$target_version" ]; then
        info "package.json version 已是 ${target_version},无需改写"
        return 0
    fi

    info "更新 package.json version: ${current:-(空)} -> ${target_version}"
    if sed --version >/dev/null 2>&1; then
        sed -i -E "0,/\"version\"[[:space:]]*:[[:space:]]*\"[^\"]+\"/s//\"version\": \"${target_version}\"/" "$pkg_file"
    else
        sed -i '' -E "1,/\"version\"[[:space:]]*:[[:space:]]*\"[^\"]+\"/s//\"version\": \"${target_version}\"/" "$pkg_file"
    fi
}

if [ "$BUILD_ONLY" != "1" ]; then
    sync_root_pkg_version "$VERSION"
fi

# -------------------- 摘要 + 最终确认 --------------------
case "$ARCH" in
    amd64) PLATFORM_DESC="linux/amd64(原生 docker build)" ;;
    arm64) PLATFORM_DESC="linux/arm64(buildx,QEMU 模拟)" ;;
    multi) PLATFORM_DESC="linux/amd64,linux/arm64(buildx --push,多架构 manifest)" ;;
esac

if [ "$BUILD_ONLY" = "1" ]; then
    step "构建摘要"
    echo "  目标镜像      : ${FULL_IMAGE}"
    echo "  构建架构      : ${PLATFORM_DESC}"
    if [ "$DO_TAR" = "1" ]; then
        echo "  输出方式      : 导出 tar → ${TAR_OUT}"
    elif [ "$DO_PUSH_CUSTOM" = "1" ]; then
        echo "  输出方式      : --push(推送到 ${FULL_IMAGE%:*})"
    else
        echo "  输出方式      : --load(加载到本机 docker)"
    fi
    echo "  git commit    : ${GIT_COMMIT}"
    echo "  构建时间      : ${BUILD_DATE}"
else
    step "发布摘要"
    echo "  版本 tag      : ${VERSION_TAG}"
    echo "  Docker 仓库   : ${IMAGE_NAME}"
    echo "  Docker 架构   : ${PLATFORM_DESC}"
    echo "  Docker latest : $([ "$DO_LATEST" = "1" ] && echo yes || echo no)"
    echo "  同步 git tag  : $([ "$DO_GIT_TAG" = "1" ] && echo yes || echo no)"
    echo "  git commit    : ${GIT_COMMIT}"
    echo "  构建时间      : ${BUILD_DATE}"
    if [ "$ARCH" = "multi" ]; then
        echo "  ${C_YELLOW}注意          : multi 模式会直接 push 多架构 manifest 到 Docker Hub${C_RESET}"
    fi
fi

echo
ask_yes_no "确认开始执行?" "y" || die "已取消"

# -------------------- 构建 tags 与 labels --------------------
START_TS=$(date +%s)

BUILD_TAGS=()
if [ "$BUILD_ONLY" = "1" ]; then
    BUILD_TAGS=( -t "${FULL_IMAGE}" )
else
    BUILD_TAGS=( -t "${IMAGE_NAME}:${VERSION_TAG}" )
    [ "$DO_LATEST" = "1" ] && BUILD_TAGS+=( -t "${IMAGE_NAME}:latest" )
fi

OCI_LABELS=(
    --label "org.opencontainers.image.revision=${GIT_SHA}"
    --label "org.opencontainers.image.created=${BUILD_DATE}"
    --label "org.opencontainers.image.source=${GITHUB_REPO_URL}"
    --label "org.opencontainers.image.title=nowen"
)
[ -n "${VERSION_TAG:-}" ] && OCI_LABELS+=( --label "org.opencontainers.image.version=${VERSION_TAG}" )

ensure_buildx_builder() {
    if ! docker buildx inspect "$BUILDX_BUILDER" >/dev/null 2>&1; then
        info "创建 buildx builder: $BUILDX_BUILDER"
        docker buildx create --name "$BUILDX_BUILDER" --use
    else
        docker buildx use "$BUILDX_BUILDER"
    fi
    docker buildx inspect --bootstrap
}

step "开始构建 Docker 镜像"
BUILD_START=$(date +%s)

# 计算 buildx 输出模式
BUILDX_OUTPUT=()
if [ "$BUILD_ONLY" = "1" ]; then
    if [ "$DO_TAR" = "1" ]; then
        BUILDX_OUTPUT=( --output "type=docker,dest=${TAR_OUT}" )
    elif [ "$DO_PUSH_CUSTOM" = "1" ]; then
        BUILDX_OUTPUT=( --push )
    else
        BUILDX_OUTPUT=( --load )
    fi
else
    if [ "$ARCH" = "multi" ]; then
        BUILDX_OUTPUT=( --push )
    else
        BUILDX_OUTPUT=( --load )
    fi
fi

case "$ARCH" in
    amd64)
        if [ "$BUILD_ONLY" = "1" ] && [ "$DO_PUSH_CUSTOM" = "1" ]; then
            # 自定义 registry push:用 buildx 直接 --push
            ensure_buildx_builder
            BUILD_CMD=(
                docker buildx build
                --platform linux/amd64
                -f "$REPO_ROOT/Dockerfile"
                "${BUILD_TAGS[@]}"
                "${OCI_LABELS[@]}"
                --push
                "$REPO_ROOT"
            )
        else
            BUILD_CMD=( docker build -f "$REPO_ROOT/Dockerfile" "${BUILD_TAGS[@]}" "${OCI_LABELS[@]}" "$REPO_ROOT" )
        fi
        echo "  ${BUILD_CMD[*]}"
        "${BUILD_CMD[@]}"
        ;;
    arm64)
        ensure_buildx_builder
        BUILD_CMD=(
            docker buildx build
            --platform linux/arm64
            -f "$REPO_ROOT/Dockerfile"
            "${BUILD_TAGS[@]}"
            "${OCI_LABELS[@]}"
            "${BUILDX_OUTPUT[@]}"
            "$REPO_ROOT"
        )
        echo "  ${BUILD_CMD[*]}"
        "${BUILD_CMD[@]}"
        ;;
    multi)
        ensure_buildx_builder
        BUILD_CMD=(
            docker buildx build
            --platform linux/amd64,linux/arm64
            -f "$REPO_ROOT/Dockerfile"
            "${BUILD_TAGS[@]}"
            "${OCI_LABELS[@]}"
            "${BUILDX_OUTPUT[@]}"
            "$REPO_ROOT"
        )
        echo "  ${BUILD_CMD[*]}"
        "${BUILD_CMD[@]}"
        ;;
esac

BUILD_END=$(date +%s)
BUILD_DURATION=$((BUILD_END - BUILD_START))
ok "Docker 构建完成,用时 ${BUILD_DURATION}s"

# -------------------- 构建模式:到此结束 --------------------
if [ "$BUILD_ONLY" = "1" ]; then
    END_TS=$(date +%s)
    TOTAL=$((END_TS - START_TS))

    step "构建完成"
    if [ "$DO_TAR" = "1" ]; then
        echo "  ${C_GREEN}${TAR_OUT}${C_RESET}  ←  已写入"
        echo
        echo "在板子上离线加载:"
        printf "    docker load -i %s\n" "$TAR_OUT"
        printf "    docker run --platform linux/arm64 -p 80:80 %s\n" "$FULL_IMAGE"
    elif [ "$DO_PUSH_CUSTOM" = "1" ]; then
        echo "  ${C_GREEN}${FULL_IMAGE}${C_RESET}  ←  已推送"
        echo
        echo "在板子 / 服务器上:"
        printf "    docker pull %s\n" "$FULL_IMAGE"
    else
        echo "  ${C_GREEN}${FULL_IMAGE}${C_RESET}  ←  已加载到本机 docker"
        echo
        echo "本机测试:"
        if [ "$ARCH" = "arm64" ]; then
            printf "    docker run --platform linux/arm64 -p 80:80 %s\n" "$FULL_IMAGE"
        else
            printf "    docker run -p 80:80 %s\n" "$FULL_IMAGE"
        fi
    fi
    echo "  构建架构      : ${PLATFORM_DESC}"
    echo "  总耗时        : ${TOTAL}s"
    echo
    ok "完成"
    exit 0
fi

# -------------------- 发布模式:docker push --------------------
PUSH_DURATION=0
if [ "$ARCH" = "multi" ]; then
    info "multi 模式 buildx 已经把镜像直接推送到 Docker Hub,跳过单独 push 步骤"
else
    step "推送镜像"
    PUSH_START=$(date +%s)
    info "推送:${IMAGE_NAME}:${VERSION_TAG}"
    docker push "${IMAGE_NAME}:${VERSION_TAG}"

    if [ "$DO_LATEST" = "1" ]; then
        info "推送:${IMAGE_NAME}:latest"
        docker push "${IMAGE_NAME}:latest"
    fi
    PUSH_END=$(date +%s)
    PUSH_DURATION=$((PUSH_END - PUSH_START))
fi

# 取 digest(multi 模式本地没镜像,拿不到)
DIGEST=""
if [ "$ARCH" != "multi" ]; then
    DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE_NAME}:${VERSION_TAG}" 2>/dev/null || echo "")"
fi

# -------------------- git tag --------------------
if [ "$DO_GIT_TAG" = "1" ]; then
    step "打 git tag 并推送到 GitHub"

    if [ -n "$(git status --porcelain -- package.json 2>/dev/null)" ]; then
        info "package.json 有变更,先 commit"
        git add package.json
        git commit -m "chore(release): ${VERSION_TAG}"
    fi

    if git rev-parse -q --verify "refs/tags/${VERSION_TAG}" >/dev/null 2>&1; then
        info "本地 tag ${VERSION_TAG} 已存在,跳过创建"
    else
        info "git tag -a ${VERSION_TAG} -m 'Release ${VERSION_TAG}'"
        git tag -a "${VERSION_TAG}" -m "Release ${VERSION_TAG}"
    fi
    info "git push origin HEAD && git push origin ${VERSION_TAG}"
    if git push origin HEAD && git push origin "${VERSION_TAG}"; then
        ok "git commit + tag ${VERSION_TAG} 已推送"
    else
        echo
        echo "${C_YELLOW}[!] git push tag 失败(Docker 镜像已推送,本地 tag 已保留)${C_RESET}"
        echo "    常见原因:GitHub 已禁用密码认证,需使用 PAT 或 SSH key"
        echo "    修复后补推:git push origin ${VERSION_TAG}"
        die "git tag 推送失败"
    fi
else
    info "已跳过 git tag"
fi

# -------------------- 完成 --------------------
END_TS=$(date +%s)
TOTAL=$((END_TS - START_TS))

step "发布完成"
echo "  ${C_GREEN}${IMAGE_NAME}:${VERSION_TAG}${C_RESET}  ←  已推送到 Docker Hub"
[ "$DO_LATEST" = "1" ] && echo "  ${C_GREEN}${IMAGE_NAME}:latest${C_RESET}  ←  已推送到 Docker Hub"
[ "$DO_GIT_TAG" = "1" ] && echo "  ${C_GREEN}git tag ${VERSION_TAG}${C_RESET}  ←  已推送到 GitHub"

echo "  总耗时        : ${TOTAL}s  (docker:${BUILD_DURATION}s push:${PUSH_DURATION}s)"
[ -n "$DIGEST" ] && echo "  docker digest : ${DIGEST}"

echo
ok "发布成功 🎉"
echo
echo "Docker 拉取命令:"
printf "    docker pull %s:%s\n" "$IMAGE_NAME" "$VERSION_TAG"
[ "$DO_LATEST" = "1" ] && printf "    docker pull %s:latest\n" "$IMAGE_NAME"