diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 23738de10..a079175b7 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -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 diff --git a/infra/docker/deploy-production.sh b/infra/docker/deploy-production.sh index c2964384c..8d7812495 100755 --- a/infra/docker/deploy-production.sh +++ b/infra/docker/deploy-production.sh @@ -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" < "$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 diff --git a/scripts/build_release_images.sh b/scripts/build_release_images.sh index d65e5b48d..7c4d08450 100755 --- a/scripts/build_release_images.sh +++ b/scripts/build_release_images.sh @@ -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"