ci: PR构建验证 + Agent提交格式化 + buildx清理 #690

Merged
auto-approve-bot merged 2 commits from ci/pr-build-validation-plus into develop 2026-07-22 00:26:52 +08:00
4 changed files with 314 additions and 0 deletions
+128
View File
@@ -416,6 +416,125 @@ jobs:
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
build-pr:
name: PR Build ${{ matrix.service_display }} Image
runs-on: runtime-builder
timeout-minutes: ${{ matrix.timeout }}
if: github.event_name == 'pull_request'
strategy:
fail-fast: false
matrix:
include:
- service: api
service_display: API
dockerfile: infra/docker/api.Dockerfile
image_name: xiaoxia-saas-api
cache_name: api-cache
timeout: 30
- service: worker
service_display: Worker
dockerfile: infra/docker/worker.Dockerfile
image_name: xiaoxia-saas-worker
cache_name: worker-cache
timeout: 40
- service: web
service_display: Web
dockerfile: infra/docker/web.Dockerfile
image_name: xiaoxia-saas-web
cache_name: web-cache
timeout: 30
steps:
- name: Checkout code
shell: sh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash
- name: Record job start time
shell: sh
run: bash scripts/ci/step_timer_start.sh
- name: Docker login to Registry (for cache read)
shell: sh
env:
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
GITEA_REGISTRY_USER: xiaoxia
GITEA_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
for i in 1 2 3; do
echo "Docker login attempt $i/3"
if printf '%s' "${ACR_PASSWORD}" | docker login xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com -u "${ACR_USERNAME}" --password-stdin && docker login git.xiaoxiajianji.com -u "${GITEA_REGISTRY_USER}" -p "${GITEA_REGISTRY_TOKEN}"; then
echo "Docker login successful"
break
fi
echo "Docker login failed ($i/3), retrying in 5s..."
sleep 5
done
- name: Build PR image (verify only, no push)
shell: sh
run: |
set -eu
REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji"
IMAGE_TAG="${REGISTRY}/${{ matrix.image_name }}:pr-${GITHUB_SHA}"
CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:develop"
EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\""
if [ "${{ matrix.service }}" = "web" ]; then
EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf"
fi
NO_CACHE_FLAG=""
for i in 1 2 3; do
echo "PR Build attempt $i/3"
if bash scripts/ci/docker_build_only.sh $NO_CACHE_FLAG ${{ matrix.dockerfile }} "${IMAGE_TAG}" "${CACHE_REF}" $EXTRA_BUILD_ARGS; then
echo "PR Build successful"
break
fi
echo "PR Build failed (attempt $i/3)"
[ $i -eq 3 ] && exit 1
sleep 10
if [ $i -eq 2 ]; then
NO_CACHE_FLAG="--no-cache"
echo "Next retry with --no-cache"
fi
done
echo
echo "${{ matrix.service_display }} PR build verified: ${IMAGE_TAG}"
- name: Cleanup buildx builder
if: always()
shell: sh
run: |
BUILDER_NAME="ci-pr-builder-${GITHUB_RUN_ID:-local}"
docker buildx rm "$BUILDER_NAME" 2>/dev/null || true
docker buildx prune -f 2>/dev/null || true
echo "Builder cleanup done"
- name: Job duration summary
if: always()
shell: sh
run: bash scripts/ci/step_timer_end.sh
- name: Notify on failure
continue-on-error: true
if: failure()
shell: sh
env:
CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }}
run: |
set +e
NOTIFY_MODE=failure JOB_NAME="PR Build ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py
- name: Report CI trace
if: always()
shell: sh
env:
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
run: |
STATUS="ok"
[ ${{ job.status }} = "success" ] || STATUS="error"
START_TIME=""
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
build-staging:
name: Build Staging ${{ matrix.service_display }} Image
runs-on: runtime-builder
@@ -530,6 +649,15 @@ jobs:
echo
echo "${{ matrix.service_display }} image pushed: ${IMAGE_TAG}"
- name: Cleanup buildx builder
if: always()
shell: sh
run: |
docker buildx rm ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} 2>/dev/null || true
docker buildx rm ci-builder 2>/dev/null || true
docker buildx prune -f 2>/dev/null || true
echo "Builder cleanup done"
- name: Job duration summary
if: always()
shell: sh
+3
View File
@@ -235,6 +235,9 @@ jobs:
CONTEXTS=(
"CI/CD Pipeline / Validate Code Quality And Tests (pull_request)"
"CI/CD Pipeline / Frontend Lint (pull_request)"
"CI/CD Pipeline / PR Build API Image (pull_request)"
"CI/CD Pipeline / PR Build Worker Image (pull_request)"
"CI/CD Pipeline / PR Build Web Image (pull_request)"
)
echo "检查required门禁(与分支保护一致)"
fi
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
# Agent代码提交前自动格式化+质量检查脚本
# 用法: scripts/agent-commit.sh <commit_message> [files...]
# 效果: 自动跑black+isort+ruff check,通过后才commit+push
set -e
if [ $# -lt 1 ]; then
echo "用法: $0 <commit_message> [file1 file2 ...]"
echo "示例: $0 \"feat: add new api\" apps/api/src/"
exit 1
fi
COMMIT_MSG="$1"
shift
TARGETS="${@:-.}"
cd "$(dirname "$0")/.."
REPO_ROOT=$(pwd)
echo "仓库根目录: $REPO_ROOT"
echo "提交信息: $COMMIT_MSG"
echo "目标路径: $TARGETS"
echo ""
# 后端代码格式化(Python文件)
PYTHON_FILES=$(find $TARGETS -name "*.py" -type f 2>/dev/null | head -100 || true)
if [ -n "$PYTHON_FILES" ]; then
echo "=== Step 1/4: 后端代码格式化 (black) ==="
if command -v black &> /dev/null; then
black $TARGETS 2>&1 | tail -3
echo "✅ black 完成"
else
echo "⚠️ 未安装black,跳过"
fi
echo ""
echo "=== Step 2/4: import排序 (isort) ==="
if command -v isort &> /dev/null; then
isort $TARGETS 2>&1 | tail -3
echo "✅ isort 完成"
else
echo "⚠️ 未安装isort,跳过"
fi
echo ""
echo "=== Step 3/4: 代码质量检查 (ruff check) ==="
if command -v ruff &> /dev/null; then
RUFF_OUTPUT=$(ruff check $TARGETS 2>&1) || true
RUFF_ERRORS=$(echo "$RUFF_OUTPUT" | grep -c "^" || echo 0)
if [ "$RUFF_ERRORS" -le 2 ] || echo "$RUFF_OUTPUT" | grep -q "All checks passed"; then
echo "✅ ruff 检查通过(错误数: $RUFF_ERRORS"
else
echo "❌ ruff 发现以下问题:"
echo "$RUFF_OUTPUT" | head -30
echo ""
echo "请修复后重新提交,或手动忽略特定问题"
exit 1
fi
else
echo "⚠️ 未安装ruff,跳过"
fi
echo ""
else
echo "ℹ️ 未检测到Python文件,跳过后端格式化"
echo ""
fi
# 前端代码格式化(TS/TSX文件)
TS_FILES=$(find $TARGETS -name "*.ts" -o -name "*.tsx" -type f 2>/dev/null | head -100 || true)
if [ -n "$TS_FILES" ] && [ -f "apps/web/package.json" ]; then
echo "=== Step 4/4: 前端代码格式化 (prettier) ==="
if command -v npx &> /dev/null; then
cd apps/web && npx prettier --write "src/**/*.{ts,tsx}" 2>&1 | tail -3 || true
cd "$REPO_ROOT"
echo "✅ prettier 完成"
else
echo "⚠️ 未安装npx,跳过前端格式化"
fi
echo ""
fi
# Git操作
echo "=== 提交代码 ==="
git add -A
git diff --cached --stat
echo ""
git commit -m "$COMMIT_MSG"
echo ""
echo "✅ 本地提交完成"
# 可选:自动推送
if [ "$AGENT_AUTO_PUSH" = "true" ]; then
echo "正在推送到远程..."
git push
echo "✅ 推送完成"
else
echo "ℹ️ 本地已提交,如需推送执行: git push"
echo " 设置 AGENT_AUTO_PUSH=true 可自动推送"
fi
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# PR构建专用:只构建不推送,只读缓存不写,用于PR阶段验证Dockerfile
set -eu
NO_CACHE_FLAG=""
if [ "$1" = "--no-cache" ]; then
NO_CACHE_FLAG="--no-cache"
shift
fi
DOCKERFILE="$1"
IMAGE_TAG="$2"
CACHE_REF="$3"
shift 3
BUILD_ARGS=""
for arg in "$@"; do
BUILD_ARGS="$BUILD_ARGS --build-arg $arg"
done
BUILDER_NAME="ci-pr-builder-${GITHUB_RUN_ID:-local}"
if ! docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then
docker buildx create --use --name "$BUILDER_NAME" --driver docker-container
else
docker buildx use "$BUILDER_NAME"
fi
docker buildx inspect --bootstrap
CACHE_NAME=$(echo "$CACHE_REF" | tr "/" "_" | tr ":" "-")
LOCAL_CACHE_DIR="/tmp/buildx-cache/${CACHE_NAME}"
mkdir -p "$LOCAL_CACHE_DIR"
echo "=== PR Build: build only, no push, read-only cache ==="
echo "Dockerfile: ${DOCKERFILE}"
echo "Image tag: ${IMAGE_TAG}"
echo ""
build_with_retry() {
local attempt=1
local max_attempts=2
while [ $attempt -le $max_attempts ]; do
local build_output
local exit_code
set +e
build_output=$(docker buildx build \
$NO_CACHE_FLAG \
$BUILD_ARGS \
--cache-from "type=local,src=${LOCAL_CACHE_DIR}" \
--cache-from "type=registry,ref=${CACHE_REF}" \
-f "${DOCKERFILE}" \
-t "${IMAGE_TAG}" \
--load \
. 2>&1)
exit_code=$?
set -e
if [ $exit_code -eq 0 ]; then
echo "$build_output"
return 0
fi
if echo "$build_output" | grep -qE "parent snapshot.*not found|snapshot.*does not exist|cache.*corrupt|failed to compute cache key"; then
echo "$build_output"
echo "Local cache corrupted, cleaning and retrying ($attempt/$max_attempts)..."
rm -rf "${LOCAL_CACHE_DIR}"
mkdir -p "${LOCAL_CACHE_DIR}"
docker buildx prune -f -a >/dev/null 2>&1 || true
attempt=$((attempt + 1))
else
echo "$build_output"
return $exit_code
fi
done
echo "Local cache failed, building with registry cache only..."
docker buildx build \
$NO_CACHE_FLAG \
$BUILD_ARGS \
--cache-from "type=registry,ref=${CACHE_REF}" \
-f "${DOCKERFILE}" \
-t "${IMAGE_TAG}" \
--load \
.
}
build_with_retry
echo ""
echo "PR build OK (not pushed): ${IMAGE_TAG}"