Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd3d366f4a | |||
| 4749071c16 | |||
| 3353865f5b | |||
| 7061d7e672 | |||
| 6efdfbe194 |
@@ -47,22 +47,33 @@ jobs:
|
||||
id: protected_images
|
||||
if: gitea.event_name != 'pull_request_target' && !gitea.event.inputs.pr_sha
|
||||
env:
|
||||
STAGING_SSH_KEY: ${{ secrets.PREVIEW_SSH_KEY }}
|
||||
STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }}
|
||||
STAGING_SSH_PORT: ${{ secrets.STAGING_SSH_PORT }}
|
||||
STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }}
|
||||
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
||||
run: |
|
||||
set +e
|
||||
echo "获取staging服务器运行中镜像作为白名单..."
|
||||
mkdir -p ~/.ssh
|
||||
echo "$STAGING_SSH_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
staging_host="${STAGING_SSH_HOST:-47.98.113.167}"
|
||||
staging_port="${STAGING_SSH_PORT:-22222}"
|
||||
staging_user="${STAGING_SSH_USER:-root}"
|
||||
|
||||
key_path=~/.ssh/id_rsa
|
||||
if [ -n "${STAGING_SSH_KEY:-}" ]; then
|
||||
printf '%s\n' "$STAGING_SSH_KEY" > "$key_path"
|
||||
chmod 600 "$key_path"
|
||||
echo "Using key from STAGING_SSH_KEY secret"
|
||||
else
|
||||
echo "⚠️ STAGING_SSH_KEY not set, skipping whitelist"
|
||||
echo "protected_tags=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh-keyscan -p "$staging_port" -H "$staging_host" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# 获取所有运行容器的镜像,提取tag部分
|
||||
IMAGES=$(ssh -p "$staging_port" -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no \
|
||||
"root@$staging_host" "docker ps --format '{{.Image}}' 2>/dev/null" 2>/dev/null | grep -v "^$" | sort -u)
|
||||
IMAGES=$(ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no \
|
||||
"$staging_user@$staging_host" "docker ps --format '{{.Image}}' 2>/dev/null" 2>/dev/null | grep -v "^$" | sort -u)
|
||||
|
||||
PROTECTED_TAGS=""
|
||||
if [ -n "$IMAGES" ]; then
|
||||
@@ -80,7 +91,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "staging运行中镜像tag: ${PROTECTED_TAGS:-(无)}"
|
||||
echo "protected_tags=$PROTECTED_TAGS" >> $GITEA_OUTPUT
|
||||
echo "protected_tags=$PROTECTED_TAGS" >> $GITHUB_OUTPUT
|
||||
|
||||
# ====== Docker登录 ======
|
||||
- name: Docker login to ACR
|
||||
|
||||
@@ -458,6 +458,8 @@ jobs:
|
||||
name: Frontend Lint
|
||||
runs-on: ci-l2
|
||||
timeout-minutes: 10
|
||||
needs: check-frontend-only
|
||||
if: always() && needs.check-frontend-only.outputs.skip_frontend != 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
shell: sh
|
||||
@@ -572,11 +574,12 @@ jobs:
|
||||
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
|
||||
build-pr-backend:
|
||||
name: PR Build ${{ matrix.service_display }} Image (Backend)
|
||||
runs-on: runtime-builder
|
||||
timeout-minutes: ${{ matrix.timeout }}
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: check-frontend-only
|
||||
if: always() && github.event_name == 'pull_request' && needs.check-frontend-only.outputs.skip_backend != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -593,6 +596,174 @@ jobs:
|
||||
image_name: xiaoxia-saas-worker
|
||||
cache_name: worker-cache
|
||||
timeout: 40
|
||||
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: Pre-build worker base images (fallback if not exist)
|
||||
if: matrix.service == 'worker'
|
||||
id: prebuild
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
REGISTRY="git.xiaoxiajianji.com/xiaoxia-saas"
|
||||
BASE_BUILDER="${REGISTRY}/worker-base-builder:latest"
|
||||
BASE_RUNTIME="${REGISTRY}/worker-base-runtime:latest"
|
||||
|
||||
# 尝试拉取基础镜像
|
||||
echo "检查基础镜像..."
|
||||
if docker pull "$BASE_BUILDER" 2>/dev/null && docker pull "$BASE_RUNTIME" 2>/dev/null; then
|
||||
echo "基础镜像已存在,使用远程镜像"
|
||||
echo "fallback=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "基础镜像不存在,本地构建(fallback模式)..."
|
||||
|
||||
# 尝试用buildx构建,失败则回退到普通docker build(DooD模式下buildx builder偶发崩溃)
|
||||
BUILDER_NAME="ci-pr-builder-${GITHUB_RUN_ID:-local}"
|
||||
BUILDX_AVAILABLE=true
|
||||
if ! docker buildx create --use --name "$BUILDER_NAME" --driver docker-container > /dev/null 2>&1; then
|
||||
BUILDX_AVAILABLE=false
|
||||
fi
|
||||
if [ "$BUILDX_AVAILABLE" = true ] && ! docker buildx inspect --bootstrap > /dev/null 2>&1; then
|
||||
BUILDX_AVAILABLE=false
|
||||
docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
build_base() {
|
||||
local df="$1"
|
||||
local tag="$2"
|
||||
local name="$3"
|
||||
if [ "$BUILDX_AVAILABLE" = true ]; then
|
||||
echo "构建 $name(buildx)..."
|
||||
if docker buildx build --load -f "$df" -t "$tag" . > /dev/null 2>&1; then
|
||||
echo "$name 构建成功"
|
||||
return 0
|
||||
fi
|
||||
echo "buildx失败,回退到普通docker build"
|
||||
BUILDX_AVAILABLE=false
|
||||
docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true
|
||||
fi
|
||||
echo "构建 $name(docker build)..."
|
||||
docker build -f "$df" -t "$tag" .
|
||||
}
|
||||
|
||||
build_base infra/docker/worker-base-builder.Dockerfile "$BASE_BUILDER" "worker-base-builder"
|
||||
build_base infra/docker/worker-base-runtime.Dockerfile "$BASE_RUNTIME" "worker-base-runtime"
|
||||
|
||||
echo "fallback=true" >> $GITHUB_OUTPUT
|
||||
echo "基础镜像本地构建完成"
|
||||
fi
|
||||
|
||||
- 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
|
||||
|
||||
# Worker fallback模式:基础镜像本地已构建,用普通docker build绕过buildx
|
||||
if [ "${{ matrix.service }}" = "worker" ] && [ "${{ steps.prebuild.outputs.fallback }}" = "true" ]; then
|
||||
echo "Fallback模式:用普通docker build(基础镜像本地已构建)"
|
||||
BUILD_ARG_STR=""
|
||||
for arg in $EXTRA_BUILD_ARGS; do
|
||||
BUILD_ARG_STR="$BUILD_ARG_STR --build-arg $arg"
|
||||
done
|
||||
docker build -f ${{ matrix.dockerfile }} -t "${IMAGE_TAG}" $BUILD_ARG_STR .
|
||||
echo "Fallback PR Build successful"
|
||||
exit 0
|
||||
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-pr-web:
|
||||
name: PR Build Web Image
|
||||
runs-on: runtime-builder
|
||||
timeout-minutes: ${{ matrix.timeout }}
|
||||
needs: check-frontend-only
|
||||
if: always() && github.event_name == 'pull_request' && needs.check-frontend-only.outputs.skip_frontend != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- service: web
|
||||
service_display: Web
|
||||
dockerfile: infra/docker/web.Dockerfile
|
||||
@@ -756,6 +927,7 @@ 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-staging:
|
||||
name: Build Staging ${{ matrix.service_display }} Image
|
||||
runs-on: runtime-builder
|
||||
@@ -1481,18 +1653,26 @@ jobs:
|
||||
shell: sh
|
||||
run: bash scripts/ci/step_timer_start.sh
|
||||
- name: Run production browser E2E
|
||||
shell: sh
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
docker run --rm --ipc=host \
|
||||
# DooD模式下不能用-v挂载(宿主机路径与CI容器路径不一致)
|
||||
# 改用 docker create + docker cp 方式把代码拷进容器
|
||||
CONTAINER_NAME="production-e2e-$$"
|
||||
docker create --name "$CONTAINER_NAME" --ipc=host \
|
||||
-e E2E_BASE_URL=https://saas.xiaoxiajianji.com \
|
||||
-e E2E_API_BASE=https://api.xiaoxiajianji.com/api/v1 \
|
||||
-e E2E_BROWSER_CHANNEL=chromium \
|
||||
-e PLAYWRIGHT_HEADLESS=1 \
|
||||
-v "$PWD:/workspace" \
|
||||
-w /workspace/apps/web \
|
||||
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
|
||||
sh -lc 'npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts'
|
||||
sh -lc "npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts"
|
||||
docker cp apps "$CONTAINER_NAME:/workspace/"
|
||||
docker cp package-lock.json "$CONTAINER_NAME:/workspace/" 2>/dev/null || true
|
||||
docker start -a "$CONTAINER_NAME"
|
||||
EXIT_CODE=$(docker wait "$CONTAINER_NAME")
|
||||
docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
||||
exit $EXIT_CODE
|
||||
|
||||
- name: Job duration summary
|
||||
if: always()
|
||||
@@ -1676,7 +1856,8 @@ jobs:
|
||||
- integration-tests
|
||||
- frontend-lint
|
||||
- frontend-unit-test
|
||||
- build-pr
|
||||
- build-pr-backend
|
||||
- build-pr-web
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -1703,7 +1884,8 @@ jobs:
|
||||
RESULT_INTEGRATION: ${{ needs.integration-tests.result }}
|
||||
RESULT_FRONTEND_LINT: ${{ needs.frontend-lint.result }}
|
||||
RESULT_FRONTEND_UNIT: ${{ needs.frontend-unit-test.result }}
|
||||
RESULT_BUILD_PR: ${{ needs.build-pr.result }}
|
||||
RESULT_BUILD_PR_BACKEND: ${{ needs.build-pr-backend.result }}
|
||||
RESULT_BUILD_PR_WEB: ${{ needs.build-pr-web.result }}
|
||||
run: |
|
||||
set -eu
|
||||
echo "=== CI Gate 评估 ==="
|
||||
@@ -1717,7 +1899,8 @@ jobs:
|
||||
echo " integration-tests: $RESULT_INTEGRATION"
|
||||
echo " frontend-lint: $RESULT_FRONTEND_LINT"
|
||||
echo " frontend-unit-test: $RESULT_FRONTEND_UNIT"
|
||||
echo " build-pr: $RESULT_BUILD_PR"
|
||||
echo " build-pr-backend: $RESULT_BUILD_PR_BACKEND"
|
||||
echo " build-pr-web: $RESULT_BUILD_PR_WEB"
|
||||
echo ""
|
||||
|
||||
# 判断PR类型
|
||||
@@ -1731,18 +1914,19 @@ jobs:
|
||||
"validate-code-quality:$RESULT_CODE_QUALITY"
|
||||
"validate-type-check:$RESULT_TYPE_CHECK"
|
||||
"validate-migration:$RESULT_MIGRATION"
|
||||
"frontend-lint:$RESULT_FRONTEND_LINT"
|
||||
"build-pr:$RESULT_BUILD_PR"
|
||||
)
|
||||
|
||||
# 后端检查
|
||||
REQUIRED_BACKEND=(
|
||||
"unit-tests:$RESULT_UNIT_TESTS"
|
||||
"build-pr-backend:$RESULT_BUILD_PR_BACKEND"
|
||||
)
|
||||
|
||||
# 前端检查
|
||||
REQUIRED_FRONTEND=(
|
||||
"frontend-lint:$RESULT_FRONTEND_LINT"
|
||||
"frontend-unit-test:$RESULT_FRONTEND_UNIT"
|
||||
"build-pr-web:$RESULT_BUILD_PR_WEB"
|
||||
)
|
||||
|
||||
ALL_PASSED=true
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
from app.auth import AuthenticatedUser
|
||||
from app.auth import get_current_user as get_authenticated_user
|
||||
from app.dependencies import get_user_repository
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from packages.domain.entities import User
|
||||
|
||||
@@ -5,3 +5,45 @@ target-version = ["py312"]
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 120
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
line-length = 120
|
||||
exclude = [
|
||||
".git",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
"node_modules",
|
||||
"alembic",
|
||||
".gitea",
|
||||
".next",
|
||||
"dist",
|
||||
"build",
|
||||
"hostexecutor",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors(同 flake8 默认)
|
||||
"F", # pyflakes(同 flake8 默认)
|
||||
]
|
||||
ignore = [
|
||||
"E203",
|
||||
"E501", # line-too-long(black管)
|
||||
"E302",
|
||||
"E402", # module-import-not-at-top(循环导入多)
|
||||
"E722", # bare-except
|
||||
"W291",
|
||||
"W293",
|
||||
"F401",
|
||||
"F403",
|
||||
"F405",
|
||||
"F841",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["F401", "F403", "F405"]
|
||||
"tests/**" = ["E402", "F401", "F821", "F841"]
|
||||
"packages/ports/*" = ["E301"]
|
||||
"apps/api/app/api/routes/auth.py" = ["ALL"]
|
||||
|
||||
Reference in New Issue
Block a user