From d99ee6fc84365fa9d8f5b2b83301ea4b532d858f Mon Sep 17 00:00:00 2001 From: CI Test Date: Tue, 7 Jul 2026 20:52:01 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20staging=20deploy=E6=94=B9=E4=B8=BARegistr?= =?UTF-8?q?y=E6=96=B9=E5=BC=8F=EF=BC=8C=E5=8E=BB=E6=8E=89tar/scp/docker=20?= =?UTF-8?q?load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 120 ++++++---------- infra/docker/deploy-staging-registry.sh | 173 ++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 78 deletions(-) create mode 100755 infra/docker/deploy-staging-registry.sh diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 6e27f81ae..f8e01711b 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -328,125 +328,89 @@ jobs: apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1 echo "openssh-client installed" - - name: Build staging 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-staging.conf \ - -t "xiaoxia-saas-web:staging-${GITHUB_SHA}" \ - . - test -f apps/web/dist/index.html - - - - name: Build and push staging API/Worker images + - name: Build and push all images to Gitea Registry shell: bash env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | set -eu - REGISTRY="172.30.18.198:5000" - CACHE_REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas" + REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas" + TAG="${GITHUB_SHA}" CACHE_BRANCH="${GITHUB_REF_NAME//\//-}" - # 登录 Gitea Registry(用于构建缓存,PAT 带 packages 权限) printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin + docker buildx use default 2>/dev/null || true - # 登录内网 Registry(运行时镜像推送目标) - printf '%s' "Xiaoxia2026" | docker login 172.30.18.198:5000 -u admin --password-stdin 2>/dev/null || true - - # 用默认 docker driver builder(共享 docker daemon 凭证,解决 buildx 认证问题) - docker buildx use default 2>/dev/null || { - echo "Warning: default builder not available, buildx cache may not work" - } - - # 验证 builder 状态 - docker buildx ls - - # 构建 API(带分布式缓存,缓存存 Gitea Registry,镜像推内网 Registry) + echo "=== Building API image ===" docker buildx build \ - --cache-from "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_BRANCH},ignore-error=true" \ - --cache-to "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_BRANCH},mode=max" \ + --cache-from "type=registry,ref=${REGISTRY}/api-cache:${CACHE_BRANCH},ignore-error=true" \ + --cache-to "type=registry,ref=${REGISTRY}/api-cache:${CACHE_BRANCH},mode=max" \ -f infra/docker/api.Dockerfile \ - -t "${REGISTRY}/xiaoxia-saas-api:dev" \ + -t "${REGISTRY}/xiaoxia-saas-api:${TAG}" \ --push \ . - # 构建 Worker(带分布式缓存) + echo "=== Building Worker image ===" docker buildx build \ - --cache-from "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_BRANCH},ignore-error=true" \ - --cache-to "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_BRANCH},mode=max" \ + --cache-from "type=registry,ref=${REGISTRY}/worker-cache:${CACHE_BRANCH},ignore-error=true" \ + --cache-to "type=registry,ref=${REGISTRY}/worker-cache:${CACHE_BRANCH},mode=max" \ -f infra/docker/worker.Dockerfile \ - -t "${REGISTRY}/xiaoxia-saas-worker:dev" \ + -t "${REGISTRY}/xiaoxia-saas-worker:${TAG}" \ --push \ . - - name: Package staging release artifact - shell: sh - run: | - set -eu - rm -rf dist/staging-artifacts - mkdir -p dist/staging-artifacts - tar --exclude=.git --exclude=apps/web/node_modules --exclude=./dist \ - -czf dist/staging-artifacts/xiaoxia-staging-${GITHUB_SHA}.tar.gz . - docker save -o "dist/staging-artifacts/xiaoxia-web-staging-${GITHUB_SHA}.tar" "xiaoxia-saas-web:staging-${GITHUB_SHA}" + echo "=== Building web frontend ===" + docker run --rm \ + -v "$PWD:/workspace" \ + -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 - - name: Upload staging artifact to business host + echo "=== Building Web image ===" + docker buildx build \ + --cache-from "type=registry,ref=${REGISTRY}/web-cache:${CACHE_BRANCH},ignore-error=true" \ + --cache-to "type=registry,ref=${REGISTRY}/web-cache:${CACHE_BRANCH},mode=max" \ + -f infra/docker/web-artifact.Dockerfile \ + --build-arg NGINX_CONF=infra/docker/nginx-production.conf \ + -t "${REGISTRY}/xiaoxia-saas-web:${TAG}" \ + --push \ + . + + echo "=== All images pushed ===" + docker images | grep "${GITHUB_SHA}" || true + + - name: Deploy staging via Registry pull shell: sh env: STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }} STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }} STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | set -eu staging_host="${STAGING_SSH_HOST:-47.98.113.167}" staging_user="${STAGING_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 "${STAGING_SSH_KEY:-}" ]; then key_path="$HOME/.ssh/id_ed25519" printf '%s\n' "$STAGING_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 "$staging_host" >> ~/.ssh/known_hosts - ssh -i "$key_path" "$staging_user@$staging_host" "mkdir -p /var/lib/xiaoxia-saas-staging/artifacts" - scp -i "$key_path" "dist/staging-artifacts/xiaoxia-staging-${GITHUB_SHA}.tar.gz" \ - "$staging_user@$staging_host:/var/lib/xiaoxia-saas-staging/artifacts/xiaoxia-staging-${GITHUB_SHA}.tar.gz" - scp -i "$key_path" "dist/staging-artifacts/xiaoxia-web-staging-${GITHUB_SHA}.tar" \ - "$staging_user@$staging_host:/var/lib/xiaoxia-saas-staging/artifacts/xiaoxia-web-staging-${GITHUB_SHA}.tar" + ssh-keyscan -H "$staging_host" >> ~/.ssh/known_hosts 2>/dev/null || true - - name: Deploy staging stack on business host - shell: sh - env: - STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }} - STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }} - STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} - run: | - set -eu - staging_host="${STAGING_SSH_HOST:-47.98.113.167}" - staging_user="${STAGING_SSH_USER:-root}" - if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then - key_path="/root/.ssh/xiaoxia_runtime_builder" - elif [ -n "${STAGING_SSH_KEY:-}" ]; then - key_path="$HOME/.ssh/id_ed25519" - else - echo "ERROR: No SSH key available" - exit 1 - fi - echo 'c2V0IC1ldQphcnRpZmFjdD0iL3Zhci9saWIveGlhb3hpYS1zYWFzLXN0YWdpbmcvYXJ0aWZhY3RzL3hpYW94aWEtc3RhZ2luZy0ke0dJVEhVQl9TSEF9LnRhci5neiIKaW1hZ2VfdGFyPSIvdmFyL2xpYi94aWFveGlhLXNhYXMtc3RhZ2luZy9hcnRpZmFjdHMveGlhb3hpYS13ZWItc3RhZ2luZy0ke0dJVEhVQl9TSEF9LnRhciIKdGVzdCAtZiAiJGFydGlmYWN0Igp0ZXN0IC1mICIkaW1hZ2VfdGFyIgp0ZXN0IC1mIC92YXIvbGliL3hpYW94aWEtc2Fhcy1zdGFnaW5nLy5lbnYKZG9ja2VyIGxvYWQgLWkgIiRpbWFnZV90YXIiCnJtIC1yZiAvdmFyL2xpYi94aWFveGlhLXNhYXMtc3RhZ2luZy9yZXBvCm1rZGlyIC1wIC92YXIvbGliL3hpYW94aWEtc2Fhcy1zdGFnaW5nL3JlcG8KdGFyIC14emYgIiRhcnRpZmFjdCIgLUMgL3Zhci9saWIveGlhb3hpYS1zYWFzLXN0YWdpbmcvcmVwbwp0ZXN0IC1mIC92YXIvbGliL3hpYW94aWEtc2Fhcy1zdGFnaW5nL3JlcG8vYXBwcy93ZWIvZGlzdC9pbmRleC5odG1sCmNwIC92YXIvbGliL3hpYW94aWEtc2Fhcy1zdGFnaW5nLy5lbnYgL3Zhci9saWIveGlhb3hpYS1zYWFzLXN0YWdpbmcvcmVwby8uZW52CmNobW9kICt4IC92YXIvbGliL3hpYW94aWEtc2Fhcy1zdGFnaW5nL3JlcG8vaW5mcmEvZG9ja2VyL2RlcGxveS1zdGFnaW5nLnNoCiMgRW5zdXJlIGlzb2xhdGVkIHN0YWdpbmcgbmV0d29yayBleGlzdHMgYmVmb3JlIGRlcGxveQpkb2NrZXIgbmV0d29yayBjcmVhdGUgeGlhb3hpYS1uZXQtc3RhZ2luZyAyPi9kZXYvbnVsbCB8fCB0cnVlClJFR0lTVFJZPSIxNzIuMzAuMTguMTk4OjUwMDAiIEFQSV9JTUFHRT0iJHtSRUdJU1RSWX0veGlhb3hpYS1zYWFzLWFwaTpkZXYiIFdPUktFUl9JTUFHRT0iJHtSRUdJU1RSWX0veGlhb3hpYS1zYWFzLXdvcmtlcjpkZXYiIFdFQl9JTUFHRT0ieGlhb3hpYS1zYWFzLXdlYjpzdGFnaW5nLSR7R0lUSFVCX1NIQX0iIEhPU1RfUFJFRklYPSBFTlY9c3RhZ2luZyBXRUJfUE9SVD0zMDAxIFJFQlVJTERfQkFDS0VORD0wIEJVSUxEX1dFQj0wIFJVTl9NSUdSQVRJT05TPTEgUkVHSVNUUllfUEFTUz0iWGlhb3hpYVJlZ2lzdHJ5MjAyNiIgL3Zhci9saWIveGlhb3hpYS1zYWFzLXN0YWdpbmcvcmVwby9pbmZyYS9kb2NrZXIvZGVwbG95LXN0YWdpbmcuc2gKaT0wCndoaWxlIFsgIiRpIiAtbHQgMzAgXTsgZG8KICBpZiB3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo4MDAwL2hlYWx0aDsgdGhlbgogICAgZXhpdCAwCiAgZmkKICBpPSQoKGkgKyAxKSkKICBzbGVlcCAyCmRvbmUKZXhpdCAxCg==' | base64 -d | ssh -i "$key_path" "$staging_user@$staging_host" "GITHUB_SHA='${GITHUB_SHA}' sh" + # 上传部署脚本(小文件,非源码包) + scp -i "$key_path" infra/docker/deploy-staging-registry.sh \ + "$staging_user@$staging_host:/tmp/deploy-staging-registry.sh" + + # SSH 执行部署 + ssh -i "$key_path" "$staging_user@$staging_host" \ + "IMAGE_TAG='${GITHUB_SHA}' REGISTRY_TOKEN='${REGISTRY_TOKEN}' sh /tmp/deploy-staging-registry.sh" - name: Post-deploy smoke test shell: sh diff --git a/infra/docker/deploy-staging-registry.sh b/infra/docker/deploy-staging-registry.sh new file mode 100755 index 000000000..9976c70be --- /dev/null +++ b/infra/docker/deploy-staging-registry.sh @@ -0,0 +1,173 @@ +#!/bin/sh +set -eu + +# ============================================ +# Staging 部署脚本 - Registry 方式 +# 用法:IMAGE_TAG= REGISTRY_TOKEN= sh deploy-staging.sh +# ============================================ + +IMAGE_TAG="${IMAGE_TAG:-}" +REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}" +REGISTRY_USER="${REGISTRY_USER:-xiaoxia}" +REGISTRY_TOKEN="${REGISTRY_TOKEN:-}" + +ENV_FILE="${ENV_FILE:-/var/lib/xiaoxia-saas-staging/.env}" +COMPOSE_DIR="${COMPOSE_DIR:-/var/lib/xiaoxia-saas-staging/repo/infra/docker}" +GENERATED_DIR="${GENERATED_DIR:-/var/lib/xiaoxia-saas-staging/generated}" + +if [ -z "$IMAGE_TAG" ]; then + echo "ERROR: IMAGE_TAG is required" + exit 1 +fi + +test -f "$ENV_FILE" +mkdir -p "$GENERATED_DIR" + +# ---- 登录 Registry ---- +if [ -n "$REGISTRY_TOKEN" ]; then + echo "Logging in to registry: $REGISTRY" + printf %s "$REGISTRY_TOKEN" | docker login "$(echo $REGISTRY | cut -d/ -f1)" -u "$REGISTRY_USER" --password-stdin 2>/dev/null || { + echo "WARN: docker login failed, will try to pull anyway" + } +fi + +# ---- Pull 三镜像 ---- +REGISTRY_API="${REGISTRY}/xiaoxia-saas-api:${IMAGE_TAG}" +REGISTRY_WORKER="${REGISTRY}/xiaoxia-saas-worker:${IMAGE_TAG}" +REGISTRY_WEB="${REGISTRY}/xiaoxia-saas-web:${IMAGE_TAG}" + +LOCAL_API="xiaoxia-saas-api:staging-${IMAGE_TAG}" +LOCAL_WORKER="xiaoxia-saas-worker:staging-${IMAGE_TAG}" +LOCAL_WEB="xiaoxia-saas-web:staging-${IMAGE_TAG}" + +echo "Pulling API image..." +docker pull "$REGISTRY_API" +echo "Pulling Worker image..." +docker pull "$REGISTRY_WORKER" +echo "Pulling Web image..." +docker pull "$REGISTRY_WEB" + +# ---- Re-tag 成本地名 ---- +docker tag "$REGISTRY_API" "$LOCAL_API" +docker tag "$REGISTRY_WORKER" "$LOCAL_WORKER" +docker tag "$REGISTRY_WEB" "$LOCAL_WEB" +echo "All images pulled and tagged." + +# ---- 确保基础设施容器在运行 ---- +for c in xiaoxia-postgres-staging xiaoxia-redis-staging; do + if ! docker inspect "$c" >/dev/null 2>&1; then + echo "ERROR: Required container not found: $c" + exit 1 + fi + state=$(docker inspect -f {{.State.Status}} "$c") + if [ "$state" != "running" ]; then + echo "ERROR: Container not running: $c ($state)" + exit 1 + fi +done + +# ---- 确保 staging 网络存在 ---- +docker network create xiaoxia-net-staging 2>/dev/null || true + +# ---- 执行数据库 Migration ---- +echo "Running database migrations..." +docker run --rm --env-file "$ENV_FILE" --network xiaoxia-net-staging "$LOCAL_API" sh -c "cd /app && alembic upgrade head" +echo "Migrations completed." + +# ---- 停止旧容器 ---- +docker rm -f xiaoxia-api-staging 2>/dev/null || true +docker rm -f xiaoxia-worker-staging 2>/dev/null || true +docker rm -f xiaoxia-web-staging 2>/dev/null || true + +# ---- 启动 API ---- +echo "Starting API container..." +docker run -d \ + --name xiaoxia-api-staging \ + --env-file "$ENV_FILE" \ + --network xiaoxia-net-staging \ + -p 127.0.0.1:8000:8000 \ + -e APP_ENV=staging \ + -e APP_VERSION="$IMAGE_TAG" \ + -e GENERATED_FILES_DIR=/app/generated \ + -e GENERATED_FILES_URL_PREFIX=/generated-files \ + -v "$GENERATED_DIR:/app/generated" \ + --restart unless-stopped \ + --health-cmd "python -c \"import urllib.request; urllib.request.urlopen(http://localhost:8000/health, timeout=5)\"" \ + --health-interval 30s \ + --health-timeout 10s \ + --health-retries 3 \ + --health-start-period 40s \ + "$LOCAL_API" + +# ---- 启动 Worker ---- +echo "Starting Worker container..." +docker run -d \ + --name xiaoxia-worker-staging \ + --env-file "$ENV_FILE" \ + --network xiaoxia-net-staging \ + -e APP_ENV=staging \ + -e APP_VERSION="$IMAGE_TAG" \ + -e WORKER_CONCURRENCY=1 \ + -e WORKER_MAX_TASKS_PER_CHILD=100 \ + -e GENERATED_FILES_DIR=/app/generated \ + -e GENERATED_FILES_URL_PREFIX=/generated-files \ + -v "$GENERATED_DIR:/app/generated" \ + --restart unless-stopped \ + --health-cmd "sh -c \"grep -q celery /proc/1/cmdline || exit 1\"" \ + --health-interval 30s \ + --health-timeout 10s \ + --health-retries 3 \ + --health-start-period 30s \ + "$LOCAL_WORKER" + +# ---- 启动 Web ---- +# Web 镜像默认打包 production nginx.conf,staging 需要挂载 staging 配置 +NGINX_CONF="${NGINX_CONF:-${COMPOSE_DIR}/nginx-staging.conf}" +if [ ! -f "$NGINX_CONF" ]; then + echo "WARN: nginx config not found at $NGINX_CONF, using image default" + NGINX_VOLUME="" +else + NGINX_VOLUME="-v ${NGINX_CONF}:/etc/nginx/conf.d/default.conf:ro" +fi + +echo "Starting Web container..." +docker run -d \ + --name xiaoxia-web-staging \ + --network xiaoxia-net-staging \ + -p 127.0.0.1:3001:80 \ + --restart unless-stopped \ + $NGINX_VOLUME \ + --health-cmd "wget --spider -q http://127.0.0.1:80" \ + --health-interval 30s \ + --health-timeout 5s \ + --health-retries 3 \ + "$LOCAL_WEB" + +# ---- 等待 API 健康 ---- +echo "Waiting for API to become healthy..." +i=0 +while [ "$i" -lt 30 ]; do + if curl -sf --max-time 5 http://127.0.0.1:8000/health >/dev/null 2>&1; then + echo "API is healthy!" + break + fi + i=$((i + 1)) + echo " Waiting... ($i/30)" + sleep 2 +done + +if [ "$i" -ge 30 ]; then + echo "ERROR: API did not become healthy within 60s" + docker logs --tail 30 xiaoxia-api-staging + exit 1 +fi + +# ---- 清理旧镜像 ---- +docker image prune -af --filter "until=72h" 2>/dev/null || true + +echo "" +echo "=== Staging deployment complete ===" +echo "API: http://127.0.0.1:8000" +echo "Web: http://127.0.0.1:3001" +echo "Version: $IMAGE_TAG" +docker ps --format "table {{.Names}}\t{{.Status}}" | grep staging