Compare commits

...

3 Commits

Author SHA1 Message Date
xiaoxia 7941654224 fix(ci): 配置docker-container buildx builder以支持cache export
新CI服务器默认docker driver不支持buildx cache export功能,
在每个构建Job中自动创建/使用docker-container类型的builder实例,
确保缓存读写正常工作。
2026-07-14 15:21:52 +08:00
CI Bot c34c1634fc fix: update runner labels to match new scheme [host, ci-check] / [host, build-only]
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 42s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m55s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m20s
2026-07-14 15:06:27 +08:00
xiaoxia cf881c6ba3 feat(ci): 镜像并行构建 - api/worker/web三镜像并行,构建时间从17min压缩到8-10min 2026-07-14 15:06:27 +08:00
+588 -130
View File
@@ -26,7 +26,7 @@ concurrency:
jobs:
validate:
name: Validate Code Quality And Tests
runs-on: [host, ci-check]
runs-on: host
timeout-minutes: 10
env:
@@ -101,53 +101,6 @@ jobs:
bandit --version
pytest --version
- name: Secret detection (gitleaks)
shell: sh
run: |
set -eu
echo "=== Installing gitleaks ==="
# 优先尝试 GitHub release,失败则用国内镜像
GITLEAKS_VERSION="8.18.4"
install_gitleaks() {
local url="$1"
curl -sSL -f -o /tmp/gitleaks.tar.gz "$url" || return 1
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks || return 1
chmod +x /tmp/gitleaks || return 1
/tmp/gitleaks version || return 1
return 0
}
if ! install_gitleaks "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"; then
echo "GitHub release failed, trying mirror..."
if ! install_gitleaks "https://gitee.com/mirrors/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"; then
echo "WARN: Failed to install gitleaks from all sources, skipping secret scan"
exit 0
fi
fi
echo ""
echo "=== Running gitleaks scan ==="
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PR触发: 增量扫描
echo "PR mode: scanning changed files"
EXIT_CODE=0
/tmp/gitleaks detect --source . --config .gitleaks.toml --verbose --exit-code 1 --log-opts="origin/${{ github.base_ref }}..HEAD" || EXIT_CODE=$?
if [ "$EXIT_CODE" = "1" ]; then
echo "ERROR: Secrets detected! Check the scan report above."
echo "If these are false positives, add them to .gitleaks.toml allowlist."
exit 1
fi
else
# Push触发: 全量扫描
echo "Push mode: full repository scan"
EXIT_CODE=0
/tmp/gitleaks detect --source . --config .gitleaks.toml --verbose --exit-code 1 || EXIT_CODE=$?
if [ "$EXIT_CODE" = "1" ]; then
echo "ERROR: Secrets detected! Check the scan report above."
echo "If these are false positives, add them to .gitleaks.toml allowlist."
exit 1
fi
fi
echo "gitleaks scan completed - no secrets detected"
- name: Run code quality checks
shell: sh
run: |
@@ -157,56 +110,12 @@ jobs:
python3 -m isort --check-only alembic apps packages tests scripts
python3 -m flake8 apps packages tests --count --statistics
- name: Run security scan (bandit)
- name: Run security scan
shell: sh
run: |
set -eu
bandit -r apps packages -q -ll
- name: Python dependency vulnerability scan (pip-audit)
shell: sh
run: |
set -eu
echo "=== Installing pip-audit ==="
python3 -m pip install -q pip-audit
pip-audit --version
echo ""
echo "=== Scanning Python dependencies ==="
EXIT_CODE=0
for req_file in requirements.txt requirements-base.txt requirements-dev.txt; do
if [ -f "$req_file" ]; then
echo "--- Scanning $req_file ---"
pip-audit -r "$req_file" --desc on --format text 2>&1 | head -30 || EXIT_CODE=$?
echo ""
fi
done
# 告警模式,不阻断CI(待稳定后再考虑改为阻断)
echo "pip-audit scan completed (advisory mode - warnings only, not blocking CI)"
if [ "$EXIT_CODE" != "0" ]; then
echo "WARNING: Potential vulnerabilities found in dependencies."
fi
exit 0
- name: Dead code detection (vulture)
shell: sh
run: |
set -eu
echo "=== Installing vulture ==="
python3 -m pip install -q vulture
vulture --version
echo ""
echo "=== Running vulture dead code scan ==="
# 告警模式,不阻断CI(P2级别,仅供参考)
EXIT_CODE=0
vulture --config vulture.conf vulture_whitelist.py || EXIT_CODE=$?
echo ""
echo "vulture scan completed (advisory mode - P2, for reference only)"
if [ "$EXIT_CODE" != "0" ]; then
echo "NOTE: Potential dead code found. Review results above."
echo "False positives can be added to vulture_whitelist.py"
fi
exit 0
- name: Validate release scripts syntax
shell: sh
run: |
@@ -255,7 +164,7 @@ jobs:
unit-tests:
name: Unit Tests
runs-on: [host, ci-check]
runs-on: host
timeout-minutes: 8
env:
@@ -373,7 +282,7 @@ jobs:
integration-tests:
name: Integration Tests
runs-on: [host, ci-check]
runs-on: host
timeout-minutes: 20
if: always()
needs: validate
@@ -634,7 +543,7 @@ jobs:
frontend-lint:
name: Frontend Lint
runs-on: [host, ci-check]
runs-on: host
timeout-minutes: 10
steps:
@@ -742,10 +651,10 @@ jobs:
echo "=== CI 失败通知 ==="
FAILED_JOB="Frontend Lint" python3 scripts/ci_notify_failure.py
deploy-staging:
name: Build & Push Staging (Watchtower auto-deploy)
runs-on: [host, build-only]
timeout-minutes: 30
build-staging-api:
name: Build Staging API Image
runs-on: saas
timeout-minutes: 20
needs: [validate, frontend-lint]
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
@@ -795,30 +704,347 @@ jobs:
if member.name:
tar.extract(member, '.')
INNERPY
- name: Build and push all images to Gitea Registry
- name: Docker login to Registry
shell: sh
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
chmod +x scripts/build_release_images.sh
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
scripts/build_release_images.sh "${GITHUB_SHA}" staging
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
# develop/main 分支写回缓存,其他分支只读
if [ "${GITHUB_REF_NAME}" = "develop" ] || [ "${GITHUB_REF_NAME}" = "main" ]; then
echo "CACHE_MODE=read-write" >> $GITHUB_ENV
echo "Cache mode: read-write (will push cache)"
else
echo "CACHE_MODE=read-only" >> $GITHUB_ENV
echo "Cache mode: read-only"
fi
- name: Tag and push :staging images (Watchtower auto-update)
- name: Setup buildx builder (docker-container driver)
shell: sh
run: |
set -eu
# 确保使用 docker-container driver 以支持 cache export 功能
if ! docker buildx inspect ci-builder > /dev/null 2>&1; then
docker buildx create --use --name ci-builder --driver docker-container
echo "Created ci-builder (docker-container driver)"
else
docker buildx use ci-builder
echo "Using existing ci-builder"
fi
docker buildx inspect --bootstrap
- name: Build and push API image (buildx cache)
shell: sh
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
if [ -n "${REGISTRY_TOKEN:-}" ]; then
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null
IMAGE_NAME="xiaoxia-saas-api"
CACHE_REF="${REGISTRY}/api-cache:develop"
CACHE_FROM="type=registry,ref=${CACHE_REF},ignore-error=true"
if [ "${CACHE_MODE}" = "read-write" ]; then
CACHE_TO="type=registry,ref=${CACHE_REF},mode=max"
echo "Building API image with read-write cache..."
docker buildx build --build-arg APP_VERSION="${GITHUB_SHA}" --cache-from "${CACHE_FROM}" --cache-to "${CACHE_TO}" -f infra/docker/api.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}" --push .
else
echo "Building API image with read-only cache..."
docker buildx build --build-arg APP_VERSION="${GITHUB_SHA}" --cache-from "${CACHE_FROM}" -f infra/docker/api.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}" --push .
fi
echo "API image pushed: ${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}"
- name: Notify CI failure
if: failure()
shell: sh
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build Staging API Image" python3 scripts/ci_notify_failure.py
build-staging-worker:
name: Build Staging Worker Image
runs-on: saas
timeout-minutes: 20
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
python3 - <<'INNERPY'
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']}"})
last_err = None
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=120) as response:
archive = response.read()
break
except urllib.error.HTTPError as e:
last_err = e
if e.code >= 500 and attempt < 4:
wait = 2 ** attempt
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
except Exception as e:
last_err = e
if attempt < 4:
wait = 2 ** attempt
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
for member in tar.getmembers():
name = member.name
if name == root_prefix[:-1]:
continue
if name.startswith(root_prefix):
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
INNERPY
- 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
# develop/main 分支写回缓存,其他分支只读
if [ "${GITHUB_REF_NAME}" = "develop" ] || [ "${GITHUB_REF_NAME}" = "main" ]; then
echo "CACHE_MODE=read-write" >> $GITHUB_ENV
echo "Cache mode: read-write (will push cache)"
else
echo "CACHE_MODE=read-only" >> $GITHUB_ENV
echo "Cache mode: read-only"
fi
- name: Setup buildx builder (docker-container driver)
shell: sh
run: |
set -eu
# 确保使用 docker-container driver 以支持 cache export 功能
if ! docker buildx inspect ci-builder > /dev/null 2>&1; then
docker buildx create --use --name ci-builder --driver docker-container
echo "Created ci-builder (docker-container driver)"
else
docker buildx use ci-builder
echo "Using existing ci-builder"
fi
docker buildx inspect --bootstrap
- name: Build and push Worker image (buildx cache)
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
IMAGE_NAME="xiaoxia-saas-worker"
CACHE_REF="${REGISTRY}/worker-cache:develop"
CACHE_FROM="type=registry,ref=${CACHE_REF},ignore-error=true"
if [ "${CACHE_MODE}" = "read-write" ]; then
CACHE_TO="type=registry,ref=${CACHE_REF},mode=max"
echo "Building Worker image with read-write cache..."
docker buildx build --build-arg APP_VERSION="${GITHUB_SHA}" --cache-from "${CACHE_FROM}" --cache-to "${CACHE_TO}" -f infra/docker/worker.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}" --push .
else
echo "Building Worker image with read-only cache..."
docker buildx build --build-arg APP_VERSION="${GITHUB_SHA}" --cache-from "${CACHE_FROM}" -f infra/docker/worker.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}" --push .
fi
echo "Worker image pushed: ${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}"
- name: Notify CI failure
if: failure()
shell: sh
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build Staging Worker Image" python3 scripts/ci_notify_failure.py
build-staging-web:
name: Build Staging Web Image
runs-on: saas
timeout-minutes: 20
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
python3 - <<'INNERPY'
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']}"})
last_err = None
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=120) as response:
archive = response.read()
break
except urllib.error.HTTPError as e:
last_err = e
if e.code >= 500 and attempt < 4:
wait = 2 ** attempt
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
except Exception as e:
last_err = e
if attempt < 4:
wait = 2 ** attempt
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
for member in tar.getmembers():
name = member.name
if name == root_prefix[:-1]:
continue
if name.startswith(root_prefix):
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
INNERPY
- 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
# develop/main 分支写回缓存,其他分支只读
if [ "${GITHUB_REF_NAME}" = "develop" ] || [ "${GITHUB_REF_NAME}" = "main" ]; then
echo "CACHE_MODE=read-write" >> $GITHUB_ENV
echo "Cache mode: read-write (will push cache)"
else
echo "CACHE_MODE=read-only" >> $GITHUB_ENV
echo "Cache mode: read-only"
fi
- name: Build frontend assets (npm build)
shell: sh
run: |
set -eu
NPM_CACHE_VOLUME="xiaoxia-npm-cache"
if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then
docker volume create "$NPM_CACHE_VOLUME" >/dev/null
echo "Created npm cache volume: $NPM_CACHE_VOLUME"
fi
docker 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 && npm run build"
test -f apps/web/dist/index.html
echo "Frontend build complete: $(ls apps/web/dist/ | head -5)"
- name: Setup buildx builder (docker-container driver)
shell: sh
run: |
set -eu
# 确保使用 docker-container driver 以支持 cache export 功能
if ! docker buildx inspect ci-builder > /dev/null 2>&1; then
docker buildx create --use --name ci-builder --driver docker-container
echo "Created ci-builder (docker-container driver)"
else
docker buildx use ci-builder
echo "Using existing ci-builder"
fi
docker buildx inspect --bootstrap
- name: Build and push Web image (buildx cache)
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
IMAGE_NAME="xiaoxia-saas-web"
CACHE_REF="${REGISTRY}/web-cache:develop"
NGINX_CONF="infra/docker/nginx-staging.conf"
CACHE_FROM="type=registry,ref=${CACHE_REF},ignore-error=true"
if [ "${CACHE_MODE}" = "read-write" ]; then
CACHE_TO="type=registry,ref=${CACHE_REF},mode=max"
echo "Building Web image with read-write cache..."
docker buildx build --cache-from "${CACHE_FROM}" --cache-to "${CACHE_TO}" -f infra/docker/web-artifact.Dockerfile --build-arg "NGINX_CONF=${NGINX_CONF}" -t "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}" --push .
else
echo "Building Web image with read-only cache..."
docker buildx build --cache-from "${CACHE_FROM}" -f infra/docker/web-artifact.Dockerfile --build-arg "NGINX_CONF=${NGINX_CONF}" -t "${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}" --push .
fi
echo "Web image pushed: ${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}"
- name: Notify CI failure
if: failure()
shell: sh
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build Staging Web Image" python3 scripts/ci_notify_failure.py
deploy-staging:
name: Deploy Staging (Watchtower auto-deploy)
runs-on: saas
timeout-minutes: 15
needs: [build-staging-api, build-staging-worker, build-staging-web]
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
steps:
- 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: Tag and push :staging images (Watchtower auto-update)
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
for svc in api worker web; do
echo "Pulling ${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA} ..."
docker pull "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}"
docker tag "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}" "${REGISTRY}/xiaoxia-saas-${svc}:staging"
docker push "${REGISTRY}/xiaoxia-saas-${svc}:staging"
echo "$svc :staging tagged and pushed"
done
echo "All :staging images pushed. Watchtower will auto-deploy within 60s."
@@ -887,13 +1113,12 @@ jobs:
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build & Push Staging (Watchtower auto-deploy)" python3 scripts/ci_notify_failure.py
FAILED_JOB="Deploy Staging" python3 scripts/ci_notify_failure.py
staging-e2e:
name: Staging E2E Tests
runs-on: [host, build-only]
runs-on: saas
timeout-minutes: 15
if: github.ref_name == 'develop' || github.ref_name == 'main'
needs: deploy-staging
@@ -969,7 +1194,7 @@ jobs:
staging-api-tests:
name: Staging API Integration Tests
runs-on: [host, build-only]
runs-on: saas
timeout-minutes: 10
if: github.ref_name == 'develop' || github.ref_name == 'main'
needs: deploy-staging
@@ -1042,10 +1267,10 @@ jobs:
build-production-runtime-images:
name: Build Production Runtime Images
runs-on: [host, build-only]
timeout-minutes: 30
build-production-api:
name: Build Production API Image
runs-on: saas
timeout-minutes: 20
needs: [validate, frontend-lint]
if: startsWith(github.ref, 'refs/tags/v')
@@ -1057,7 +1282,7 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -eu
python3 - <<'PY'
python3 - <<'INNERPY'
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']}"})
@@ -1082,7 +1307,6 @@ jobs:
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
@@ -1095,16 +1319,251 @@ jobs:
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
PY
- name: Build and push all images (api + worker + web, with buildx cache)
INNERPY
- name: Docker login to Registry
shell: sh
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
chmod +x scripts/build_release_images.sh
REGISTRY_TOKEN="${REGISTRY_TOKEN}" scripts/build_release_images.sh "${GITHUB_REF_NAME}"
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
# 确保使用 docker-container driver 以支持 cache export 功能
if ! docker buildx inspect ci-builder > /dev/null 2>&1; then
docker buildx create --use --name ci-builder --driver docker-container
echo "Created ci-builder (docker-container driver)"
else
docker buildx use ci-builder
echo "Using existing ci-builder"
fi
docker buildx inspect --bootstrap
- name: Build and push API image (buildx cache)
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
IMAGE_NAME="xiaoxia-saas-api"
VERSION="${GITHUB_REF_NAME}"
CACHE_REF="${REGISTRY}/api-cache:main"
echo "Building Production API image: ${VERSION}"
docker buildx build --build-arg APP_VERSION="${VERSION}" --cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" --cache-to "type=registry,ref=${CACHE_REF},mode=max" -f infra/docker/api.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --push .
echo "Production API image pushed: ${REGISTRY}/${IMAGE_NAME}:${VERSION}"
- name: Notify CI failure
if: failure()
shell: sh
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build Production API Image" python3 scripts/ci_notify_failure.py
build-production-worker:
name: Build Production Worker Image
runs-on: saas
timeout-minutes: 20
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
python3 - <<'INNERPY'
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']}"})
last_err = None
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=120) as response:
archive = response.read()
break
except urllib.error.HTTPError as e:
last_err = e
if e.code >= 500 and attempt < 4:
wait = 2 ** attempt
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
except Exception as e:
last_err = e
if attempt < 4:
wait = 2 ** attempt
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
for member in tar.getmembers():
name = member.name
if name == root_prefix[:-1]:
continue
if name.startswith(root_prefix):
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
INNERPY
- 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
# 确保使用 docker-container driver 以支持 cache export 功能
if ! docker buildx inspect ci-builder > /dev/null 2>&1; then
docker buildx create --use --name ci-builder --driver docker-container
echo "Created ci-builder (docker-container driver)"
else
docker buildx use ci-builder
echo "Using existing ci-builder"
fi
docker buildx inspect --bootstrap
- name: Build and push Worker image (buildx cache)
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
IMAGE_NAME="xiaoxia-saas-worker"
VERSION="${GITHUB_REF_NAME}"
CACHE_REF="${REGISTRY}/worker-cache:main"
echo "Building Production Worker image: ${VERSION}"
docker buildx build --build-arg APP_VERSION="${VERSION}" --cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" --cache-to "type=registry,ref=${CACHE_REF},mode=max" -f infra/docker/worker.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --push .
echo "Production Worker image pushed: ${REGISTRY}/${IMAGE_NAME}:${VERSION}"
- name: Notify CI failure
if: failure()
shell: sh
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build Production Worker Image" python3 scripts/ci_notify_failure.py
build-production-web:
name: Build Production Web Image
runs-on: saas
timeout-minutes: 20
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
python3 - <<'INNERPY'
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']}"})
last_err = None
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=120) as response:
archive = response.read()
break
except urllib.error.HTTPError as e:
last_err = e
if e.code >= 500 and attempt < 4:
wait = 2 ** attempt
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
except Exception as e:
last_err = e
if attempt < 4:
wait = 2 ** attempt
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
for member in tar.getmembers():
name = member.name
if name == root_prefix[:-1]:
continue
if name.startswith(root_prefix):
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
INNERPY
- 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: Build frontend assets (npm build)
shell: sh
run: |
set -eu
NPM_CACHE_VOLUME="xiaoxia-npm-cache"
if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then
docker volume create "$NPM_CACHE_VOLUME" >/dev/null
fi
docker 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 && npm run build"
test -f apps/web/dist/index.html
echo "Frontend build complete"
- name: Setup buildx builder (docker-container driver)
shell: sh
run: |
set -eu
# 确保使用 docker-container driver 以支持 cache export 功能
if ! docker buildx inspect ci-builder > /dev/null 2>&1; then
docker buildx create --use --name ci-builder --driver docker-container
echo "Created ci-builder (docker-container driver)"
else
docker buildx use ci-builder
echo "Using existing ci-builder"
fi
docker buildx inspect --bootstrap
- name: Build and push Web image (buildx cache)
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
IMAGE_NAME="xiaoxia-saas-web"
VERSION="${GITHUB_REF_NAME}"
CACHE_REF="${REGISTRY}/web-cache:main"
NGINX_CONF="infra/docker/nginx-production.conf"
echo "Building Production Web image: ${VERSION}"
docker buildx build --cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" --cache-to "type=registry,ref=${CACHE_REF},mode=max" -f infra/docker/web-artifact.Dockerfile --build-arg "NGINX_CONF=${NGINX_CONF}" -t "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --push .
echo "Production Web image pushed: ${REGISTRY}/${IMAGE_NAME}:${VERSION}"
- name: Cleanup old Docker images
if: always()
@@ -1127,15 +1586,14 @@ jobs:
run: |
set +e
echo "=== CI 失败通知 ==="
FAILED_JOB="Build Production Runtime Images" python3 scripts/ci_notify_failure.py
FAILED_JOB="Build Production Web Image" python3 scripts/ci_notify_failure.py
deploy-production:
name: Deploy Production
runs-on: [host, build-only]
runs-on: saas
timeout-minutes: 20
if: startsWith(github.ref, 'refs/tags/v')
needs: build-production-runtime-images
needs: [build-production-api, build-production-worker, build-production-web]
steps:
- name: Install SSH client
@@ -1214,7 +1672,7 @@ jobs:
production-e2e:
name: Production Browser E2E
runs-on: [host, build-only]
runs-on: saas
timeout-minutes: 15
if: startsWith(github.ref, 'refs/tags/v')
needs: deploy-production