Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7941654224 | |||
| c34c1634fc | |||
| cf881c6ba3 |
+588
-130
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user