ci: PR构建验证 + Agent提交格式化 + buildx清理 #690
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+99
@@ -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
|
||||
Executable
+84
@@ -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}"
|
||||
Reference in New Issue
Block a user