From d120785b1603a57669a5bfe8045cefe1ecff4ffc Mon Sep 17 00:00:00 2001 From: XiaoXia Bot Date: Wed, 15 Jul 2026 13:31:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ci):=20M-3=20=E4=B8=89=E9=95=9C?= =?UTF-8?q?=E5=83=8Fmatrix=E5=B9=B6=E8=A1=8C=E6=9E=84=E5=BB=BA=E9=87=8D?= =?UTF-8?q?=E6=9E=84\n\n=E5=B0=866=E4=B8=AA=E7=8B=AC=E7=AB=8B=E6=9E=84?= =?UTF-8?q?=E5=BB=BAjob=EF=BC=883=20staging=20+=203=20production=EF=BC=89?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=BA2=E4=B8=AAmatrix=20job\n-=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=87=8F=E5=87=8F=E5=B0=91249=E8=A1=8C?= =?UTF-8?q?=EF=BC=8821%=EF=BC=89=EF=BC=8C=E7=BB=B4=E6=8A=A4=E6=80=A7?= =?UTF-8?q?=E5=A4=A7=E5=B9=85=E6=8F=90=E5=8D=87\n-=20API/Worker/Web?= =?UTF-8?q?=E4=B8=89=E6=9C=8D=E5=8A=A1=E5=85=B1=E7=94=A8=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E5=A5=97=E6=9E=84=E5=BB=BA=E9=80=BB=E8=BE=91\n-=20web=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1npm=20build=E6=AD=A5=E9=AA=A4=E9=80=9A=E8=BF=87matrix?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E6=8E=A7=E5=88=B6\n-=20worker=E6=9E=84?= =?UTF-8?q?=E5=BB=BA40=E5=88=86=E9=92=9F=E8=B6=85=E6=97=B6=E9=80=9A?= =?UTF-8?q?=E8=BF=87matrix.timeout=E9=85=8D=E7=BD=AE\n-=20deploy-staging/d?= =?UTF-8?q?eploy-production=20needs=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=B8=BA=E5=8D=95=E4=B8=AAmatrix=20job\n-=20fail-fast:=20false?= =?UTF-8?q?=EF=BC=8C=E5=8D=95=E4=B8=AA=E6=9C=8D=E5=8A=A1=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E4=B8=8D=E5=BD=B1=E5=93=8D=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 383 +++++++------------------------------ 1 file changed, 67 insertions(+), 316 deletions(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 8c429c1ef..cc2fb924a 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,28 +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() && matrix.service == 'web' if: always() 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" @@ -1036,7 +789,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 +798,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 -- 2.54.0 From d7b7c0b235e2afcb54bba7c268c1d6862e305cdf Mon Sep 17 00:00:00 2001 From: XiaoXia Bot Date: Wed, 15 Jul 2026 13:51:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8Dmatrix?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=AD=E9=87=8D=E5=A4=8D=E7=9A=84if?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E5=AF=BC=E8=87=B4YAML=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index cc2fb924a..11cc356c8 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -774,7 +774,6 @@ echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\" " - name: Cleanup old Docker images if: always() && matrix.service == 'web' - if: always() 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 -- 2.54.0