Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(Vercel 部署正确性)
Bug 描述
backend/internal/deploy/vercel_deployer.go:253-304 的 createDeployment 实现:
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
var errResp struct {
Error struct {
Code string `json:\"code\"`
Missing []string `json:\"missing\"`
} `json:\"error\"`
}
if json.Unmarshal(bodyBytes, &errResp) == nil && len(errResp.Error.Missing) > 0 {
return errResp.Error.Missing, nil
}
return nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(bodyBytes))
}
return nil, nil
以及调用方 Deploy (71-142):
missing, err := p.createDeployment(ctx, projectName, fileResults, token)
...
if len(missing) > 0 {
... 上传 missing 集合 ...
// 4. 重新创建部署(文件已上传完毕)
if _, err := p.createDeployment(ctx, projectName, fileResults, token); err != nil {
return fmt.Errorf(\"触发最终部署失败: %w\", err)
}
}
问题细节:
- missing 只从非 2xx 响应解析:
- Vercel v13 在不同场景下,有时会用
200 OK + body 里 id + missing 的形式返回(文档里也暗示了这种情况)。目前代码对 2xx 直接 return nil, nil,默认"没有缺失",但真实情况可能是"有一部分依然缺失"。
- 第二次 createDeployment 仅调用一次,假设 100% 成功:
- 如果第二次调用再度返回 missing(例如有文件上传时网络抖动,服务端没收全),代码不再重试;最终 deploy 就带着缺文件触发了——可能成功也可能半挂。
- 不能识别"部分上传成功但对端认为缺失":
uploadFiles 里单个文件失败会让 errgroup 整组失败,但若服务端因 digest 不匹配认为"没收到",客户端并不会知道。
- projectSettings.framework 写死为 nil(258-263 行):用户若想以"Next.js 项目"部署,强制走 static 会出错。
复现步骤
- 对一个较大的站(几十 MB)发布 Vercel。
- 模拟部分文件上传超时 / 被服务端拒绝(比如文件名含特殊字符)。
- 观察:Deploy 最终可能假装成功("✅ Vercel 部署成功"),但访问站点时部分页面 404。
或更直接:
- 配合一个代理,把 1 个
/v2/files PUT 改成 502 响应。
uploadSingleFile 返回错误,但因为 p.uploadFiles 里的 errgroup 已经接收了这条错误,其它上传也被取消。
- 最终整盘失败——但如果把 502 改成 200 但 body 清空,服务端 digest 校验失败,客户端就看不见问题。
期望行为
createDeployment 无论 2xx / 非 2xx,都尝试解析 missing 字段;有即返回,没有即 ok。
Deploy 改为循环最多 N 次(例如 3 次):每次基于上次返回的 missing 做差量上传,直到 missing 为空或达到上限。
- 每轮之间失败的文件要能报告给用户。
- framework 由设置决定,而非硬编码。
实际行为
解析脆弱、只重试一次、大文件 / 网络差的场景下可能静默漏文件。
截图 / 日志
无。
补充信息 — 最佳解决方案
重构 createDeployment 与 Deploy
type createDeployResp struct {
Id string `json:\"id\"`
Missing []string `json:\"missing\"`
}
func (p *VercelProvider) createDeployment(ctx context.Context, project string, files []VercelFileResult, token string, fw *string) (*createDeployResp, error) {
...
// 无论 2xx/非 2xx,都解析 body
var top struct {
Id string `json:\"id\"`
Missing []string `json:\"missing\"`
Error *struct {
Code string `json:\"code\"`
Missing []string `json:\"missing\"`
Message string `json:\"message\"`
} `json:\"error\"`
}
_ = json.Unmarshal(bodyBytes, &top)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return &createDeployResp{Id: top.Id, Missing: top.Missing}, nil
}
// 非 2xx,若有 missing 按特殊情况返回
if top.Error != nil && len(top.Error.Missing) > 0 {
return &createDeployResp{Missing: top.Error.Missing}, nil
}
// 真正的错误
return nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(bodyBytes))
}
Deploy 循环:
const maxRounds = 3
for round := 0; round < maxRounds; round++ {
resp, err := p.createDeployment(ctx, projectName, fileResults, token, framework)
if err != nil {
return err
}
if len(resp.Missing) == 0 {
break
}
logger(fmt.Sprintf(\"第 %d 轮:需要上传 %d 个文件\", round+1, len(resp.Missing)))
if err := p.uploadMissing(ctx, outputDir, fileResults, resp.Missing, token); err != nil {
return err
}
if round == maxRounds-1 {
return fmt.Errorf(\"Vercel 始终有 %d 个文件未能入库,请重试\", len(resp.Missing))
}
}
其它改动
单测
- 伪造 API:第一次返回
{ id:"", missing:[sha1,sha2] } → 第二次返回 { id:"dep_xx" } → 验证流程成功 + 仅上传 2 文件。
- 连续 3 轮都返回 missing → 验证报错。
- 2xx 响应里含 missing → 验证能识别。
影响文件:
backend/internal/deploy/vercel_deployer.go
backend/internal/domain/setting.go(framework 字段)
- 新增单测
vercel_deployer_test.go
检查清单
Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(Vercel 部署正确性)
Bug 描述
backend/internal/deploy/vercel_deployer.go:253-304的createDeployment实现:以及调用方
Deploy(71-142):问题细节:
200 OK+ body 里id + missing的形式返回(文档里也暗示了这种情况)。目前代码对 2xx 直接return nil, nil,默认"没有缺失",但真实情况可能是"有一部分依然缺失"。uploadFiles里单个文件失败会让 errgroup 整组失败,但若服务端因 digest 不匹配认为"没收到",客户端并不会知道。复现步骤
或更直接:
/v2/filesPUT 改成 502 响应。uploadSingleFile返回错误,但因为p.uploadFiles里的 errgroup 已经接收了这条错误,其它上传也被取消。期望行为
createDeployment无论 2xx / 非 2xx,都尝试解析missing字段;有即返回,没有即 ok。Deploy改为循环最多 N 次(例如 3 次):每次基于上次返回的 missing 做差量上传,直到 missing 为空或达到上限。实际行为
解析脆弱、只重试一次、大文件 / 网络差的场景下可能静默漏文件。
截图 / 日志
无。
补充信息 — 最佳解决方案
重构
createDeployment与DeployDeploy循环:其它改动
projectSettings.framework:新增 setting 字段vercelFramework(static/next/nuxt/...)允许用户选择,默认static(字符串而非 nil)。uploadFiles内部失败不再直接中断 errgroup,改为收集[]UploadFailure让上层决定重试(和 CDN issue [Bug] CDN 批量上传单文件失败被静默吞错,部署声称成功但线上图片可能大面积 404 #44 同样的思路);此时配合 issue [Bug] Vercel missing 文件解析脆弱,二次上传无循环校验,部分大文件可能被吞 #48 的 retry helper 更稳。单测
{ id:"", missing:[sha1,sha2] }→ 第二次返回{ id:"dep_xx" }→ 验证流程成功 + 仅上传 2 文件。影响文件:
backend/internal/deploy/vercel_deployer.gobackend/internal/domain/setting.go(framework 字段)vercel_deployer_test.go检查清单