Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f17e607ad2 | |||
| 3cb232a84d |
+37
-278
File diff suppressed because one or more lines are too long
@@ -103,7 +103,7 @@ jobs:
|
||||
- name: Type check (mypy, advisory mode)
|
||||
if: always()
|
||||
shell: sh
|
||||
run: "bash scripts/ci/mypy_check.sh"
|
||||
run: "set +e\necho \"=== Installing mypy ===\"\npython3 -m pip install -q mypy\nmypy --version\necho \"\"\necho \"=== Running mypy type check (advisory mode) ===\"\necho \"告警模式,不阻断CI\"\necho \"\"\n# 只检查核心业务代码,跳过测试和迁移\nEXIT_CODE=0\nmypy apps/api/app packages --ignore-missing-imports --no-site-packages --no-strict-optional --explicit-package-bases --exclude 'tests/|test_|migrations/|alembic/' --no-error-summary 2>&1 | head -60 || EXIT_CODE=$?\necho \"\"\nif [ \"$EXIT_CODE\" != \"0\" ]; then\n echo \"mypy 发现类型问题(告警模式,不阻断)\"\n echo \"建议后续逐步修复\"\nelse\n echo \"mypy 类型检查通过 ✅\"\nfi\nexit 0\n"
|
||||
- name: Run security scan (bandit)
|
||||
shell: sh
|
||||
run: 'set -eu
|
||||
@@ -142,11 +142,15 @@ jobs:
|
||||
python3 scripts/check_schema_metadata.py
|
||||
|
||||
'
|
||||
- name: Check migration safety
|
||||
- name: Prepare git for migration diff
|
||||
shell: sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: "set -eu\npython3 scripts/check_migration_safety.py --allow-medium-risk --diff-against origin/develop\n"
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
run: "set -eu\nif command -v git >/dev/null 2>&1; then\n if [ ! -d .git ]; then\n git init -q\n git config user.email \"ci@localhost\"\n git config user.name \"CI\"\n git add .\n git commit -q -m \"current\"\n REPO_URL=\"https://x-access-token:${GITHUB_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git\"\n git remote add origin \"$REPO_URL\"\n fi\n git fetch origin main --depth=1 -q 2>/dev/null || echo \"WARN: cannot fetch main, will check all migrations\"\nelse\n echo \"WARN: git not available, will check all migrations\"\nfi\n"
|
||||
- name: Check migration safety
|
||||
shell: sh
|
||||
run: "set -eu\nif git rev-parse origin/main >/dev/null 2>&1; then\n python3 scripts/check_migration_safety.py --allow-medium-risk --diff-against origin/main\nelse\n python3 scripts/check_migration_safety.py --allow-medium-risk\nfi\n"
|
||||
- name: Job duration summary
|
||||
if: always()
|
||||
shell: sh
|
||||
@@ -399,4 +403,4 @@ jobs:
|
||||
|
||||
NOTIFY_MODE=failure JOB_NAME="Frontend Lint" python3 scripts/ci_notify.py
|
||||
|
||||
'
|
||||
'
|
||||
|
||||
@@ -72,7 +72,6 @@ class EditPlanResponse(BaseModel):
|
||||
name: str
|
||||
status: str
|
||||
total_duration: float
|
||||
result_count: int = 0
|
||||
project_id: str = ""
|
||||
created_by_user_id: str = ""
|
||||
config: dict[str, Any]
|
||||
@@ -242,7 +241,6 @@ def _to_response(p: EditPlan) -> EditPlanResponse:
|
||||
name=p.name,
|
||||
status=p.status.value if hasattr(p.status, "value") else p.status,
|
||||
total_duration=p.total_duration,
|
||||
result_count=getattr(p, "result_count", 0),
|
||||
project_id=p.project_id or "",
|
||||
created_by_user_id=p.created_by_user_id or "",
|
||||
config=p.config,
|
||||
|
||||
@@ -130,8 +130,6 @@ export interface EditPlan {
|
||||
name: string;
|
||||
status: EditPlanStatus;
|
||||
total_duration: number;
|
||||
/** 生成视频数量(后端 EditPlanResponse.result_count) */
|
||||
result_count: number;
|
||||
config: EditPlanConfig;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -163,21 +161,14 @@ export interface GenerateResponse {
|
||||
clip_count: number;
|
||||
}
|
||||
|
||||
/** 剪辑计划关联的生成记录(实际是 GenerationTask 对象) */
|
||||
/** 剪辑计划关联的生成记录 */
|
||||
export interface EditPlanGeneration {
|
||||
id: string; // 即 generation_task_id
|
||||
source_edit_plan_id: string;
|
||||
template_id: string;
|
||||
asset_ids: string[];
|
||||
id: string;
|
||||
edit_plan_id: string;
|
||||
generation_task_id: string;
|
||||
status: EditPlanStatus;
|
||||
progress: number;
|
||||
result_count: number;
|
||||
error_message: string;
|
||||
error_info: Record<string, unknown>;
|
||||
logs: Array<Record<string, unknown>>;
|
||||
retry_count: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** 片段生成状态 */
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ConfigProvider, App as AntApp } from "antd";
|
||||
import { ConfigProvider } from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import router from "./router";
|
||||
import "./index.css";
|
||||
@@ -91,9 +91,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConfigProvider locale={zhCN} theme={theme}>
|
||||
<AntApp>
|
||||
<RouterProvider router={router} />
|
||||
</AntApp>
|
||||
<RouterProvider router={router} />
|
||||
</ConfigProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
|
||||
@@ -254,16 +254,6 @@ export default function EditPlans() {
|
||||
<span className="plan-duration">{formatDuration(seconds)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "视频数",
|
||||
dataIndex: "result_count",
|
||||
key: "result_count",
|
||||
width: 80,
|
||||
align: "center",
|
||||
render: (count: number) => (
|
||||
<span className="plan-result-count">{count > 0 ? count : "—"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "created_at",
|
||||
|
||||
@@ -1812,27 +1812,7 @@
|
||||
═══════════════════════════════════════ */
|
||||
|
||||
.ep-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 16px;
|
||||
background: var(--ep-bg-card, #fff);
|
||||
border-bottom: 1px solid var(--ep-border, #e8e8e8);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #666);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ep-status-left,
|
||||
.ep-status-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ep-status-sep {
|
||||
margin: 0 4px;
|
||||
opacity: 0.35;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
|
||||
@@ -955,21 +955,12 @@ const EditingPlanner: React.FC = () => {
|
||||
let planId = loadedPlanId;
|
||||
|
||||
if (planId) {
|
||||
// 已有计划 → 先重置状态为 draft(failed/editing 等非 draft 状态会被后端拒绝更新和生成)
|
||||
try {
|
||||
await updateEditPlan(planId, { status: "draft" });
|
||||
} catch (resetErr) {
|
||||
console.warn("[状态重置跳过]", resetErr);
|
||||
}
|
||||
// 再更新配置
|
||||
try {
|
||||
await updateEditPlan(planId, {
|
||||
config,
|
||||
total_duration: totalDuration,
|
||||
});
|
||||
} catch (updateErr) {
|
||||
console.warn("[计划更新跳过]", updateErr);
|
||||
}
|
||||
// 已有计划 → 更新配置
|
||||
await updateEditPlan(planId, {
|
||||
config,
|
||||
total_duration: totalDuration,
|
||||
status: "editing",
|
||||
});
|
||||
} else {
|
||||
// 无计划 → 创建新计划
|
||||
const plan = await createEditPlan({
|
||||
@@ -1288,8 +1279,8 @@ const EditingPlanner: React.FC = () => {
|
||||
|
||||
{/* ═══ 生成进度弹窗 ═══ */}
|
||||
<Modal
|
||||
title={genError ? "生成失败" : generated ? "生成完成" : "正在生成视频"}
|
||||
open={generating || generated || !!genError}
|
||||
title={generated ? "生成完成" : "正在生成视频"}
|
||||
open={generating || generated}
|
||||
footer={
|
||||
generated
|
||||
? [
|
||||
|
||||
@@ -65,7 +65,7 @@ const GenerationHistoryModal: React.FC<GenerationHistoryModalProps> = ({
|
||||
return (
|
||||
<tr key={gen.id} className="ep-gh-table-row">
|
||||
<td className="ep-gh-td ep-gh-td-id">
|
||||
{gen.id ? `${gen.id.slice(0, 8)}...` : "—"}
|
||||
{gen.generation_task_id.slice(0, 8)}...
|
||||
</td>
|
||||
<td className="ep-gh-td">
|
||||
<span className={`ep-gh-status-tag ${statusClass}`}>
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# CI 大量失败根因排查报告
|
||||
|
||||
**排查时间:** 2026-07-13
|
||||
**排查人:** 构建服务器运维Agent
|
||||
**范围:** 最近15次 CI run(PR #258~#265 + develop 分支多次 push)
|
||||
|
||||
## 一、整体概况
|
||||
|
||||
最近 20 次 CI run 中 16 次失败,失败率 **80%**。失败集中在 3 个 Job:
|
||||
|
||||
| Job | 失败率 | 根因类型 |
|
||||
|-----|--------|----------|
|
||||
| Validate Code Quality | 100% | black 代码格式检查失败 |
|
||||
| Unit Tests | 100% | 测试断言未同步国际化改动 |
|
||||
| Integration Tests | 100% | 密码重置接口变更未同步测试 |
|
||||
| Frontend Lint | 20% | 各 PR 代码质量问题 |
|
||||
|
||||
**结论:3 个全局性失败点导致所有 PR CI 全红,不是代码本身问题,是基础设施/测试用例滞后。**
|
||||
|
||||
---
|
||||
|
||||
## 二、详细根因分析
|
||||
|
||||
### 1. Validate — black 格式检查失败
|
||||
|
||||
**现象:**
|
||||
```
|
||||
would reformat scripts/check_migration_safety.py
|
||||
1 file would be reformatted, 369 files would be left unchanged.
|
||||
Oh no! 💥 💔 💥
|
||||
```
|
||||
|
||||
**根因:**
|
||||
`scripts/check_migration_safety.py` 文件不符合 black 格式化规范。该文件是最近新增的迁移安全检查脚本,提交前未本地跑 black 格式化。
|
||||
|
||||
**影响范围:** 所有 PR 及 develop 分支,全量失败。
|
||||
|
||||
**修复方案:**
|
||||
```bash
|
||||
black scripts/check_migration_safety.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Unit Tests — 1 个用例失败
|
||||
|
||||
**现象:**
|
||||
```
|
||||
FAILED tests/unit/test_asset_library_delete.py::TestDeleteAssetLibrary::test_delete_library_access_denied
|
||||
AssertionError: assert 'Access denied' in '无权访问该项目'
|
||||
```
|
||||
|
||||
**统计:** 1442 passed, 1 failed
|
||||
|
||||
**根因:**
|
||||
项目之前做了国际化(i18n)改造,错误信息从英文改成了中文,但对应的单元测试断言仍然检查英文 "Access denied",导致断言失败。
|
||||
|
||||
**影响范围:** 所有 PR 及 develop 分支,全量失败。
|
||||
|
||||
**修复方案:**
|
||||
修改 `tests/unit/test_asset_library_delete.py` 中的断言,将 `'Access denied'` 改为 `'无权访问该项目'`,或改为断言 HTTP 状态码(403)而不是错误消息文本。
|
||||
|
||||
---
|
||||
|
||||
### 3. Integration Tests — 1 个用例失败
|
||||
|
||||
**现象:**
|
||||
```
|
||||
FAILED tests/integration/test_auth.py::TestPasswordReset::test_request_password_reset_success
|
||||
assert 404 in (200, 202)
|
||||
```
|
||||
|
||||
**统计:** 45 passed, 1 failed, 13 deselected, 2 rerun
|
||||
|
||||
**根因:**
|
||||
密码重置请求接口(`POST /auth/password-reset/request` 或类似路由)返回 404,说明该接口已被移除、路由变更,或对应的功能模块暂时被注释/下线。
|
||||
|
||||
**影响范围:** 所有 PR 及 develop 分支,全量失败。
|
||||
|
||||
**修复方案:**
|
||||
- 如果接口确实下线了:删除或 skip 这个测试用例
|
||||
- 如果是路由改了:更新测试中的 API 路径
|
||||
- 如果是功能待开发:标记为 `@pytest.mark.skip` 并加上 TODO
|
||||
|
||||
---
|
||||
|
||||
## 三、修复优先级
|
||||
|
||||
| 优先级 | 问题 | 修复难度 | 预估时间 |
|
||||
|--------|------|----------|----------|
|
||||
| P0 | black 格式检查失败 | ⭐ | 5分钟 |
|
||||
| P0 | 单元测试国际化断言失败 | ⭐ | 10分钟 |
|
||||
| P1 | 集成测试密码重置接口404 | ⭐⭐ | 30分钟(需确认接口状态) |
|
||||
|
||||
**建议:** 先修前两个 P0(能让 2/3 的 job 变绿),再处理密码重置那个。
|
||||
|
||||
---
|
||||
|
||||
## 四、Runner 执行情况观察
|
||||
|
||||
- 当前 9 个 Runner 全部在线(构建服务器 4 个 + 新服务器 5 个)
|
||||
- 失败的 Job 都是在构建服务器的 Runner 上执行的(xiaoxia-ci-runner-2/3 等)
|
||||
- 新服务器 5 个 Runner 目前全部空闲(标签修复后首次接任务可能需要时间)
|
||||
- 并发能力充足,瓶颈在代码/测试本身,不在 Runner 资源
|
||||
@@ -1,136 +0,0 @@
|
||||
# 三台服务器 Runner 分工规划
|
||||
|
||||
**制定日期:** 2026-07-13
|
||||
**状态:** 规划中
|
||||
|
||||
---
|
||||
|
||||
## 一、现状总览
|
||||
|
||||
当前共 9 个 Gitea Actions Runner,分布在 3 台服务器上:
|
||||
|
||||
| 服务器 | IP | 配置 | Runner 数量 | 当前状态 |
|
||||
|--------|-----|------|-------------|----------|
|
||||
| 构建服务器 | 114.55.236.178 | 4核 / 7.1G RAM / 49G NVMe | 4个(ID: 8, 42, 46, 47) | ✅ 在线 |
|
||||
| 新CI服务器 | 116.62.226.203 | 8核 / 14G RAM | 5个(ID: 58-62) | ✅ 在线 |
|
||||
| 业务服务器 | 47.98.113.167 | - | 0个(旧3个已下线) | ⚠️ 待规划 |
|
||||
|
||||
**所有 Runner 共用标签:** `saas`, `runtime-builder`, `host`, `ubuntu-latest`
|
||||
|
||||
---
|
||||
|
||||
## 二、问题分析
|
||||
|
||||
### 2.1 标签无区分
|
||||
所有 Runner 标签完全一致,CI 任务随机分配到任意 Runner,导致:
|
||||
- 构建任务(Build)可能跑到配置低的机器上,构建慢
|
||||
- 代码检查任务占着构建服务器,影响构建速度
|
||||
- 业务服务器跑 CI 影响线上服务稳定性
|
||||
|
||||
### 2.2 资源浪费
|
||||
- 新服务器 8核14G 跑 validate/lint 有点大材小用
|
||||
- 构建服务器 4核7G 跑 Docker 构建偏紧张
|
||||
|
||||
---
|
||||
|
||||
## 三、规划方案
|
||||
|
||||
### 3.1 分工原则
|
||||
|
||||
| 服务器 | 角色 | 主要任务类型 | 标签策略 |
|
||||
|--------|------|-------------|----------|
|
||||
| **构建服务器** (114.55.236.178) | 构建专机 | Build Staging / Build Production / Docker 镜像构建 | 保留 `saas` + `host`,新增 `build-only` |
|
||||
| **新CI服务器** (116.62.226.203) | 代码检查专机 | Validate / Unit Tests / Integration Tests / Frontend Lint | 保留 `saas` + `host`,新增 `ci-check` |
|
||||
| **业务服务器** (47.98.113.167) | 部署专机 | Deploy Staging / Deploy Production / E2E Tests | 保留 `saas` + `host`,新增 `deploy-only` |
|
||||
|
||||
### 3.2 具体配置
|
||||
|
||||
#### 构建服务器(4个 Runner)
|
||||
- **数量:** 3个(从4个缩减,释放资源给构建缓存)
|
||||
- **标签:** `saas`, `host`, `build-only`, `ubuntu-latest`
|
||||
- **负责 Job:**
|
||||
- `build-staging`
|
||||
- `build-production-runtime-images`
|
||||
- 其他需要 Docker buildx 的任务
|
||||
|
||||
#### 新CI服务器(5个 Runner)
|
||||
- **数量:** 5个(保持不变)
|
||||
- **标签:** `saas`, `host`, `ci-check`, `ubuntu-latest`
|
||||
- **负责 Job:**
|
||||
- `validate`
|
||||
- `unit-tests`
|
||||
- `integration-tests`
|
||||
- `frontend-lint`
|
||||
- 安全扫描(gitleaks / pip-audit / vulture 等)
|
||||
|
||||
#### 业务服务器(1-2个 Runner)
|
||||
- **数量:** 1-2个(逐步替换旧的3个)
|
||||
- **标签:** `saas`, `host`, `deploy-only`, `ubuntu-latest`
|
||||
- **负责 Job:**
|
||||
- `deploy-staging`
|
||||
- `deploy-production`
|
||||
- `staging-e2e` / `production-e2e`
|
||||
- `staging-api-tests`
|
||||
|
||||
---
|
||||
|
||||
## 四、实施步骤
|
||||
|
||||
### Phase 1: 标签打标(低风险,立即做)
|
||||
1. 新服务器 5 个 Runner 添加 `ci-check` 标签
|
||||
2. 构建服务器保留 3 个 Runner,添加 `build-only` 标签
|
||||
3. 业务服务器部署 1 个新 Runner,标签 `deploy-only`
|
||||
|
||||
### Phase 2: Job 路由调整(中风险,逐步来)
|
||||
1. validate / unit-tests / integration-tests / frontend-lint 改为 `runs-on: ci-check`
|
||||
2. build-staging / build-production 改为 `runs-on: build-only`
|
||||
3. deploy-* / e2e 改为 `runs-on: deploy-only`
|
||||
|
||||
### Phase 3: 旧 Runner 下线
|
||||
- 业务服务器旧的 3 个 Runner 确认无任务后下线
|
||||
- 构建服务器多余的 1 个 Runner 迁移到新服务器
|
||||
|
||||
---
|
||||
|
||||
## 五、并发配置优化建议
|
||||
|
||||
### 5.1 当前并发情况
|
||||
- 首发并行 Job:validate + unit-tests + frontend-lint(3个并行)
|
||||
- integration-tests 依赖 validate(串行,浪费资源)
|
||||
- 无 concurrency 限制,同一分支多次 push 会重复跑
|
||||
|
||||
### 5.2 优化建议
|
||||
|
||||
**1. integration-tests 改为与 unit-tests 并行**
|
||||
```yaml
|
||||
# 当前
|
||||
integration-tests:
|
||||
needs: validate # 没必要等validate
|
||||
|
||||
# 优化后
|
||||
integration-tests:
|
||||
needs: [] # 直接和unit-tests并行跑
|
||||
```
|
||||
|
||||
**2. 增加分支级 concurrency,取消重复构建**
|
||||
```yaml
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
```
|
||||
同一 PR 多次 push 时,取消旧的构建,只跑最新的。
|
||||
|
||||
**3. Build Staging 移出 PR 门禁**
|
||||
- 已在阶段二优化中完成(PR #245)
|
||||
- Build Staging 只在 develop/main 上异步构建
|
||||
|
||||
---
|
||||
|
||||
## 六、预期收益
|
||||
|
||||
| 指标 | 当前 | 优化后 | 提升 |
|
||||
|------|------|--------|------|
|
||||
| PR CI 总时长 | ~8-12分钟 | ~4-6分钟 | ⏱️ 缩短 40-50% |
|
||||
| 构建速度 | 可能抢到慢机器 | 固定高配构建机 | 🚀 更稳定更快 |
|
||||
| 线上稳定性 | CI和业务抢资源 | 部署独立Runner | 🛡️ 隔离保障 |
|
||||
| Runner 利用率 | 随机分配 | 按任务类型调度 | 📈 更合理 |
|
||||
@@ -1,10 +1,9 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
Base: Any = declarative_base()
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class UserModel(Base):
|
||||
|
||||
@@ -63,7 +63,6 @@ exclude = [
|
||||
".next",
|
||||
"dist",
|
||||
"build",
|
||||
"hostexecutor",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
+11
-76
@@ -1,108 +1,43 @@
|
||||
#!/bin/bash
|
||||
# 自动合并通过 CI 检查的 PR
|
||||
# 用法: ./scripts/auto_merge_prs.sh [target_branch]
|
||||
#
|
||||
# 合并前必须验证的 CI 检查项:
|
||||
# - CI/CD Pipeline / Validate Code Quality And Tests (push)
|
||||
# - CI/CD Pipeline / Frontend Lint (push)
|
||||
# 只有两个检查项均为 success 状态才允许合并
|
||||
|
||||
GITEA_API="${GITEA_API_URL:-https://git.xiaoxiajianji.com/api/v1}"
|
||||
GITEA_API="https://git.xiaoxiajianji.com/api/v1"
|
||||
TOKEN="${GITEA_API_TOKEN:?Please set GITEA_API_TOKEN environment variable}"
|
||||
REPO="xiaoxia/xiaoxia-saas"
|
||||
TARGET_BRANCH="${1:-develop}"
|
||||
|
||||
# 必需的 CI 检查项(context 名称前缀匹配,避免 pipeline 名称变化导致匹配失败)
|
||||
REQUIRED_CHECKS=(
|
||||
"Validate Code Quality And Tests"
|
||||
"Frontend Lint"
|
||||
)
|
||||
|
||||
echo "=== Checking open PRs targeting $TARGET_BRANCH ==="
|
||||
|
||||
# 获取所有 open PR
|
||||
PRS=$(curl -s -H "Authorization: token $TOKEN" \
|
||||
"$GITEA_API/repos/$REPO/pulls?state=open&sort=updated&direction=desc" | python3 -c "
|
||||
"$GITEA_API/repos/$REPO/pulls?state=open&labels=0" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for pr in data:
|
||||
if pr.get('base', {}).get('ref') == '$TARGET_BRANCH':
|
||||
head_sha = pr.get('head', {}).get('sha', '')
|
||||
print(f\"{pr['number']}|{pr['title']}|{head_sha}\")
|
||||
if pr.get('mergeable', False):
|
||||
print(f\"{pr['number']}|{pr['title']}|{pr.get('mergeable', 'unknown')}\")
|
||||
")
|
||||
|
||||
if [ -z "$PRS" ]; then
|
||||
echo "No open PRs found for $TARGET_BRANCH"
|
||||
echo "No mergeable PRs found for $TARGET_BRANCH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
merge_count=0
|
||||
skip_count=0
|
||||
|
||||
echo "$PRS" | while IFS='|' read -r number title head_sha; do
|
||||
echo ""
|
||||
echo "--- PR #$number: $title ---"
|
||||
echo " Head SHA: $head_sha"
|
||||
|
||||
# 获取该 commit 的 combined CI 状态
|
||||
STATUS_JSON=$(curl -s -H "Authorization: token $TOKEN" \
|
||||
"$GITEA_API/repos/$REPO/commits/$head_sha/status")
|
||||
|
||||
# 检查每个必需的 CI 项是否通过
|
||||
all_passed=true
|
||||
failed_checks=""
|
||||
|
||||
for check_pattern in "${REQUIRED_CHECKS[@]}"; do
|
||||
state=$(echo "$STATUS_JSON" | python3 -c "
|
||||
import json, sys
|
||||
d = json.load(sys.stdin)
|
||||
pattern = '$check_pattern'
|
||||
# 在 statuses 中找到匹配的最新状态
|
||||
target = None
|
||||
for s in d.get('statuses', []):
|
||||
if pattern in s.get('context', ''):
|
||||
target = s
|
||||
break # status 接口返回的是每个 context 的最新状态,取第一个匹配即可
|
||||
if target:
|
||||
print(target.get('state', 'unknown'))
|
||||
else:
|
||||
print('not_found')
|
||||
")
|
||||
|
||||
if [ "$state" = "success" ]; then
|
||||
echo " ✅ $check_pattern: $state"
|
||||
else
|
||||
echo " ❌ $check_pattern: $state"
|
||||
all_passed=false
|
||||
failed_checks="$failed_checks $check_pattern($state)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$all_passed" != "true" ]; then
|
||||
echo " ⏭️ Skipping - CI not passed:$failed_checks"
|
||||
skip_count=$((skip_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# CI 全部通过,执行合并
|
||||
echo " 🚀 All CI checks passed, merging..."
|
||||
echo "$PRS" | while IFS='|' read -r number title mergeable; do
|
||||
echo "Merging PR #$number: $title"
|
||||
RESULT=$(curl -s -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$GITEA_API/repos/$REPO/pulls/$number/merge" \
|
||||
-d '{"Do": "merge"}')
|
||||
|
||||
if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get('merged', False) or 'id' in d else 1)" 2>/dev/null; then
|
||||
-d '{\"merge_method\": \"merge\"}')
|
||||
|
||||
if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if 'id' in d else 1)"; then
|
||||
echo " ✅ PR #$number merged successfully"
|
||||
merge_count=$((merge_count + 1))
|
||||
else
|
||||
echo " ❌ PR #$number merge failed"
|
||||
# 提取错误信息
|
||||
err_msg=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('message', str(d)[:200]))" 2>/dev/null)
|
||||
echo " Error: $err_msg"
|
||||
echo " ❌ PR #$number failed: $RESULT"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Merged: $merge_count | Skipped: $skip_count"
|
||||
|
||||
@@ -30,13 +30,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
@@ -102,37 +99,30 @@ def extract_upgrade_content(content: str) -> str:
|
||||
|
||||
def get_new_migrations_via_diff(diff_target: str) -> List[Path]:
|
||||
"""
|
||||
通过 Gitea API 对比目标分支,找出 alembic/versions/ 下新增的迁移文件。
|
||||
不依赖本地 git,避免 CI 环境下 git 操作不稳定的问题。
|
||||
通过 git diff 对比目标分支/commit,找出 alembic/versions/ 下新增的迁移文件。
|
||||
只包含新增文件(A状态),不包含修改或删除的文件。
|
||||
"""
|
||||
api_url = os.environ.get("GITHUB_API_URL", "")
|
||||
repo = os.environ.get("GITHUB_REPOSITORY", "")
|
||||
token = os.environ.get("GITHUB_TOKEN", "")
|
||||
branch = diff_target.replace("origin/", "")
|
||||
|
||||
if not api_url or not repo or not token:
|
||||
print("⚠️ CI 环境变量不完整,降级为检查所有迁移文件")
|
||||
return sorted(ALEMBIC_VERSIONS_DIR.glob("*.py"))
|
||||
|
||||
try:
|
||||
url = f"{api_url}/repos/{repo}/contents/alembic/versions?ref={branch}"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
|
||||
remote_files = {item["name"] for item in data if item["name"].endswith(".py")}
|
||||
local_files = {f.name for f in ALEMBIC_VERSIONS_DIR.glob("*.py")}
|
||||
new_file_names = sorted(local_files - remote_files)
|
||||
|
||||
if new_file_names:
|
||||
result = [ALEMBIC_VERSIONS_DIR / f for f in new_file_names]
|
||||
print(f" (API 对比 {branch} 分支,发现 {len(result)} 个新增迁移)")
|
||||
return result
|
||||
else:
|
||||
print(f" (API 对比 {branch} 分支,无新增迁移)")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"⚠️ API 获取迁移列表失败:{e}")
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--name-only",
|
||||
"--diff-filter=A",
|
||||
diff_target,
|
||||
"HEAD",
|
||||
"--",
|
||||
"alembic/versions/",
|
||||
],
|
||||
cwd=str(REPO_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
files = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
||||
return [REPO_ROOT / f for f in files]
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"⚠️ git diff 失败({diff_target}):{e.stderr.strip()}")
|
||||
print(" 降级为检查所有迁移文件")
|
||||
return sorted(ALEMBIC_VERSIONS_DIR.glob("*.py"))
|
||||
|
||||
|
||||
@@ -54,38 +54,38 @@ echo ""
|
||||
echo "Image pushed: ${IMAGE_TAG}"
|
||||
echo "Local cache updated"
|
||||
|
||||
# DISABLED: registry cache too slow echo ""
|
||||
# DISABLED: registry cache too slow echo "=== Step 2: Sync registry cache (best effort, retries 3x) ==="
|
||||
# DISABLED: registry cache too slow CACHE_TO_REGISTRY="type=registry,ref=${CACHE_REF},mode=max,compression=zstd"
|
||||
# DISABLED: registry cache too slow
|
||||
# DISABLED: registry cache too slow MAX_RETRIES=3
|
||||
# DISABLED: registry cache too slow SUCCESS=0
|
||||
# DISABLED: registry cache too slow for attempt in $(seq 1 $MAX_RETRIES); do
|
||||
# DISABLED: registry cache too slow echo "Registry cache sync attempt $attempt/$MAX_RETRIES"
|
||||
# DISABLED: registry cache too slow if docker buildx build \
|
||||
# DISABLED: registry cache too slow $BUILD_ARGS \
|
||||
# DISABLED: registry cache too slow --cache-from "${CACHE_FROM_LOCAL}" \
|
||||
# DISABLED: registry cache too slow --cache-to "${CACHE_TO_REGISTRY}" \
|
||||
# DISABLED: registry cache too slow -f "${DOCKERFILE}" \
|
||||
# DISABLED: registry cache too slow -t "${IMAGE_TAG}" \
|
||||
# DISABLED: registry cache too slow --push \
|
||||
# DISABLED: registry cache too slow .; then
|
||||
# DISABLED: registry cache too slow echo "Registry cache synced (attempt $attempt)"
|
||||
# DISABLED: registry cache too slow SUCCESS=1
|
||||
# DISABLED: registry cache too slow break
|
||||
# DISABLED: registry cache too slow else
|
||||
# DISABLED: registry cache too slow echo "Registry cache sync failed (attempt $attempt)"
|
||||
# DISABLED: registry cache too slow if [ $attempt -lt $MAX_RETRIES ]; then
|
||||
# DISABLED: registry cache too slow WAIT=$((attempt * 5))
|
||||
# DISABLED: registry cache too slow echo "Retrying in ${WAIT}s..."
|
||||
# DISABLED: registry cache too slow sleep $WAIT
|
||||
# DISABLED: registry cache too slow fi
|
||||
# DISABLED: registry cache too slow fi
|
||||
# DISABLED: registry cache too slow done
|
||||
# DISABLED: registry cache too slow
|
||||
# DISABLED: registry cache too slow if [ $SUCCESS -eq 0 ]; then
|
||||
# DISABLED: registry cache too slow echo "WARNING: Registry cache sync failed after $MAX_RETRIES attempts (non-fatal, local cache still works)"
|
||||
# DISABLED: registry cache too slow fi
|
||||
echo ""
|
||||
echo "=== Step 2: Sync registry cache (best effort, retries 3x) ==="
|
||||
CACHE_TO_REGISTRY="type=registry,ref=${CACHE_REF},mode=max,compression=zstd"
|
||||
|
||||
MAX_RETRIES=3
|
||||
SUCCESS=0
|
||||
for attempt in $(seq 1 $MAX_RETRIES); do
|
||||
echo "Registry cache sync attempt $attempt/$MAX_RETRIES"
|
||||
if docker buildx build \
|
||||
$BUILD_ARGS \
|
||||
--cache-from "${CACHE_FROM_LOCAL}" \
|
||||
--cache-to "${CACHE_TO_REGISTRY}" \
|
||||
-f "${DOCKERFILE}" \
|
||||
-t "${IMAGE_TAG}" \
|
||||
--push \
|
||||
.; then
|
||||
echo "Registry cache synced (attempt $attempt)"
|
||||
SUCCESS=1
|
||||
break
|
||||
else
|
||||
echo "Registry cache sync failed (attempt $attempt)"
|
||||
if [ $attempt -lt $MAX_RETRIES ]; then
|
||||
WAIT=$((attempt * 5))
|
||||
echo "Retrying in ${WAIT}s..."
|
||||
sleep $WAIT
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $SUCCESS -eq 0 ]; then
|
||||
echo "WARNING: Registry cache sync failed after $MAX_RETRIES attempts (non-fatal, local cache still works)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Build completed: ${IMAGE_TAG}"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
# mypy憓鮋��急��𡁏𧋦 - CI銝剛���
|
||||
# �臬��㗛�: SCAN_MODE, CHANGED_PY_FILES
|
||||
|
||||
set +e
|
||||
|
||||
echo "=== Installing mypy ==="
|
||||
python3 -m pip install -q mypy
|
||||
mypy --version
|
||||
echo ""
|
||||
echo "=== Running mypy type check (advisory mode) ==="
|
||||
echo "�𡃏郎璅∪�嚗䔶裊�餅鱏CI"
|
||||
echo ""
|
||||
|
||||
MYPY_COMMON_ARGS="--ignore-missing-imports --no-site-packages --no-strict-optional --explicit-package-bases --exclude tests/|test_|migrations/|alembic/ --no-error-summary --incremental --cache-dir .mypy_cache"
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
if [ "$SCAN_MODE" = "incremental" ] && [ -n "$CHANGED_PY_FILES" ]; then
|
||||
echo "=== Incremental mypy scan (PR mode) ==="
|
||||
echo "Changed files: $(echo $CHANGED_PY_FILES | wc -w) files"
|
||||
MYPY_FILES=""
|
||||
for f in $CHANGED_PY_FILES; do
|
||||
case "$f" in
|
||||
apps/*|packages/*)
|
||||
MYPY_FILES="$MYPY_FILES $f"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$MYPY_FILES" ]; then
|
||||
echo "Checking: $MYPY_FILES"
|
||||
mypy $MYPY_FILES $MYPY_COMMON_ARGS 2>&1 | head -80 || EXIT_CODE=$?
|
||||
else
|
||||
echo "No mypy-checkable files changed, skipping"
|
||||
fi
|
||||
else
|
||||
echo "=== Full mypy scan ==="
|
||||
mypy apps/api/app packages $MYPY_COMMON_ARGS 2>&1 | head -60 || EXIT_CODE=$?
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ "$EXIT_CODE" != "0" ]; then
|
||||
echo "mypy �𤑳緵蝐餃��桅�嚗��霅行芋撘𧶏�銝俛獈�哨�"
|
||||
echo "撱箄悅�𡒊賒�鞉郊靽桀�"
|
||||
else
|
||||
echo "mypy 蝐餃�璉��仿�朞�"
|
||||
fi
|
||||
exit 0
|
||||
@@ -7,7 +7,7 @@
|
||||
# 环境变量:
|
||||
# IMAGE_TAG - 镜像版本 tag(如 commit SHA 或分支名)
|
||||
# REGISTRY_TOKEN - Registry 访问令牌
|
||||
# REGISTRY - Registry 地址(默认 xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji)
|
||||
# REGISTRY - Registry 地址(默认 git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas)
|
||||
# REGISTRY_USER - Registry 用户名(默认 xiaoxia)
|
||||
# ENV_FILE - 环境变量文件路径
|
||||
# GENERATED_DIR - 生成文件目录
|
||||
@@ -16,9 +16,9 @@
|
||||
set -eu
|
||||
|
||||
IMAGE_TAG="${IMAGE_TAG:-}"
|
||||
REGISTRY="${REGISTRY:-xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji}"
|
||||
REGISTRY_USER="${ACR_USERNAME:-${REGISTRY_USER:-nick0415343655}}"
|
||||
REGISTRY_TOKEN="${ACR_PASSWORD:-${REGISTRY_TOKEN:-}}"
|
||||
REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
|
||||
REGISTRY_USER="${REGISTRY_USER:-xiaoxia}"
|
||||
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-/var/lib/xiaoxia-saas-staging/.env}"
|
||||
GENERATED_DIR="${GENERATED_DIR:-/var/lib/xiaoxia-saas-staging/generated}"
|
||||
|
||||
Reference in New Issue
Block a user