Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7941654224 | |||
| c34c1634fc | |||
| cf881c6ba3 |
+588
-130
@@ -26,7 +26,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
name: Validate Code Quality And Tests
|
name: Validate Code Quality And Tests
|
||||||
runs-on: [host, ci-check]
|
runs-on: host
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -101,53 +101,6 @@ jobs:
|
|||||||
bandit --version
|
bandit --version
|
||||||
pytest --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
|
- name: Run code quality checks
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
@@ -157,56 +110,12 @@ jobs:
|
|||||||
python3 -m isort --check-only alembic apps packages tests scripts
|
python3 -m isort --check-only alembic apps packages tests scripts
|
||||||
python3 -m flake8 apps packages tests --count --statistics
|
python3 -m flake8 apps packages tests --count --statistics
|
||||||
|
|
||||||
- name: Run security scan (bandit)
|
- name: Run security scan
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
bandit -r apps packages -q -ll
|
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
|
- name: Validate release scripts syntax
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
@@ -255,7 +164,7 @@ jobs:
|
|||||||
|
|
||||||
unit-tests:
|
unit-tests:
|
||||||
name: Unit Tests
|
name: Unit Tests
|
||||||
runs-on: [host, ci-check]
|
runs-on: host
|
||||||
timeout-minutes: 8
|
timeout-minutes: 8
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -373,7 +282,7 @@ jobs:
|
|||||||
|
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Integration Tests
|
name: Integration Tests
|
||||||
runs-on: [host, ci-check]
|
runs-on: host
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
if: always()
|
if: always()
|
||||||
needs: validate
|
needs: validate
|
||||||
@@ -634,7 +543,7 @@ jobs:
|
|||||||
|
|
||||||
frontend-lint:
|
frontend-lint:
|
||||||
name: Frontend Lint
|
name: Frontend Lint
|
||||||
runs-on: [host, ci-check]
|
runs-on: host
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -742,10 +651,10 @@ jobs:
|
|||||||
echo "=== CI 失败通知 ==="
|
echo "=== CI 失败通知 ==="
|
||||||
FAILED_JOB="Frontend Lint" python3 scripts/ci_notify_failure.py
|
FAILED_JOB="Frontend Lint" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
deploy-staging:
|
build-staging-api:
|
||||||
name: Build & Push Staging (Watchtower auto-deploy)
|
name: Build Staging API Image
|
||||||
runs-on: [host, build-only]
|
runs-on: saas
|
||||||
timeout-minutes: 30
|
timeout-minutes: 20
|
||||||
needs: [validate, frontend-lint]
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
||||||
@@ -795,30 +704,347 @@ jobs:
|
|||||||
if member.name:
|
if member.name:
|
||||||
tar.extract(member, '.')
|
tar.extract(member, '.')
|
||||||
INNERPY
|
INNERPY
|
||||||
|
- name: Docker login to Registry
|
||||||
- name: Build and push all images to Gitea Registry
|
|
||||||
shell: sh
|
shell: sh
|
||||||
env:
|
env:
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
chmod +x scripts/build_release_images.sh
|
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin
|
||||||
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
echo "Docker login successful"
|
||||||
scripts/build_release_images.sh "${GITHUB_SHA}" staging
|
- 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
|
shell: sh
|
||||||
env:
|
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
|
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
|
||||||
if [ -n "${REGISTRY_TOKEN:-}" ]; then
|
IMAGE_NAME="xiaoxia-saas-api"
|
||||||
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null
|
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
|
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
|
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 tag "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}" "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
||||||
docker push "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
docker push "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
||||||
|
echo "$svc :staging tagged and pushed"
|
||||||
done
|
done
|
||||||
echo "All :staging images pushed. Watchtower will auto-deploy within 60s."
|
echo "All :staging images pushed. Watchtower will auto-deploy within 60s."
|
||||||
|
|
||||||
@@ -887,13 +1113,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
echo "=== CI 失败通知 ==="
|
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:
|
staging-e2e:
|
||||||
name: Staging E2E Tests
|
name: Staging E2E Tests
|
||||||
runs-on: [host, build-only]
|
runs-on: saas
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
if: github.ref_name == 'develop' || github.ref_name == 'main'
|
if: github.ref_name == 'develop' || github.ref_name == 'main'
|
||||||
needs: deploy-staging
|
needs: deploy-staging
|
||||||
@@ -969,7 +1194,7 @@ jobs:
|
|||||||
|
|
||||||
staging-api-tests:
|
staging-api-tests:
|
||||||
name: Staging API Integration Tests
|
name: Staging API Integration Tests
|
||||||
runs-on: [host, build-only]
|
runs-on: saas
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
if: github.ref_name == 'develop' || github.ref_name == 'main'
|
if: github.ref_name == 'develop' || github.ref_name == 'main'
|
||||||
needs: deploy-staging
|
needs: deploy-staging
|
||||||
@@ -1042,10 +1267,10 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-production-runtime-images:
|
build-production-api:
|
||||||
name: Build Production Runtime Images
|
name: Build Production API Image
|
||||||
runs-on: [host, build-only]
|
runs-on: saas
|
||||||
timeout-minutes: 30
|
timeout-minutes: 20
|
||||||
needs: [validate, frontend-lint]
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
@@ -1057,7 +1282,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
python3 - <<'PY'
|
python3 - <<'INNERPY'
|
||||||
import io, os, tarfile, time, urllib.request, urllib.error
|
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"
|
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']}"})
|
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)...")
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
continue
|
continue
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
raise last_err
|
raise last_err
|
||||||
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||||
@@ -1095,16 +1319,251 @@ jobs:
|
|||||||
member.name = name[len(root_prefix):]
|
member.name = name[len(root_prefix):]
|
||||||
if member.name:
|
if member.name:
|
||||||
tar.extract(member, '.')
|
tar.extract(member, '.')
|
||||||
PY
|
INNERPY
|
||||||
|
- name: Docker login to Registry
|
||||||
- name: Build and push all images (api + worker + web, with buildx cache)
|
|
||||||
shell: sh
|
shell: sh
|
||||||
env:
|
env:
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
chmod +x scripts/build_release_images.sh
|
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin
|
||||||
REGISTRY_TOKEN="${REGISTRY_TOKEN}" scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
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
|
- name: Cleanup old Docker images
|
||||||
if: always()
|
if: always()
|
||||||
@@ -1127,15 +1586,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
echo "=== CI 失败通知 ==="
|
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:
|
deploy-production:
|
||||||
name: Deploy Production
|
name: Deploy Production
|
||||||
runs-on: [host, build-only]
|
runs-on: saas
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: build-production-runtime-images
|
needs: [build-production-api, build-production-worker, build-production-web]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install SSH client
|
- name: Install SSH client
|
||||||
@@ -1214,7 +1672,7 @@ jobs:
|
|||||||
|
|
||||||
production-e2e:
|
production-e2e:
|
||||||
name: Production Browser E2E
|
name: Production Browser E2E
|
||||||
runs-on: [host, build-only]
|
runs-on: saas
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: deploy-production
|
needs: deploy-production
|
||||||
|
|||||||
Reference in New Issue
Block a user