diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 421d63385..d50dc24d2 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -90,7 +90,7 @@ jobs: ' - name: Secret detection (detect-secrets) shell: sh - run: "set -eu\necho \"=== Installing detect-secrets ===\"\npython3 -m pip install -q detect-secrets\ndetect-secrets --version\necho \"\"\necho \"=== Running secret scan ===\"\ndetect-secrets scan \\\n --all-files \\\n --exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \\\n --exclude-files '\\.(md|rst|txt|lock|example|sample|min\\.js|min\\.css|spec\\.ts|test\\.ts|test\\.py)$' \\\n --exclude-files '(package-lock|yarn\\.lock|poetry\\.lock|Pipfile\\.lock)$' \\\n --disable-plugin Base64HighEntropyString \\\n --disable-plugin HexHighEntropyString \\\n --disable-plugin BasicAuthDetector \\\n --disable-plugin KeywordDetector \\\n --disable-plugin IPPublicDetector \\\n 2>&1 | tee /tmp/secrets-scan.json\n\nFOUND=$(python3 -c \"\nimport json\ntry:\n with open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\n results = data.get('results', {})\n total = sum(len(v) for\ + run: "set -eu\necho \"=== Installing detect-secrets ===\"\npython3 -m pip install -q detect-secrets\ndetect-secrets --version\necho \"\"\necho \"=== Running secret scan ===\"\ndetect-secrets scan \\\n --all-files \\\n --exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \\\n --exclude-files '\\.(md|rst|txt|lock|example|sample|min\\.js|min\\.css|spec\\.ts|test\\.ts|test\\.py)$' \\\n --exclude-files '(package-lock|yarn\\.lock|poetry\\.lock|Pipfile\\.lock)$' \\\n --disable-plugin Base64HighEntropyString \\\n --disable-plugin HexHighEntropyString \\\n --disable-plugin BasicAuthDetector \\\n --disable-plugin KeywordDetector \\\n --disable-plugin IPPublicDetector \\\n > /tmp/secrets-scan.json 2>&1\n\nFOUND=$(python3 -c \"\nimport json\ntry:\n with open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\n results = data.get('results', {})\n total = sum(len(v) for\ \ v in results.values())\n print(total)\nexcept Exception:\n print('error')\n\")\necho \"\"\necho \"Secrets detected: $FOUND\"\nif [ \"$FOUND\" != \"0\" ] && [ \"$FOUND\" != \"error\" ]; then\n echo \"\"\n echo \"=== Secret details ===\"\n python3 -c \"\nimport json\nwith open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\nfor fpath, items in data.get('results', {}).items():\n for item in items:\n line = item.get('line_number', '?')\n stype = item.get('type', '?')\n hashed = item.get('hashed_secret', '')[:16]\n print(f' {fpath}:{line} [{stype}] {hashed}...')\n\"\n echo \"\"\n echo \"ERROR: Potential secrets detected in code!\"\n echo \"If these are false positives, add exclusions in the CI workflow.\"\n exit 1\nfi\necho \"Secret scan completed - no secrets detected\"\n" - name: Calculate changed Python files (incremental scan) shell: sh @@ -129,24 +129,51 @@ jobs: bash -n scripts/init_production_env.sh ' - - name: Validate Alembic migrations - shell: sh - run: 'set -eu - - python3 -m alembic upgrade head --sql > /tmp/alembic-upgrade.sql - - test -s /tmp/alembic-upgrade.sql - - grep -q "Running upgrade" /tmp/alembic-upgrade.sql - - python3 scripts/check_schema_metadata.py - - ' - - name: Check migration safety + - name: Validate Alembic migrations (with isolated PG) shell: sh env: GITHUB_TOKEN: ${{ github.token }} - run: "set -eu\npython3 scripts/check_migration_safety.py --allow-medium-risk --diff-against origin/develop\n" + run: | + set -eu + PG_CONTAINER=ci-pg-validate-${GITHUB_RUN_ID:-$$} + docker rm -f "$PG_CONTAINER" 2>/dev/null || true + docker run -d --name "$PG_CONTAINER" \ + --shm-size=256m \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=xiaoxia_saas \ + -P \ + --health-cmd "pg_isready -U postgres" \ + --health-interval 3s \ + --health-timeout 3s \ + --health-retries 20 \ + postgres:16-alpine + PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | cut -d: -f2) + echo "PostgreSQL port: $PG_PORT" + export DATABASE_URL=postgresql+psycopg://postgres:postgres@127.0.0.1:$PG_PORT/xiaoxia_saas + for i in $(seq 1 30); do + if docker inspect --format='{{.State.Health.Status}}' "$PG_CONTAINER" 2>/dev/null | grep -q healthy; then + echo "PostgreSQL is ready on port $PG_PORT" + break + fi + echo "Waiting for PostgreSQL... ($i/30)" + sleep 2 + done + docker inspect --format='{{.State.Health.Status}}' "$PG_CONTAINER" | grep -q healthy + python3 -m alembic upgrade head --sql > /tmp/alembic-upgrade.sql + test -s /tmp/alembic-upgrade.sql + grep -q "Running upgrade" /tmp/alembic-upgrade.sql + python3 scripts/check_schema_metadata.py + # Initialize git for migration safety diff (CI checkout is tar.gz without .git) + git init > /dev/null 2>&1 + git remote add origin https://git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas.git > /dev/null 2>&1 + git fetch origin develop:refs/remotes/origin/develop --depth=100 > /dev/null 2>&1 + git add -A > /dev/null 2>&1 + git -c user.email=ci@local -c user.name=CI commit -m "ci-tmp" > /dev/null 2>&1 + python3 scripts/check_migration_safety.py --allow-medium-risk --diff-against origin/develop + docker rm -f "$PG_CONTAINER" 2>/dev/null || true + echo "PostgreSQL container cleaned up" + - name: Job duration summary if: always() shell: sh diff --git a/apps/api/app/api/routes/edit_plans_clips.py b/apps/api/app/api/routes/edit_plans_clips.py index 544ec6d46..81342a132 100755 --- a/apps/api/app/api/routes/edit_plans_clips.py +++ b/apps/api/app/api/routes/edit_plans_clips.py @@ -52,7 +52,9 @@ class EditPlanClipListResponse(BaseModel): class EditPlanClipCreateRequest(BaseModel): """创建剪辑片段请求体""" - clip_type: str = Field(..., min_length=1, max_length=50, description="片段类型: main/intro/outro/overlay/background/b_roll 等") + clip_type: str = Field( + ..., min_length=1, max_length=50, description="片段类型: main/intro/outro/overlay/background/b_roll 等" + ) order: int = Field(..., ge=0, description="排序序号") asset_id: str = Field(default="", max_length=64, description="关联素材 ID") text_content: str = Field(default="", max_length=5000, description="文本内容(字幕/配音等)") @@ -86,9 +88,10 @@ def _check_plan_access(plan_id: str, user_id: str, project_repository: Any, db: """验证用户是否有权限访问该剪辑计划(通过项目关联)。 返回 plan 对象供后续使用,避免重复查询。 """ - from ._helpers import check_project_access from app.services.edit_plan_service import EditPlanService + from ._helpers import check_project_access + svc = EditPlanService(db) plan = svc.get_plan(plan_id) if plan is None: diff --git a/apps/api/app/api/routes/edit_plans_clips_batch.py b/apps/api/app/api/routes/edit_plans_clips_batch.py index 29af9cf20..bb4a1fb7b 100755 --- a/apps/api/app/api/routes/edit_plans_clips_batch.py +++ b/apps/api/app/api/routes/edit_plans_clips_batch.py @@ -75,9 +75,10 @@ class ClipsFromAssetsResponse(BaseModel): def _check_plan_access(plan_id: str, user_id: str, project_repository: Any, db: Session) -> Any: """验证用户是否有权限访问该剪辑计划,返回 plan 对象。""" - from ._helpers import check_project_access from app.services.edit_plan_service import EditPlanService + from ._helpers import check_project_access + svc = EditPlanService(db) plan = svc.get_plan(plan_id) if plan is None: diff --git a/scripts/check_migration_safety.py b/scripts/check_migration_safety.py index 805180c48..17649b270 100644 --- a/scripts/check_migration_safety.py +++ b/scripts/check_migration_safety.py @@ -195,16 +195,28 @@ def get_new_migrations_via_git(diff_target: str) -> List[Path] | None: timeout=30, ) + # 优先使用三点diff(找合并基线),失败时回退到两点diff(兼容tar.gz checkout + git init的CI环境) + diff_args = ["git", "diff", "--name-only", "--diff-filter=A", f"{diff_target}...HEAD"] result = subprocess.run( - ["git", "diff", "--name-only", "--diff-filter=A", f"{diff_target}...HEAD"], + diff_args, capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=10, ) if result.returncode != 0: - print(f" (git diff 失败:{result.stderr.strip()})") - return None + # fallback: 两点diff(无需共同祖先) + diff_args_2 = ["git", "diff", "--name-only", "--diff-filter=A", diff_target, "HEAD"] + result = subprocess.run( + diff_args_2, + capture_output=True, + text=True, + cwd=str(REPO_ROOT), + timeout=10, + ) + if result.returncode != 0: + print(f" (git diff 失败:{result.stderr.strip()})") + return None new_migrations = [] for line in result.stdout.strip().split("\n"):