#!/bin/sh set -eu VERSION="${1:-${RELEASE_VERSION:-}}" if [ -z "$VERSION" ]; then echo "Usage: $0 [staging|production]" echo "Example: $0 v0.1.5 production" echo " $0 abc1234 staging" exit 1 fi # 环境参数:staging 或 production(默认 production) BUILD_ENV="${2:-production}" case "$BUILD_ENV" in staging) NGINX_CONF_FILE="infra/docker/nginx-staging.conf" ;; *) NGINX_CONF_FILE="infra/docker/nginx-production.conf" ;; esac echo "Build environment: $BUILD_ENV → nginx config: $NGINX_CONF_FILE" ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" cd "$ROOT_DIR" if docker ps --format "{{.Names}}" | grep -Eq "^(xiaoxia-(api|web|worker|postgres|redis)-production|gitea)$"; then if [ "${ALLOW_SHARED_PRODUCTION_BUILD_HOST:-false}" != "true" ]; then echo "Refusing to build runtime images on a host that is running production services." exit 1 fi fi # ---- Registry 配置 ---- REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}" CACHE_REGISTRY="${CACHE_REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}" # 主缓存 tag:develop 分支构建时写入,所有分支读取 CACHE_TAG_PRIMARY="${CACHE_TAG:-develop}" API_IMAGE="xiaoxia-saas-api:$VERSION" WORKER_IMAGE="xiaoxia-saas-worker:$VERSION" WEB_IMAGE="xiaoxia-saas-web:$VERSION" API_LATEST="xiaoxia-saas-api:dev" WORKER_LATEST="xiaoxia-saas-worker:dev" # Registry 上的完整镜像名 REGISTRY_API="${REGISTRY}/xiaoxia-saas-api:$VERSION" REGISTRY_WORKER="${REGISTRY}/xiaoxia-saas-worker:$VERSION" REGISTRY_WEB="${REGISTRY}/xiaoxia-saas-web:$VERSION" USE_CACHE=0 USE_PUSH=0 CACHE_WRITE=0 # 检查 buildx 和 Registry 认证 if docker buildx version >/dev/null 2>&1; then if [ -n "${REGISTRY_TOKEN:-}" ]; then printf "%s" "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null && USE_CACHE=1 && USE_PUSH=1 fi docker buildx use default 2>/dev/null || true fi # ---- 缓存读写策略(按分支隔离)---- # 默认只读不写,防止 feature 分支污染主缓存 # 只有 develop/main 分支才写回缓存 BRANCH_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_BRANCH:-unknown}}" # 清理本地旧镜像 docker rmi -f "$API_IMAGE" "$API_LATEST" 2>/dev/null || true if [ "$USE_CACHE" -eq 1 ]; then docker buildx build \ --build-arg APP_VERSION="$VERSION" \ --cache-from "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG_PRIMARY},ignore-error=true" \ -f infra/docker/api.Dockerfile \ -t "$API_IMAGE" -t "$API_LATEST" \ --load \ . else docker build --pull=false --build-arg APP_VERSION="$VERSION" -f infra/docker/api.Dockerfile -t "$API_IMAGE" -t "$API_LATEST" . fi build_with_cache() { # usage: build_with_cache IMG_NAME="$1" DOCKERFILE="$2" shift 2 EXTRA_ARGS="$*" CACHE_FROM="type=registry,ref=${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY},ignore-error=true" if [ "$CACHE_WRITE" -eq 1 ]; then CACHE_TO="type=registry,ref=${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY},mode=max" echo " cache: read+write from ${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY}" else CACHE_TO="" echo " cache: read-only from ${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY}" fi # 清理本地旧镜像,避免 buildx --load 报 already exists 错误 docker rmi -f "$IMG_NAME:$VERSION" 2>/dev/null || true if [ "$USE_CACHE" -eq 1 ]; then if [ -n "$CACHE_TO" ]; then docker buildx build \ $EXTRA_ARGS \ --cache-from "$CACHE_FROM" \ --cache-to "$CACHE_TO" \ -f "$DOCKERFILE" \ -t "$IMG_NAME:$VERSION" \ --load \ . else docker buildx build \ $EXTRA_ARGS \ --cache-from "$CACHE_FROM" \ -f "$DOCKERFILE" \ -t "$IMG_NAME:$VERSION" \ --load \ . fi else docker build --pull=false $EXTRA_ARGS -f "$DOCKERFILE" -t "$IMG_NAME:$VERSION" . fi } echo "=== Building API image ===" build_with_cache "api" "infra/docker/api.Dockerfile" \ "--build-arg APP_VERSION=$VERSION" docker tag "$API_IMAGE" "$API_LATEST" echo "=== Building Worker image ===" # 清理本地旧镜像 docker rmi -f "$WORKER_IMAGE" "$WORKER_LATEST" 2>/dev/null || true if [ "$USE_CACHE" -eq 1 ]; then docker buildx build \ --build-arg APP_VERSION="$VERSION" \ --cache-from "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG_PRIMARY},ignore-error=true" \ -f infra/docker/worker.Dockerfile \ -t "$WORKER_IMAGE" -t "$WORKER_LATEST" \ --load \ . else docker build --pull=false --build-arg APP_VERSION="$VERSION" -f infra/docker/worker.Dockerfile -t "$WORKER_IMAGE" -t "$WORKER_LATEST" . fi echo "=== Building Web image (with buildx cache) ===" # 先构建前端产物(使用持久化 npm 缓存卷) NPM_CACHE_VOLUME="xiaoxia-npm-cache" if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then docker volume create "$NPM_CACHE_VOLUME" >/dev/null echo " Created npm cache volume: $NPM_CACHE_VOLUME" fi docker run --rm \ -v "$PWD:/workspace" \ -v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" \ -w /workspace/apps/web \ docker.m.daocloud.io/library/node:20 \ sh -lc "npm ci && npm run build" test -f apps/web/dist/index.html # 清理本地旧镜像 docker rmi -f "$WEB_IMAGE" 2>/dev/null || true if [ "$USE_CACHE" -eq 1 ]; then docker buildx build \ --cache-from "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG_PRIMARY},ignore-error=true" \ -f infra/docker/web-artifact.Dockerfile \ --build-arg "NGINX_CONF=$NGINX_CONF_FILE" \ -t "$WEB_IMAGE" \ --load \ . else docker build --pull=false \ -f infra/docker/web-artifact.Dockerfile \ --build-arg "NGINX_CONF=$NGINX_CONF_FILE" \ -t "$WEB_IMAGE" \ . fi # Push 到 Registry if [ "$USE_PUSH" -eq 1 ]; then echo "=== Pushing images to Registry ===" docker tag "$API_IMAGE" "$REGISTRY_API" docker tag "$WORKER_IMAGE" "$REGISTRY_WORKER" docker tag "$WEB_IMAGE" "$REGISTRY_WEB" docker push "$REGISTRY_API" docker push "$REGISTRY_WORKER" docker push "$REGISTRY_WEB" echo "All images pushed to $REGISTRY" else echo "Registry push skipped (no auth token available)" fi echo "=== Build complete ===" docker images | grep "xiaoxia-saas" | grep "$VERSION" || true