ci: 优化生产镜像部署 - 改用Registry push/pull替代tar+scp\n\n- build_release_images.sh: 增加web镜像buildx缓存构建 + push到Gitea Registry\n- deploy-production.sh: 改用docker pull从Registry拉取镜像,替代docker load\n- ci-cd.yml: 简化build-production和deploy-production job\n - 移除docker save + scp传镜像tar的流程\n - 仅保留源码tar.gz scp(compose/migration脚本需要)\n - web镜像构建合并到build_release_images.sh统一管理
CI/CD Pipeline / Deploy Staging (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 60h28m37s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 60h30m7s

This commit is contained in:
DevOps Bot
2026-07-06 19:20:03 +08:00
parent 6e30a96f6d
commit e6dfeafb85
3 changed files with 112 additions and 85 deletions
+9 -30
View File
@@ -602,7 +602,6 @@ jobs:
import io, os, tarfile, time, urllib.request, urllib.error
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
# Retry up to 5 times with backoff for transient 5xx errors
last_err = None
for attempt in range(5):
try:
@@ -639,7 +638,7 @@ jobs:
tar.extract(member, '.')
PY
- name: Build runtime image artifact
- name: Build and push all images (api + worker + web, with buildx cache)
shell: sh
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
@@ -648,22 +647,6 @@ jobs:
chmod +x scripts/build_release_images.sh
REGISTRY_TOKEN="${REGISTRY_TOKEN}" scripts/build_release_images.sh "${GITHUB_REF_NAME}"
- name: Build production web artifact
shell: sh
run: |
set -eu
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace/apps/web \
docker.m.daocloud.io/library/node:20 \
sh -lc 'npm ci && npm run build'
docker build --pull=false \
-f infra/docker/web-artifact.Dockerfile \
--build-arg NGINX_CONF=infra/docker/nginx-production.conf \
-t "xiaoxia-saas-web:${GITHUB_REF_NAME}" \
.
test -f apps/web/dist/index.html
- name: Package release source artifact
shell: sh
run: |
@@ -671,9 +654,8 @@ jobs:
mkdir -p dist/release-artifacts
tar --exclude=.git --exclude=apps/web/node_modules --exclude=./dist \
-czf "dist/release-artifacts/xiaoxia-release-${GITHUB_REF_NAME}.tar.gz" .
docker save -o "dist/release-artifacts/xiaoxia-web-${GITHUB_REF_NAME}.tar" "xiaoxia-saas-web:${GITHUB_REF_NAME}"
- name: Upload runtime image and release artifacts
- name: Upload source artifact to production
shell: sh
env:
PRODUCTION_SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
@@ -684,7 +666,6 @@ jobs:
production_host="${PRODUCTION_SSH_HOST:-47.98.113.167}"
production_user="${PRODUCTION_SSH_USER:-root}"
mkdir -p ~/.ssh
# Prefer host key if available (more reliable), fallback to secrets
if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then
key_path="/root/.ssh/xiaoxia_runtime_builder"
echo "Using host SSH key: $key_path"
@@ -698,12 +679,8 @@ jobs:
exit 1
fi
ssh-keyscan -H "$production_host" >> ~/.ssh/known_hosts
scp -i "$key_path" "dist/release-images/xiaoxia-runtime-images-${GITHUB_REF_NAME}.tar" \
"$production_user@$production_host:/var/lib/xiaoxia-saas-production/runtime-images-${GITHUB_REF_NAME}.tar"
scp -i "$key_path" "dist/release-artifacts/xiaoxia-release-${GITHUB_REF_NAME}.tar.gz" \
"$production_user@$production_host:/var/lib/xiaoxia-saas-production/release-${GITHUB_REF_NAME}.tar.gz"
scp -i "$key_path" "dist/release-artifacts/xiaoxia-web-${GITHUB_REF_NAME}.tar" \
"$production_user@$production_host:/var/lib/xiaoxia-saas-production/web-${GITHUB_REF_NAME}.tar"
- name: Cleanup old Docker images
if: always()
@@ -734,32 +711,34 @@ jobs:
apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1
echo "openssh-client installed"
- name: Deploy production over SSH
- name: Deploy production over SSH (pull images from registry)
shell: sh
env:
PRODUCTION_SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
PRODUCTION_SSH_USER: ${{ secrets.PRODUCTION_SSH_USER }}
PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
production_host="${PRODUCTION_SSH_HOST:-47.98.113.167}"
production_user="${PRODUCTION_SSH_USER:-root}"
mkdir -p ~/.ssh
# Prefer host key if available (more reliable), fallback to secrets
if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then
key_path="/root/.ssh/xiaoxia_runtime_builder"
echo "Using host SSH key: $key_path"
elif [ -n "${PRODUCTION_SSH_KEY:-}" ]; then
key_path="$HOME/.ssh/id_ed25519"
printf '%s\n' "$PRODUCTION_SSH_KEY" > "$key_path"
chmod 600 "$key_path"
echo "Using secret SSH key: $key_path"
else
echo "ERROR: No SSH key available"
exit 1
fi
ssh-keyscan -H "$production_host" >> ~/.ssh/known_hosts
echo 'c2V0IC1ldQpyZWxlYXNlX3Rhcj0iL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVsZWFzZS0ke1JFTEVBU0VfVkVSU0lPTn0udGFyLmd6Igp0ZXN0IC1mICIkcmVsZWFzZV90YXIiCnRlc3QgLWYgIi92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3J1bnRpbWUtaW1hZ2VzLSR7UkVMRUFTRV9WRVJTSU9OfS50YXIiCnRlc3QgLWYgIi92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3dlYi0ke1JFTEVBU0VfVkVSU0lPTn0udGFyIgpta2RpciAtcCAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbgpvbGRfYXNzZXRzX2Rpcj0iL3RtcC94aWFveGlhLXByZXZpb3VzLXdlYi1hc3NldHMtJHtSRUxFQVNFX1ZFUlNJT059IgpybSAtcmYgIiRvbGRfYXNzZXRzX2RpciIKbWtkaXIgLXAgIiRvbGRfYXNzZXRzX2RpciIKaWYgZG9ja2VyIGluc3BlY3QgeGlhb3hpYS13ZWItcHJvZHVjdGlvbiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICBkb2NrZXIgY3AgeGlhb3hpYS13ZWItcHJvZHVjdGlvbjovdXNyL3NoYXJlL25naW54L2h0bWwvYXNzZXRzLy4gIiRvbGRfYXNzZXRzX2RpciIvIDI+L2Rldi9udWxsIHx8IHRydWUKZmkKaWYgWyAtZCAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi9yZXBvL2FwcHMvd2ViL2Rpc3QvYXNzZXRzIF07IHRoZW4KICBjcCAtYSAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi9yZXBvL2FwcHMvd2ViL2Rpc3QvYXNzZXRzLy4gIiRvbGRfYXNzZXRzX2RpciIvCmZpCnJtIC1yZiAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi9yZXBvCm1rZGlyIC1wIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8KdGFyIC14emYgIiRyZWxlYXNlX3RhciIgLUMgL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwbwp0ZXN0IC1mIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8vYXBwcy93ZWIvZGlzdC9pbmRleC5odG1sCmlmIFsgLWQgIiRvbGRfYXNzZXRzX2RpciIgXTsgdGhlbgogIG1rZGlyIC1wIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8vYXBwcy93ZWIvZGlzdC9hc3NldHMKICBmb3IgYXNzZXQgaW4gIiRvbGRfYXNzZXRzX2RpciIvKjsgZG8KICAgIFsgLWUgIiRhc3NldCIgXSB8fCBjb250aW51ZQogICAgbmFtZT0iJChiYXNlbmFtZSAiJGFzc2V0IikiCiAgICBpZiBbICEgLWUgIi92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8vYXBwcy93ZWIvZGlzdC9hc3NldHMvJG5hbWUiIF07IHRoZW4KICAgICAgY3AgLWEgIiRhc3NldCIgIi92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8vYXBwcy93ZWIvZGlzdC9hc3NldHMvJG5hbWUiCiAgICBmaQogIGRvbmUKICBybSAtcmYgIiRvbGRfYXNzZXRzX2RpciIKZmkKdGVzdCAtZiAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi8uZW52CmNwIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uLy5lbnYgL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby8uZW52CiMgRW5zdXJlIGlzb2xhdGVkIHByb2R1Y3Rpb24gbmV0d29yayBleGlzdHMgYmVmb3JlIGRlcGxveQpkb2NrZXIgbmV0d29yayBjcmVhdGUgeGlhb3hpYS1uZXQtcHJvZHVjdGlvbiAyPi9kZXYvbnVsbCB8fCB0cnVlCkhPU1RfUFJFRklYPSBFTlY9cHJvZHVjdGlvbiBXRUJfSU1BR0U9InhpYW94aWEtc2Fhcy13ZWI6JHtSRUxFQVNFX1ZFUlNJT059IiBXRUJfSU1BR0VfVEFSPSIvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi93ZWItJHtSRUxFQVNFX1ZFUlNJT059LnRhciIgc2ggL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby9pbmZyYS9kb2NrZXIvZGVwbG95LXByb2R1Y3Rpb24uc2gKaT0wCndoaWxlIFsgIiRpIiAtbHQgMzAgXTsgZG8KICBpZiB3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo4MDAxL2hlYWx0aDsgdGhlbgogICAgZXhpdCAwCiAgZmkKICBpPSQoKGkgKyAxKSkKICBzbGVlcCAyCmRvbmUKZXhpdCAxCg==' | base64 -d | ssh -i "$key_path" "$production_user@$production_host" "RELEASE_VERSION='${GITHUB_REF_NAME}' sh"
# 部署脚本(base64 编码避免转义问题)
DEPLOY_B64="c2V0IC1ldQpyZWxlYXNlX3Rhcj0iL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVsZWFzZS0ke1JFTEVBU0VfVkVSU0lPTn0udGFyLmd6Igp0ZXN0IC1mICIkcmVsZWFzZV90YXIiCm1rZGlyIC1wIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uCm9sZF9hc3NldHNfZGlyPSIvdG1wL3hpYW94aWEtcHJldmlvdXMtd2ViLWFzc2V0cy0ke1JFTEVBU0VfVkVSU0lPTn0iCnJtIC1yZiAiJG9sZF9hc3NldHNfZGlyIgpta2RpciAtcCAiJG9sZF9hc3NldHNfZGlyIgppZiBkb2NrZXIgaW5zcGVjdCB4aWFveGlhLXdlYi1wcm9kdWN0aW9uID4vZGV2L251bGwgMj4mMTsgdGhlbgogIGRvY2tlciBjcCB4aWFveGlhLXdlYi1wcm9kdWN0aW9uOi91c3Ivc2hhcmUvbmdpbngvaHRtbC9hc3NldHMvLiAiJG9sZF9hc3NldHNfZGlyLyIgMj4vZGV2L251bGwgfHwgdHJ1ZQpmaQppZiBbIC1kIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8vYXBwcy93ZWIvZGlzdC9hc3NldHMgXTsgdGhlbgogIGNwIC1hIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8vYXBwcy93ZWIvZGlzdC9hc3NldHMvLiAiJG9sZF9hc3NldHNfZGlyLyIKZmkKcm0gLXJmIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL3JlcG8KbWtkaXIgLXAgL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwbwp0YXIgLXh6ZiAiJHJlbGVhc2VfdGFyIiAtQyAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi9yZXBvCnRlc3QgLWYgL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby9hcHBzL3dlYi9kaXN0L2luZGV4Lmh0bWwKaWYgWyAtZCAiJG9sZF9hc3NldHNfZGlyIiBdOyB0aGVuCiAgbWtkaXIgLXAgL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby9hcHBzL3dlYi9kaXN0L2Fzc2V0cwogIGZvciBhc3NldCBpbiAiJG9sZF9hc3NldHNfZGlyIi8qOyBkbwogICAgWyAtZSAiJGFzc2V0IiBdIHx8IGNvbnRpbnVlCiAgICBuYW1lPSIkKGJhc2VuYW1lICIkYXNzZXQiKSIKICAgIGlmIFsgISAtZSAiL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby9hcHBzL3dlYi9kaXN0L2Fzc2V0cy8kbmFtZSIgXTsgdGhlbgogICAgICBjcCAtYSAiJGFzc2V0IiAiL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby9hcHBzL3dlYi9kaXN0L2Fzc2V0cy8kbmFtZSIKICAgIGZpCiAgZG9uZQogIHJtIC1yZiAiJG9sZF9hc3NldHNfZGlyIgpmaQp0ZXN0IC1mIC92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uLy5lbnYKY3AgL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vLmVudiAvdmFyL2xpYi94aWFveGlhLXNhYXMtcHJvZHVjdGlvbi9yZXBvLy5lbnYKZG9ja2VyIG5ldHdvcmsgY3JlYXRlIHhpYW94aWEtbmV0LXByb2R1Y3Rpb24gMj4vZGV2L251bGwgfHwgdHJ1ZQpSRUdJU1RSWV9UT0tFTj0iJHtSRUdJU1RSWV9UT0tFTn0iIFJFTEVBU0VfVkVSU0lPTj0iJHtSRUxFQVNFX1ZFUlNJT059IiBIT1NUX1BSRUZJWD0gRU5WPXByb2R1Y3Rpb24gc2ggL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vcmVwby9pbmZyYS9kb2NrZXIvZGVwbG95LXByb2R1Y3Rpb24uc2gKaz0wCndoaWxlIFsgIiRpIiAtbHQgMzAgXTsgZG8KICBpZiB3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo4MDAxL2hlYWx0aDsgdGhlbgogICAgZXhpdCAwCiAgZmkKICBpPSQoKGkgKyAxKSkKICBzbGVlcCAyCmRvbmUKZXhpdCAxCg=="
echo "$DEPLOY_B64" | base64 -d | ssh -i "$key_path" "$production_user@$production_host" "RELEASE_VERSION='${GITHUB_REF_NAME}' REGISTRY_TOKEN='${REGISTRY_TOKEN}' sh"
production-e2e:
name: Production Browser E2E
+35 -33
View File
@@ -6,25 +6,19 @@ ROOT_DIR="$HOST_PREFIX/var/lib/xiaoxia-saas-production/repo"
COMPOSE_DIR="$ROOT_DIR/infra/docker"
ENV_FILE="$HOST_PREFIX/var/lib/xiaoxia-saas-production/.env"
RELEASE_VERSION="${RELEASE_VERSION:-}"
RUNTIME_IMAGE_TAR="${RUNTIME_IMAGE_TAR:-}"
WEB_IMAGE_TAR="${WEB_IMAGE_TAR:-}"
if [ -n "$RELEASE_VERSION" ] && [ -z "$RUNTIME_IMAGE_TAR" ]; then
RUNTIME_IMAGE_TAR="$HOST_PREFIX/var/lib/xiaoxia-saas-production/runtime-images-$RELEASE_VERSION.tar"
fi
if [ -n "$RELEASE_VERSION" ] && [ -z "$WEB_IMAGE_TAR" ]; then
WEB_IMAGE_TAR="$HOST_PREFIX/var/lib/xiaoxia-saas-production/web-$RELEASE_VERSION.tar"
fi
# Registry 配置
REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
REGISTRY_USER="${REGISTRY_USER:-xiaoxia}"
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
ensure_container_running() {
name="$1"
if ! docker inspect "$name" >/dev/null 2>&1; then
echo "Required infrastructure container not found: $name"
echo "Run infra/docker/infra-production.yml first before deploying production applications."
exit 1
fi
state="$(docker inspect -f '{{.State.Status}}' "$name")"
state="$(docker inspect -f "{{.State.Status}}" "$name")"
if [ "$state" != "running" ]; then
echo "Required infrastructure container is not running: $name ($state)"
exit 1
@@ -39,10 +33,10 @@ fi
cd "$ROOT_DIR"
cp "$ENV_FILE" "$ROOT_DIR/.env"
mkdir -p "$ROOT_DIR/apps/web/public" "$HOST_PREFIX/var/lib/xiaoxia-saas-production/generated"
[ -f "$ROOT_DIR/apps/web/public/.keep" ] || printf 'placeholder' > "$ROOT_DIR/apps/web/public/.keep"
[ -f "$ROOT_DIR/apps/web/public/.keep" ] || printf "placeholder" > "$ROOT_DIR/apps/web/public/.keep"
if [ ! -f "$ROOT_DIR/apps/web/dist/index.html" ]; then
echo "Missing prebuilt web artifact: $ROOT_DIR/apps/web/dist/index.html"
echo "Production deploy must not build frontend assets on the server. Run npm run build before packaging the release artifact."
echo "Production deploy must not build frontend assets on the server."
exit 1
fi
@@ -50,23 +44,33 @@ ensure_container_running xiaoxia-postgres-production
ensure_container_running xiaoxia-redis-production
cd "$COMPOSE_DIR"
if [ -n "$RELEASE_VERSION" ]; then
if [ ! -f "$RUNTIME_IMAGE_TAR" ]; then
echo "Missing production runtime image artifact: $RUNTIME_IMAGE_TAR"
echo "Build it on a dedicated build host with scripts/build_release_images.sh $RELEASE_VERSION, then upload it before production deploy."
exit 1
fi
docker load -i "$RUNTIME_IMAGE_TAR"
if [ ! -f "$WEB_IMAGE_TAR" ]; then
echo "Missing production web image artifact: $WEB_IMAGE_TAR"
echo "Build it on a dedicated build host, then upload it before production deploy."
exit 1
fi
docker load -i "$WEB_IMAGE_TAR"
export API_IMAGE="xiaoxia-saas-api:$RELEASE_VERSION"
export WORKER_IMAGE="xiaoxia-saas-worker:$RELEASE_VERSION"
export WEB_IMAGE="${WEB_IMAGE:-xiaoxia-saas-web:$RELEASE_VERSION}"
export WEB_IMAGE="xiaoxia-saas-web:$RELEASE_VERSION"
export APP_VERSION="$RELEASE_VERSION"
# ---- 从 Registry Pull 镜像 ----
echo "Pulling images from registry: $REGISTRY"
if [ -n "$REGISTRY_TOKEN" ]; then
printf "%s" "$REGISTRY_TOKEN" | docker login git.xiaoxiajianji.com -u "$REGISTRY_USER" --password-stdin 2>/dev/null || true
fi
REGISTRY_API="${REGISTRY}/xiaoxia-saas-api:$RELEASE_VERSION"
REGISTRY_WORKER="${REGISTRY}/xiaoxia-saas-worker:$RELEASE_VERSION"
REGISTRY_WEB="${REGISTRY}/xiaoxia-saas-web:$RELEASE_VERSION"
docker pull "$REGISTRY_API"
docker pull "$REGISTRY_WORKER"
docker pull "$REGISTRY_WEB"
# Re-tag 为本地镜像名,保持 compose 兼容
docker tag "$REGISTRY_API" "$API_IMAGE"
docker tag "$REGISTRY_WORKER" "$WORKER_IMAGE"
docker tag "$REGISTRY_WEB" "$WEB_IMAGE"
echo "All images pulled and tagged."
fi
export DOCKER_BUILDKIT=0
@@ -76,7 +80,6 @@ export ENV=production
export WEB_DOCKERFILE=infra/docker/web-artifact.Dockerfile
export WEB_NGINX_CONF=infra/docker/nginx-production.conf
# Ensure isolated production network exists
docker network create xiaoxia-net-production 2>/dev/null || true
export WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-1}"
export WORKER_MAX_TASKS_PER_CHILD="${WORKER_MAX_TASKS_PER_CHILD:-100}"
@@ -86,24 +89,23 @@ if [ "${ALLOW_PRODUCTION_BUILDS:-false}" = "true" ]; then
docker compose --env-file "$ENV_FILE" build --pull=false worker
docker compose --env-file "$ENV_FILE" build --pull=false web
else
echo "Skipping production API/worker/web image builds. Set ALLOW_PRODUCTION_BUILDS=true only on a dedicated build host."
echo "Skipping production image builds (using prebuilt images from registry)."
docker image inspect "${API_IMAGE:-xiaoxia-saas-api:dev}" >/dev/null
docker image inspect "${WORKER_IMAGE:-xiaoxia-saas-worker:dev}" >/dev/null
docker image inspect "${WEB_IMAGE:-xiaoxia-saas-web:dev}" >/dev/null
fi
docker compose --env-file "$ENV_FILE" run --rm --no-deps api sh -c '
docker compose --env-file "$ENV_FILE" run --rm --no-deps api sh -c "
cd /app &&
python /app/scripts/validate_release_env.py --from-environ --strict-external &&
alembic upgrade head
'
"
docker compose --env-file "$ENV_FILE" up -d api worker
# Recreate web after API so nginx resolves the current API container IP.
# Docker's embedded DNS is resolved by nginx at startup for this static upstream.
docker compose --env-file "$ENV_FILE" up -d --force-recreate web
docker compose --env-file "$ENV_FILE" ps
if [ -n "$RELEASE_VERSION" ] && [ -d "$HOST_PREFIX/etc/cron.d" ]; then
cat > "$HOST_PREFIX/etc/cron.d/xiaoxia-production-resource-check" <<EOF
cat > "$HOST_PREFIX/etc/cron.d/xiaoxia-production-resource-check" << EOF
*/5 * * * * root cd /var/lib/xiaoxia-saas-production/repo && EXPECTED_VERSION=$RELEASE_VERSION sh scripts/production_resource_check.sh >/var/log/xiaoxia-resource-check.log 2>&1
EOF
fi
+68 -22
View File
@@ -9,42 +9,44 @@ if [ -z "$VERSION" ]; then
fi
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
OUTPUT_DIR="${OUTPUT_DIR:-$ROOT_DIR/dist/release-images}"
mkdir -p "$OUTPUT_DIR"
API_IMAGE="xiaoxia-saas-api:$VERSION"
WORKER_IMAGE="xiaoxia-saas-worker:$VERSION"
API_LATEST="xiaoxia-saas-api:dev"
WORKER_LATEST="xiaoxia-saas-worker:dev"
TAR_PATH="$OUTPUT_DIR/xiaoxia-runtime-images-$VERSION.tar"
cd "$ROOT_DIR"
if docker ps --format '{{.Names}}' | grep -Eq '^(xiaoxia-(api|web|worker|postgres|redis)-production|gitea)$'; then
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."
echo "Use a dedicated build host/CI runner, or set ALLOW_SHARED_PRODUCTION_BUILD_HOST=true only for an explicitly approved emergency."
exit 1
fi
fi
# ---- BuildKit 缓存优化 ----
# ---- Registry 配置 ----
REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
CACHE_REGISTRY="${CACHE_REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
CACHE_TAG="${CACHE_TAG:-release}"
USE_CACHE=0
# 检查 buildx 和 Gitea Registry 认证是否可用
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
# 检查 buildx 和 Registry 认证
if docker buildx version >/dev/null 2>&1; then
# 尝试登录缓存 Registry(有 token 才启用缓存)
if [ -n "${REGISTRY_TOKEN:-}" ]; then
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null && USE_CACHE=1
printf "%s" "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null && USE_CACHE=1 && USE_PUSH=1
fi
# 确保使用 docker driver(共享 daemon 凭证)
docker buildx use default 2>/dev/null || true
fi
echo "=== Building API image ==="
if [ "$USE_CACHE" -eq 1 ]; then
echo "Using buildx with distributed cache (${CACHE_REGISTRY})"
docker buildx build \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG},mode=max" \
@@ -52,6 +54,12 @@ if [ "$USE_CACHE" -eq 1 ]; then
-t "$API_IMAGE" -t "$API_LATEST" \
--load \
.
else
docker build --pull=false -f infra/docker/api.Dockerfile -t "$API_IMAGE" -t "$API_LATEST" .
fi
echo "=== Building Worker image ==="
if [ "$USE_CACHE" -eq 1 ]; then
docker buildx build \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG},mode=max" \
@@ -60,11 +68,49 @@ if [ "$USE_CACHE" -eq 1 ]; then
--load \
.
else
echo "Buildx cache not available, using plain docker build"
docker build --pull=false -f infra/docker/api.Dockerfile -t "$API_IMAGE" -t "$API_LATEST" .
docker build --pull=false -f infra/docker/worker.Dockerfile -t "$WORKER_IMAGE" -t "$WORKER_LATEST" .
fi
docker save "$API_IMAGE" "$API_LATEST" "$WORKER_IMAGE" "$WORKER_LATEST" -o "$TAR_PATH"
echo "=== Building Web image (with buildx cache) ==="
# 先构建前端产物
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace/apps/web \
docker.m.daocloud.io/library/node:20 \
sh -lc "npm ci && npm run build"
printf '%s\n' "$TAR_PATH"
test -f apps/web/dist/index.html
if [ "$USE_CACHE" -eq 1 ]; then
docker buildx build \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},mode=max" \
-f infra/docker/web-artifact.Dockerfile \
--build-arg NGINX_CONF=infra/docker/nginx-production.conf \
-t "$WEB_IMAGE" \
--load \
.
else
docker build --pull=false \
-f infra/docker/web-artifact.Dockerfile \
--build-arg NGINX_CONF=infra/docker/nginx-production.conf \
-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.*:$VERSION"