diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 8c429c1ef..11cc356c8 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -403,164 +403,38 @@ jobs: NOTIFY_MODE=failure JOB_NAME="Frontend Lint" python3 scripts/ci_notify.py ' - build-staging-api: - name: Build Staging API Image + build-staging: + name: Build Staging ${{ matrix.service_display }} Image runs-on: - saas - build-farm - timeout-minutes: 30 - needs: - - validate - - frontend-lint - if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop') - steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: "set -eu\npython3 - <<'INNERPY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\"\ - )\n time.sleep(wait)\n continue\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nINNERPY\n" - - name: Record job start time - shell: sh - run: 'set -eu - - echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV - - echo "Job started at $(date)" - - ' - - name: Docker login to Registry - shell: sh - env: - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: 'set -eu - - printf ''%s'' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin - - echo "Docker login successful" - - ' - - name: Setup cache strategy - shell: sh - run: "set -eu\n# develop/main 分支写回缓存,其他分支只读\nif [ \"${GITHUB_REF_NAME}\" = \"develop\" ] || [ \"${GITHUB_REF_NAME}\" = \"main\" ]; then\n echo \"CACHE_MODE=read-write\" >> $GITHUB_ENV\n echo \"Cache mode: read-write (will push cache)\"\nelse\n echo \"CACHE_MODE=read-only\" >> $GITHUB_ENV\n echo \"Cache mode: read-only\"\nfi\n" - - name: Setup buildx builder (docker-container driver) - shell: sh - run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" - - name: Build and push API image (buildx cache) - shell: sh - run: "set -eu -REGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\" -IMAGE_TAG=\"${REGISTRY}/xiaoxia-saas-api:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/api-cache:${GITHUB_REF_NAME}\" - -bash scripts/ci/docker_build_push.sh \ - infra/docker/api.Dockerfile \ - \"${IMAGE_TAG}\" \ - \"${CACHE_REF}\" \ - APP_VERSION=\"${GITHUB_SHA}\" - -echo -echo \"API image pushed: ${IMAGE_TAG}\" -" - - name: Job duration summary - if: always() - shell: sh - run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n" - - name: Notify on failure - continue-on-error: true - if: failure() - shell: sh - env: - CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e - - NOTIFY_MODE=failure JOB_NAME="Build Staging API Image" python3 scripts/ci_notify.py - - ' - build-staging-worker: - name: Build Staging Worker Image - runs-on: - - saas - - build-farm - timeout-minutes: 40 - needs: - - validate - - frontend-lint - if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop') - steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: "set -eu\npython3 - <<'INNERPY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\"\ - )\n time.sleep(wait)\n continue\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nINNERPY\n" - - name: Record job start time - shell: sh - run: 'set -eu - - echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV - - echo "Job started at $(date)" - - ' - - name: Docker login to Registry - shell: sh - env: - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: 'set -eu - - printf ''%s'' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin - - echo "Docker login successful" - - ' - - name: Setup cache strategy - shell: sh - run: "set -eu\n# develop/main 分支写回缓存,其他分支只读\nif [ \"${GITHUB_REF_NAME}\" = \"develop\" ] || [ \"${GITHUB_REF_NAME}\" = \"main\" ]; then\n echo \"CACHE_MODE=read-write\" >> $GITHUB_ENV\n echo \"Cache mode: read-write (will push cache)\"\nelse\n echo \"CACHE_MODE=read-only\" >> $GITHUB_ENV\n echo \"Cache mode: read-only\"\nfi\n" - - name: Setup buildx builder (docker-container driver) - shell: sh - run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" - - name: Build and push Worker image (buildx cache) - shell: sh - run: "set -eu -REGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\" -IMAGE_TAG=\"${REGISTRY}/xiaoxia-saas-worker:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/worker-cache:${GITHUB_REF_NAME}\" - -bash scripts/ci/docker_build_push.sh \ - infra/docker/worker.Dockerfile \ - \"${IMAGE_TAG}\" \ - \"${CACHE_REF}\" \ - APP_VERSION=\"${GITHUB_SHA}\" - -echo -echo \"WORKER image pushed: ${IMAGE_TAG}\" -" - - name: Job duration summary - if: always() - shell: sh - run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n" - - name: Notify on failure - continue-on-error: true - if: failure() - shell: sh - env: - CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e - - NOTIFY_MODE=failure JOB_NAME="Build Staging Worker Image" python3 scripts/ci_notify.py - - ' - build-staging-web: - name: Build Staging Web Image - runs-on: - - saas - - build-farm - timeout-minutes: 30 + timeout-minutes: ${{ matrix.timeout }} needs: - validate - frontend-lint if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop') + strategy: + fail-fast: false + matrix: + include: + - service: api + service_display: API + dockerfile: infra/docker/api.Dockerfile + image_name: xiaoxia-saas-api + cache_name: api-cache + timeout: 30 + - service: worker + service_display: Worker + dockerfile: infra/docker/worker.Dockerfile + image_name: xiaoxia-saas-worker + cache_name: worker-cache + timeout: 40 + - service: web + service_display: Web + dockerfile: infra/docker/web.Dockerfile + image_name: xiaoxia-saas-web + cache_name: web-cache + timeout: 30 steps: - name: Checkout code shell: sh @@ -592,26 +466,27 @@ echo \"WORKER image pushed: ${IMAGE_TAG}\" shell: sh run: "set -eu\n# develop/main 分支写回缓存,其他分支只读\nif [ \"${GITHUB_REF_NAME}\" = \"develop\" ] || [ \"${GITHUB_REF_NAME}\" = \"main\" ]; then\n echo \"CACHE_MODE=read-write\" >> $GITHUB_ENV\n echo \"Cache mode: read-write (will push cache)\"\nelse\n echo \"CACHE_MODE=read-only\" >> $GITHUB_ENV\n echo \"Cache mode: read-only\"\nfi\n" - name: Build frontend assets (npm build) + if: matrix.service == 'web' shell: sh run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker 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 && npx tsc --incremental --tsBuildInfoFile node_modules/.tsbuildinfo && npx vite build\"\n\ntest -f apps/web/dist/index.html\necho \"Frontend build complete: $(ls apps/web/dist/ | head -5)\"\n" - name: Setup buildx builder (docker-container driver) shell: sh run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" - - name: Build and push Web image (buildx cache) + - name: Build and push ${{ matrix.service_display }} image (buildx cache) shell: sh run: "set -eu REGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\" -IMAGE_TAG=\"${REGISTRY}/xiaoxia-saas-web:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/web-cache:${GITHUB_REF_NAME}\" +IMAGE_TAG=\"${REGISTRY}/${{ matrix.image_name }}:${GITHUB_SHA}\" +CACHE_REF=\"${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}\" bash scripts/ci/docker_build_push.sh \ - infra/docker/web.Dockerfile \ + ${{ matrix.dockerfile }} \ \"${IMAGE_TAG}\" \ \"${CACHE_REF}\" \ APP_VERSION=\"${GITHUB_SHA}\" echo -echo \"WEB image pushed: ${IMAGE_TAG}\" +echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\" " - name: Job duration summary if: always() @@ -625,7 +500,7 @@ echo \"WEB image pushed: ${IMAGE_TAG}\" CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} run: 'set +e - NOTIFY_MODE=failure JOB_NAME="Build Staging Web Image" python3 scripts/ci_notify.py + NOTIFY_MODE=failure JOB_NAME="Build Staging ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py ' deploy-staging: @@ -633,9 +508,7 @@ echo \"WEB image pushed: ${IMAGE_TAG}\" runs-on: saas timeout-minutes: 15 needs: - - build-staging-api - - build-staging-worker - - build-staging-web + - build-staging if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop') steps: - name: Checkout code @@ -819,158 +692,36 @@ echo \"WEB image pushed: ${IMAGE_TAG}\" NOTIFY_MODE=failure JOB_NAME="Staging API Integration Tests" python3 scripts/ci_notify.py ' - build-production-api: - name: Build Production API Image + build-production: + name: Build Production ${{ matrix.service_display }} Image runs-on: - saas - build-farm - timeout-minutes: 30 + timeout-minutes: ${{ matrix.timeout }} needs: - - validate - - frontend-lint - if: startsWith(github.ref, 'refs/tags/v') - steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: "set -eu\npython3 - <<'INNERPY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\"\ - )\n time.sleep(wait)\n continue\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nINNERPY\n" - - name: Record job start time - shell: sh - run: 'set -eu - - echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV - - echo "Job started at $(date)" - - ' - - name: Docker login to Registry - shell: sh - env: - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: 'set -eu - - printf ''%s'' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin - - echo "Docker login successful" - - ' - - name: Setup buildx builder (docker-container driver) - shell: sh - run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" - - name: Build and push API image (buildx cache) - shell: sh - run: "set -eu -REGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\" -IMAGE_TAG=\"${REGISTRY}/xiaoxia-saas-api:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/api-cache:${GITHUB_REF_NAME}\" - -bash scripts/ci/docker_build_push.sh \ - infra/docker/api.Dockerfile \ - \"${IMAGE_TAG}\" \ - \"${CACHE_REF}\" \ - APP_VERSION=\"${GITHUB_SHA}\" - -echo -echo \"API image pushed: ${IMAGE_TAG}\" -" - - name: Job duration summary - if: always() - shell: sh - run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n" - - name: Notify on failure - continue-on-error: true - if: failure() - shell: sh - env: - CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e - - NOTIFY_MODE=failure JOB_NAME="Build Production API Image" python3 scripts/ci_notify.py - - ' - build-production-worker: - name: Build Production Worker Image - runs-on: - - saas - - build-farm - timeout-minutes: 40 - needs: - - validate - - frontend-lint - if: startsWith(github.ref, 'refs/tags/v') - steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: "set -eu\npython3 - <<'INNERPY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\"\ - )\n time.sleep(wait)\n continue\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nINNERPY\n" - - name: Record job start time - shell: sh - run: 'set -eu - - echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV - - echo "Job started at $(date)" - - ' - - name: Docker login to Registry - shell: sh - env: - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: 'set -eu - - printf ''%s'' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin - - echo "Docker login successful" - - ' - - name: Setup buildx builder (docker-container driver) - shell: sh - run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" - - name: Build and push Worker image (buildx cache) - shell: sh - run: "set -eu -REGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\" -IMAGE_TAG=\"${REGISTRY}/xiaoxia-saas-worker:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/worker-cache:${GITHUB_REF_NAME}\" - -bash scripts/ci/docker_build_push.sh \ - infra/docker/worker.Dockerfile \ - \"${IMAGE_TAG}\" \ - \"${CACHE_REF}\" \ - APP_VERSION=\"${GITHUB_SHA}\" - -echo -echo \"WORKER image pushed: ${IMAGE_TAG}\" -" - - name: Job duration summary - if: always() - shell: sh - run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n" - - name: Notify on failure - continue-on-error: true - if: failure() - shell: sh - env: - CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e - - NOTIFY_MODE=failure JOB_NAME="Build Production Worker Image" python3 scripts/ci_notify.py - - ' - build-production-web: - name: Build Production Web Image - runs-on: - - saas - - build-farm - timeout-minutes: 30 - needs: - - validate - - frontend-lint if: startsWith(github.ref, 'refs/tags/v') + strategy: + fail-fast: false + matrix: + include: + - service: api + service_display: API + dockerfile: infra/docker/api.Dockerfile + image_name: xiaoxia-saas-api + cache_name: api-cache + timeout: 30 + - service: worker + service_display: Worker + dockerfile: infra/docker/worker.Dockerfile + image_name: xiaoxia-saas-worker + cache_name: worker-cache + timeout: 40 + - service: web + service_display: Web + dockerfile: infra/docker/web.Dockerfile + image_name: xiaoxia-saas-web + cache_name: web-cache + timeout: 30 steps: - name: Checkout code shell: sh @@ -999,29 +750,30 @@ echo \"WORKER image pushed: ${IMAGE_TAG}\" ' - name: Build frontend assets (npm build) + if: matrix.service == 'web' shell: sh run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\nfi\n\ndocker 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 && npx tsc --incremental --tsBuildInfoFile node_modules/.tsbuildinfo && npx vite build\"\n\ntest -f apps/web/dist/index.html\necho \"Frontend build complete\"\n" - name: Setup buildx builder (docker-container driver) shell: sh run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" - - name: Build and push Web image (buildx cache) + - name: Build and push ${{ matrix.service_display }} image (buildx cache) shell: sh run: "set -eu REGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\" -IMAGE_TAG=\"${REGISTRY}/xiaoxia-saas-web:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/web-cache:${GITHUB_REF_NAME}\" +IMAGE_TAG=\"${REGISTRY}/${{ matrix.image_name }}:${GITHUB_SHA}\" +CACHE_REF=\"${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}\" bash scripts/ci/docker_build_push.sh \ - infra/docker/web.Dockerfile \ + ${{ matrix.dockerfile }} \ \"${IMAGE_TAG}\" \ \"${CACHE_REF}\" \ APP_VERSION=\"${GITHUB_SHA}\" echo -echo \"WEB image pushed: ${IMAGE_TAG}\" +echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\" " - name: Cleanup old Docker images - if: always() + if: always() && matrix.service == 'web' shell: sh run: "set -eu\nif [ -f scripts/cleanup_old_images.sh ]; then\n chmod +x scripts/cleanup_old_images.sh\n scripts/cleanup_old_images.sh\nelse\n echo \"Cleanup script not found, doing basic prune...\"\n docker image prune -f 2>/dev/null || true\nfi\necho \"Disk usage after cleanup:\"\ndf -h / | tail -1\n" - name: Job duration summary @@ -1036,7 +788,7 @@ echo \"WEB image pushed: ${IMAGE_TAG}\" CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} run: 'set +e - NOTIFY_MODE=failure JOB_NAME="Build Production Web Image" python3 scripts/ci_notify.py + NOTIFY_MODE=failure JOB_NAME="Build Production ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py ' deploy-production: @@ -1045,9 +797,7 @@ echo \"WEB image pushed: ${IMAGE_TAG}\" timeout-minutes: 30 if: startsWith(github.ref, 'refs/tags/v') needs: - - build-production-api - - build-production-worker - - build-production-web + - build-production steps: - name: Checkout code shell: sh