Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1abab50582 | |||
| faf018030f | |||
| cffe84c764 | |||
| ec5b29197e | |||
| 5e3a3099cf | |||
| d9f9bce1bd |
@@ -639,19 +639,84 @@ def get_draft_plan_id(
|
||||
template_id: str,
|
||||
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db_session),
|
||||
) -> str:
|
||||
"""
|
||||
路径依赖:根据 template_id 获取或创建草稿,返回 plan_id。
|
||||
|
||||
这是模板编辑器路由的核心依赖——所有编辑器端点都先经过这里,
|
||||
确保 template_id → plan_id 的映射始终存在。
|
||||
|
||||
兼容策略:优先从新模板系统(edit_templates 表)查找,
|
||||
若不存在则回退到旧模板系统(templates 表),确保用户自建模板可用。
|
||||
"""
|
||||
tpl_svc, _ = services
|
||||
draft = tpl_svc.get_or_create_draft(
|
||||
template_id,
|
||||
user_id=str(current_user.user.id),
|
||||
tpl_svc, plan_svc = services
|
||||
user_id = str(current_user.user.id)
|
||||
|
||||
# 1. 草稿已存在 → 直接返回
|
||||
draft = tpl_svc.get_template_draft(template_id)
|
||||
if draft is not None:
|
||||
return draft.id
|
||||
|
||||
# 2. 新系统有模板 → 用新服务创建草稿
|
||||
if tpl_svc.get_template(template_id) is not None:
|
||||
draft = tpl_svc.create_template_draft(template_id, user_id=user_id)
|
||||
return draft.id
|
||||
|
||||
# 3. 回退到旧模板系统(templates 表)
|
||||
old_repo = SQLAlchemyTemplateRepository(db)
|
||||
old_template = old_repo.get(template_id, user_id=user_id)
|
||||
if old_template is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
|
||||
|
||||
# 4. 基于旧模板创建草稿计划
|
||||
from app.services.plan_generator_service import PlanGeneratorService
|
||||
|
||||
from packages.domain.edit_template import EditTemplate, EditTemplateStatus
|
||||
from packages.domain.template_clip_config import ClipType, TemplateClipConfig
|
||||
|
||||
# 构造伪 EditTemplate 对象(只填 generate_from_template 需要的字段)
|
||||
pseudo_template = EditTemplate(
|
||||
id=old_template.id,
|
||||
name=old_template.name,
|
||||
editing_mode=old_template.mode,
|
||||
status=EditTemplateStatus.ACTIVE,
|
||||
)
|
||||
return draft.id
|
||||
|
||||
# 将旧模板 segments 转换为 clip_configs
|
||||
clip_configs: list[TemplateClipConfig] = []
|
||||
for seg in old_template.segments or []:
|
||||
clip_configs.append(
|
||||
TemplateClipConfig(
|
||||
id=f"seg_{seg.id}",
|
||||
template_id=old_template.id,
|
||||
clip_type=ClipType.MAIN,
|
||||
order=seg.segment_order,
|
||||
min_duration=seg.duration_min,
|
||||
max_duration=seg.duration_max,
|
||||
)
|
||||
)
|
||||
|
||||
generator = PlanGeneratorService(db)
|
||||
result = generator.generate_from_template(
|
||||
template=pseudo_template,
|
||||
clip_configs=clip_configs,
|
||||
asset_ids=[],
|
||||
created_by_user_id=user_id,
|
||||
name=f"{old_template.name} - 草稿",
|
||||
)
|
||||
plan = result["plan"]
|
||||
|
||||
# 标记为模板草稿(后续可复用 tpl_svc.get_template_draft 的查找逻辑)
|
||||
plan_svc.update_plan_config(plan.id, {"is_template_draft": True})
|
||||
|
||||
logger.info(
|
||||
"旧模板自动创建草稿: template_id=%s draft_plan_id=%s user_id=%s",
|
||||
template_id,
|
||||
plan.id,
|
||||
user_id,
|
||||
)
|
||||
return plan.id
|
||||
|
||||
|
||||
# ── 草稿核心端点 ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -20,24 +20,56 @@ export const useLogin = () => {
|
||||
const data = await mutation.mutateAsync(credentials)
|
||||
const refreshToken = data.refresh_token ?? null
|
||||
|
||||
// 先保存 token
|
||||
localStorage.setItem("access_token", data.access_token)
|
||||
if (refreshToken) {
|
||||
localStorage.setItem("refresh_token", refreshToken)
|
||||
} else {
|
||||
localStorage.removeItem("refresh_token")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await authApi.getCurrentUser()
|
||||
setAuth(user, data.access_token, refreshToken)
|
||||
navigate("/")
|
||||
|
||||
// 跳转到登录前页面或首页
|
||||
const redirect = localStorage.getItem("login_redirect") || "/"
|
||||
localStorage.removeItem("login_redirect")
|
||||
navigate(redirect, { replace: true })
|
||||
return data
|
||||
}
|
||||
|
||||
return { ...mutation, mutateAsync: login }
|
||||
}
|
||||
|
||||
// 微信登录 Hook(用于回调后处理登录状态)
|
||||
export const useWechatCallback = () => {
|
||||
const navigate = useNavigate()
|
||||
const setAuth = useAuthStore((state) => state.setAuth)
|
||||
const setUser = useAuthStore((state) => state.setUser)
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ code, state }: { code: string; state: string }) =>
|
||||
authApi.wechatCallback(code, state),
|
||||
})
|
||||
|
||||
const handleCallback = async (code: string, state: string) => {
|
||||
// 校验 state
|
||||
const savedState = localStorage.getItem("wechat_state")
|
||||
if (!savedState || savedState !== state) {
|
||||
throw new Error("安全校验失败")
|
||||
}
|
||||
localStorage.removeItem("wechat_state")
|
||||
|
||||
const result = await mutation.mutateAsync({ code, state })
|
||||
const user = await authApi.getCurrentUser()
|
||||
setAuth(user, result.access_token, result.refresh_token)
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
// 绑定成功后跳转
|
||||
const finishLogin = () => {
|
||||
const redirect = localStorage.getItem("login_redirect") || "/"
|
||||
localStorage.removeItem("login_redirect")
|
||||
navigate(redirect, { replace: true })
|
||||
}
|
||||
|
||||
return { ...mutation, handleCallback, finishLogin, setUser }
|
||||
}
|
||||
|
||||
// 注册 Hook
|
||||
export const useRegister = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -41,6 +41,13 @@ const Login: React.FC = () => {
|
||||
const result = await getWechatAuthUrl()
|
||||
// 保存 state 到 localStorage 用于回调时验证
|
||||
localStorage.setItem("wechat_state", result.state)
|
||||
// 记录登录前的来源页,登录成功后跳回
|
||||
const from = window.location.pathname + window.location.search
|
||||
if (from !== "/login" && from !== "/register") {
|
||||
localStorage.setItem("login_redirect", from)
|
||||
} else {
|
||||
localStorage.removeItem("login_redirect")
|
||||
}
|
||||
// 跳转到微信授权页
|
||||
window.location.href = result.auth_url
|
||||
} catch (error) {
|
||||
|
||||
@@ -28,15 +28,16 @@ const WechatCallback: React.FC = () => {
|
||||
|
||||
const handleCallback = async () => {
|
||||
try {
|
||||
const result = await wechatCallback(code, state)
|
||||
|
||||
// 保存 token
|
||||
localStorage.setItem("access_token", result.access_token)
|
||||
if (result.refresh_token) {
|
||||
localStorage.setItem("refresh_token", result.refresh_token)
|
||||
} else {
|
||||
localStorage.removeItem("refresh_token")
|
||||
// 校验 state,防止 CSRF
|
||||
const savedState = localStorage.getItem("wechat_state")
|
||||
if (!savedState || savedState !== state) {
|
||||
setError("安全校验失败,请重新登录")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
localStorage.removeItem("wechat_state")
|
||||
|
||||
const result = await wechatCallback(code, state)
|
||||
|
||||
// 获取用户信息
|
||||
const userData = await getCurrentUser()
|
||||
@@ -44,9 +45,11 @@ const WechatCallback: React.FC = () => {
|
||||
setAuth(user, result.access_token, result.refresh_token)
|
||||
|
||||
if (result.binding_complete) {
|
||||
// 已绑定,直接跳转到首页
|
||||
// 已绑定,跳转到登录前页面或首页
|
||||
message.success("登录成功")
|
||||
navigate("/app/dashboard")
|
||||
const redirect = localStorage.getItem("login_redirect") || "/"
|
||||
localStorage.removeItem("login_redirect")
|
||||
navigate(redirect, { replace: true })
|
||||
} else {
|
||||
// 未绑定,显示绑定弹窗
|
||||
setLoading(false)
|
||||
@@ -61,10 +64,14 @@ const WechatCallback: React.FC = () => {
|
||||
handleCallback()
|
||||
}, [searchParams, navigate, setAuth])
|
||||
|
||||
const handleBindSuccess = (_user: User) => {
|
||||
const handleBindSuccess = (user: User) => {
|
||||
const setUser = useAuthStore.getState().setUser
|
||||
setUser(user)
|
||||
setShowBindModal(false)
|
||||
message.success("绑定成功")
|
||||
navigate("/app/dashboard")
|
||||
const redirect = localStorage.getItem("login_redirect") || "/"
|
||||
localStorage.removeItem("login_redirect")
|
||||
navigate(redirect, { replace: true })
|
||||
}
|
||||
|
||||
const handleBindCancel = () => {
|
||||
|
||||
@@ -7,8 +7,18 @@ import { renderHook, act } from "@testing-library/react"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
const mockSetAuth = vi.fn()
|
||||
const mockClearAuth = vi.fn()
|
||||
const mockSetAuth = vi.fn((_user: any, accessToken: string, refreshToken?: string | null) => {
|
||||
localStorage.setItem("access_token", accessToken)
|
||||
if (refreshToken) {
|
||||
localStorage.setItem("refresh_token", refreshToken)
|
||||
} else {
|
||||
localStorage.removeItem("refresh_token")
|
||||
}
|
||||
})
|
||||
const mockClearAuth = vi.fn(() => {
|
||||
localStorage.removeItem("access_token")
|
||||
localStorage.removeItem("refresh_token")
|
||||
})
|
||||
const mockMutateAsync = vi.fn()
|
||||
const mockQueryClear = vi.fn()
|
||||
|
||||
|
||||
+55
-18
@@ -1,33 +1,69 @@
|
||||
# ============================================================
|
||||
# API Dockerfile - 专门用于 FastAPI 应用
|
||||
# 优化:依赖分层缓存 + 多阶段构建基础层
|
||||
# API Dockerfile - FastAPI 应用
|
||||
# 优化:多阶段构建 + pip cache mount + 依赖分层缓存
|
||||
# ============================================================
|
||||
|
||||
# 基础镜像:Python 3.12
|
||||
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim
|
||||
# ==================== Builder 阶段 ====================
|
||||
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim AS builder
|
||||
|
||||
# 使用阿里云镜像加速
|
||||
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
|
||||
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
|
||||
|
||||
# 安装编译依赖(仅 builder 需要)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 创建虚拟环境
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# ---- 依赖分层:基础依赖(变化少,缓存命中率高)----
|
||||
COPY requirements-base.txt /tmp/requirements-base.txt
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
||||
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
-r /tmp/requirements-base.txt \
|
||||
&& rm /tmp/requirements-base.txt
|
||||
|
||||
# ---- 依赖分层:业务依赖(变化频繁)----
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
||||
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
-r /tmp/requirements.txt \
|
||||
&& rm /tmp/requirements.txt
|
||||
|
||||
# ---- Python 依赖瘦身 ----
|
||||
RUN find /opt/venv -name "*.so" -type f -exec strip --strip-all {} \; 2>/dev/null || true
|
||||
RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
|
||||
find /opt/venv -name "*.pyc" -delete 2>/dev/null || true
|
||||
|
||||
# ==================== Runtime 阶段 ====================
|
||||
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim AS runtime
|
||||
|
||||
# 构建参数:版本号(CI 传入 commit hash)
|
||||
ARG APP_VERSION=dev
|
||||
|
||||
# 使用阿里云镜像加速
|
||||
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
|
||||
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
|
||||
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||
# 只装运行时需要的库(libpq5 是 psycopg2 运行时依赖)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 从 builder 复制虚拟环境
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# ---- 依赖分层:基础依赖(变化少,缓存命中率高)----
|
||||
COPY requirements-base.txt /tmp/requirements-base.txt
|
||||
|
||||
RUN python -m venv /opt/venv && /opt/venv/bin/pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r /tmp/requirements-base.txt && rm /tmp/requirements-base.txt
|
||||
|
||||
# ---- 依赖分层:业务依赖(变化频繁)----
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
|
||||
RUN /opt/venv/bin/pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r /tmp/requirements.txt && rm /tmp/requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY apps/api/ /app/apps/api/
|
||||
COPY packages/ /app/packages/
|
||||
@@ -43,7 +79,8 @@ ENV PYTHONUNBUFFERED=1
|
||||
ENV APP_VERSION=$APP_VERSION
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5)"
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5)"
|
||||
|
||||
# API 入口点
|
||||
WORKDIR /app/apps/api
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# ============================================================
|
||||
# Worker Dockerfile - 优化版(多阶段构建 + 镜像瘦身)
|
||||
# Worker Dockerfile - 优化版(多阶段构建 + 镜像瘦身 + cache mount加速)
|
||||
# 优化项:
|
||||
# 1. 多阶段构建:builder 阶段安装编译依赖,runtime 阶段只保留运行时
|
||||
# 2. ffmpeg 静态编译替换:从 apt 安装(457MB) 改为静态二进制(~80MB)
|
||||
# 3. Python 依赖瘦身:strip .so 调试符号 + 清理测试文件 + 清理缓存
|
||||
# 4. pip cache mount:加速依赖下载(跨构建共享pip wheel缓存)
|
||||
# 5. ffmpeg cache mount:避免每次重新下载静态编译包
|
||||
# ============================================================
|
||||
|
||||
# ==================== Builder 阶段 ====================
|
||||
@@ -23,11 +25,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ---- 下载静态编译 ffmpeg ----
|
||||
# 使用 johnvansickle.com 的静态编译版本(业界标准)
|
||||
RUN cd /tmp \
|
||||
&& wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz \
|
||||
&& tar xf ffmpeg-release-amd64-static.tar.xz \
|
||||
# ---- 下载静态编译 ffmpeg(带缓存,避免每次重新下载)----
|
||||
RUN --mount=type=cache,target=/tmp/ffmpeg-cache,sharing=locked \
|
||||
cd /tmp \
|
||||
&& if [ ! -f /tmp/ffmpeg-cache/ffmpeg-release-amd64-static.tar.xz ]; then \
|
||||
wget -q -O /tmp/ffmpeg-cache/ffmpeg-release-amd64-static.tar.xz \
|
||||
https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz; \
|
||||
fi \
|
||||
&& tar xf /tmp/ffmpeg-cache/ffmpeg-release-amd64-static.tar.xz \
|
||||
&& cp ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/ffmpeg \
|
||||
&& cp ffmpeg-*-amd64-static/ffprobe /usr/local/bin/ffprobe \
|
||||
&& chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe \
|
||||
@@ -42,19 +47,22 @@ ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# 基础依赖
|
||||
COPY requirements-base.txt /tmp/requirements-base.txt
|
||||
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
||||
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
-r /tmp/requirements-base.txt \
|
||||
&& rm /tmp/requirements-base.txt
|
||||
|
||||
# Worker 专属大包
|
||||
COPY requirements-worker.txt /tmp/requirements-worker.txt
|
||||
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
||||
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
-r /tmp/requirements-worker.txt \
|
||||
&& rm /tmp/requirements-worker.txt
|
||||
|
||||
# 业务依赖
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
||||
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
|
||||
-r /tmp/requirements.txt \
|
||||
&& rm /tmp/requirements.txt
|
||||
|
||||
|
||||
@@ -25,51 +25,47 @@ def run(cmd, check=True, capture=True, cwd=None):
|
||||
|
||||
|
||||
def ensure_git_repo(api_url, repo, token, pr_number):
|
||||
"""确保当前目录是git仓库,并切换到PR源分支。
|
||||
"""确保当前目录是git仓库,保持在 merge commit 状态。
|
||||
|
||||
checkout脚本用tarball方式下载代码(PR merge后的commit),没有.git目录。
|
||||
这里自动初始化git仓库,fetch PR源分支并强制checkout,
|
||||
使工作区变为PR源分支的代码,确保后续格式化修复基于源分支。
|
||||
这里自动初始化git仓库,但不切换分支,保持在merge commit状态,
|
||||
使格式化检查与 Frontend Lint 的范围完全一致。
|
||||
"""
|
||||
if os.path.exists(".git"):
|
||||
return
|
||||
|
||||
print("检测到tarball checkout(无.git目录),自动初始化git仓库...")
|
||||
print("保持在 merge commit 状态(与 Frontend Lint 一致)")
|
||||
|
||||
# 构造带认证的远端URL
|
||||
server_url = api_url.rsplit("/api/v1", 1)[0]
|
||||
remote_url = f"{server_url.replace('https://', f'https://x-access-token:{token}@')}/{repo}.git"
|
||||
|
||||
# 获取PR的源分支
|
||||
# 获取PR的源分支(仅用于后续推送,不checkout)
|
||||
pr_api_url = f"{api_url}/repos/{repo}/pulls/{pr_number}"
|
||||
req_obj = urllib.request.Request(pr_api_url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req_obj) as resp:
|
||||
pr = json.loads(resp.read())
|
||||
head_branch = pr["head"]["ref"]
|
||||
|
||||
print(f"PR源分支: {head_branch}")
|
||||
print(f"PR源分支: {head_branch}(仅用于推送,不切换)")
|
||||
|
||||
# 初始化git
|
||||
# 初始化git(不fetch/checkout,保持tarball内容即merge commit状态)
|
||||
run("git init -q")
|
||||
run(f"git remote add origin {remote_url}")
|
||||
run('git config user.name "CI Bot"')
|
||||
run('git config user.email "ci-bot@xiaoxiajianji.com"')
|
||||
|
||||
# fetch源分支(浅克隆,只要最新commit)
|
||||
print("fetch源分支...")
|
||||
run(f"git fetch --depth=1 origin {head_branch}")
|
||||
|
||||
# 强制checkout到源分支(覆盖tarball内容)
|
||||
# tarball是merge后的commit,源分支才是我们要修改并推送的目标
|
||||
print("切换到源分支...")
|
||||
run(f"git checkout -f -B {head_branch} FETCH_HEAD")
|
||||
# 将当前目录(tarball解压的merge commit)作为初始commit
|
||||
run("git add -A")
|
||||
run('git commit -q -m "CI merge commit snapshot"')
|
||||
|
||||
result = run("git status --porcelain")
|
||||
if result.stdout.strip():
|
||||
n = len(result.stdout.strip().splitlines())
|
||||
print(f"⚠️ 工作区有 {n} 个未追踪文件")
|
||||
print(f"⚠️ 工作区有 {n} 个未追踪文件")
|
||||
else:
|
||||
print("✅ git仓库就绪,工作区clean")
|
||||
print("✅ git仓库就绪(merge commit 状态)")
|
||||
|
||||
return head_branch
|
||||
|
||||
@@ -236,24 +232,39 @@ def main():
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
print(f" {line}")
|
||||
|
||||
# 提交修复
|
||||
# 提交修复(在 merge commit 状态)
|
||||
run("git add -A")
|
||||
run('git commit -m "style: auto-format with black + isort + prettier [ci skip]"')
|
||||
run('git commit -m "style: auto-format with black + isort + prettier"')
|
||||
|
||||
# 推送(head_branch已从ensure_git_repo获取)
|
||||
# 记录格式修复的 commit hash
|
||||
format_commit = run("git rev-parse HEAD").stdout.strip()
|
||||
|
||||
# 推送:将格式化变更推回 PR 源分支
|
||||
print(f"\nPR来源分支: {head_branch}")
|
||||
|
||||
# 切换到源分支,cherry-pick 格式化修复
|
||||
print(f"fetch 源分支 {head_branch}...")
|
||||
run(f"git fetch --depth=1 origin {head_branch}")
|
||||
run(f"git checkout -f -B {head_branch} FETCH_HEAD")
|
||||
|
||||
print(f"cherry-pick 格式化修复到源分支...")
|
||||
cherry_result = run(f"git cherry-pick {format_commit}", check=False)
|
||||
if cherry_result.returncode != 0:
|
||||
print(f"cherry-pick 冲突(源分支可能已有不同格式),中止cherry-pick")
|
||||
run("git cherry-pick --abort", check=False)
|
||||
print("没有需要推送的格式化改动")
|
||||
return
|
||||
|
||||
# 推送(保留原有的重试逻辑)
|
||||
print("推送格式修复到远端...")
|
||||
|
||||
# 推送前先 rebase 拉取远端最新,避免快进冲突
|
||||
# 最多重试 3 次:rebase → push,失败则重新拉取再试
|
||||
max_retries = 3
|
||||
push_success = False
|
||||
last_error = ""
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
print(f" 尝试 {attempt}/{max_retries}: 拉取最新代码并推送...")
|
||||
print(f" 尝试 {attempt}/{max_retries}: 推送...")
|
||||
|
||||
# 先拉取远端最新 commit 并 rebase
|
||||
fetch_result = run(f"git fetch origin {head_branch}", check=False)
|
||||
if fetch_result.returncode != 0:
|
||||
last_error = f"git fetch 失败: {fetch_result.stderr.strip()}"
|
||||
@@ -266,10 +277,8 @@ def main():
|
||||
last_error = f"git rebase 失败,中止并重置: {rebase_result.stderr.strip()[:200]}"
|
||||
print(f" {last_error}")
|
||||
run("git rebase --abort", check=False)
|
||||
# rebase 失败通常是冲突,重试没用,直接跳出
|
||||
break
|
||||
|
||||
# 推送
|
||||
push_result = run(f'git push origin "HEAD:{head_branch}"', check=False)
|
||||
if push_result.returncode == 0:
|
||||
push_success = True
|
||||
|
||||
@@ -50,7 +50,6 @@ build_with_cache_retry() {
|
||||
--cache-from "type=local,src=${LOCAL_CACHE_DIR}" \
|
||||
--cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" \
|
||||
--cache-to "type=local,dest=${LOCAL_CACHE_DIR},mode=max" \
|
||||
--cache-to "type=registry,ref=${CACHE_REF},mode=max,ignore-error=true" \
|
||||
-f "${DOCKERFILE}" \
|
||||
-t "${IMAGE_TAG}" \
|
||||
--push \
|
||||
@@ -84,7 +83,6 @@ build_with_cache_retry() {
|
||||
$BUILD_ARGS \
|
||||
--cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" \
|
||||
--cache-to "type=local,dest=${LOCAL_CACHE_DIR},mode=max" \
|
||||
--cache-to "type=registry,ref=${CACHE_REF},mode=max,ignore-error=true" \
|
||||
-f "${DOCKERFILE}" \
|
||||
-t "${IMAGE_TAG}" \
|
||||
--push \
|
||||
|
||||
Reference in New Issue
Block a user