|
|
|
@@ -651,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)
|
|
|
|
|
build-staging-api:
|
|
|
|
|
name: Build Staging API Image
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 30
|
|
|
|
|
timeout-minutes: 20
|
|
|
|
|
needs: [validate, frontend-lint]
|
|
|
|
|
|
|
|
|
|
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
|
|
|
@@ -704,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."
|
|
|
|
|
|
|
|
|
@@ -796,8 +1113,7 @@ 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:
|
|
|
|
@@ -951,10 +1267,10 @@ jobs:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
build-production-runtime-images:
|
|
|
|
|
name: Build Production Runtime Images
|
|
|
|
|
build-production-api:
|
|
|
|
|
name: Build Production API Image
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 30
|
|
|
|
|
timeout-minutes: 20
|
|
|
|
|
needs: [validate, frontend-lint]
|
|
|
|
|
|
|
|
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
|
|
@@ -966,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']}"})
|
|
|
|
@@ -991,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:
|
|
|
|
@@ -1004,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()
|
|
|
|
@@ -1036,63 +1586,16 @@ 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: 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: Checkout code
|
|
|
|
|
shell: sh
|
|
|
|
|
env:
|
|
|
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
|
|
|
run: |
|
|
|
|
|
set -eu
|
|
|
|
|
python3 - <<'PY'
|
|
|
|
|
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
|
|
|
|
|
raise
|
|
|
|
|
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, '.')
|
|
|
|
|
PY
|
|
|
|
|
|
|
|
|
|
- name: Install SSH client
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
@@ -1127,16 +1630,10 @@ jobs:
|
|
|
|
|
key_path="$HOME/.ssh/xiaoxia_runtime_builder"
|
|
|
|
|
echo "Using key: $key_path (home key)"
|
|
|
|
|
elif [ -n "${PRODUCTION_SSH_KEY:-}" ]; then
|
|
|
|
|
key_path="$HOME/.ssh/production_deploy_key"
|
|
|
|
|
key_path="$HOME/.ssh/id_ed25519"
|
|
|
|
|
printf '%s\n' "$PRODUCTION_SSH_KEY" > "$key_path"
|
|
|
|
|
chmod 600 "$key_path"
|
|
|
|
|
echo "Using key from PRODUCTION_SSH_KEY secret"
|
|
|
|
|
elif [ -f "$HOME/.ssh/id_ed25519" ]; then
|
|
|
|
|
key_path="$HOME/.ssh/id_ed25519"
|
|
|
|
|
echo "Using key: $key_path (default id_ed25519)"
|
|
|
|
|
elif [ -f /root/.ssh/id_ed25519 ]; then
|
|
|
|
|
key_path="/root/.ssh/id_ed25519"
|
|
|
|
|
echo "Using key: $key_path (root id_ed25519)"
|
|
|
|
|
else
|
|
|
|
|
echo "ERROR: No SSH key available"
|
|
|
|
|
ls -la ~/.ssh/ 2>/dev/null || true
|
|
|
|
|