qwen-code PR 证据托管迁移指南:从 Git 分支到阿里云 OSS 的不可变对象前缀设计
2026/9/13 6:48:23 网站建设 项目流程

qwen-code PR 证据托管迁移指南:从 Git 分支到阿里云 OSS 的不可变对象前缀设计

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

导读

本文基于 qwen-code 仓库的设计文档 docs/design/2026-08-25-pr-evidence-oss-hosting.md,讲解自动化 PR 校验链路如何将"截图证据"的托管方式从"写入仓库的pr-assets/*图片分支"迁移为"上传至阿里云 OSS 的不可变对象前缀"。读完本文,你将掌握:两个可信发布者任务(Web Shell 视觉预览发布者与/verify沙箱报告发布者)的完整对象键设计、共享上传器脚本 scripts/upload-aliyun-oss-assets.js 的重试与超时语义、bucket 选择回退规则(ALIYUN_OSS_PR_ASSETS_BUCKET变量),以及遗留 Git 分支的清理与兼容策略。

背景:为什么不能再把截图提交进仓库分支

问题根源:克隆流量与仓库存储的隐性膨胀

此前,自动化的 PR 校验流程把截图直接提交到主仓库的pr-assets/*分支上。一个普通git clone会拉取所有被通告分支可达的对象,因此这些长期存活、且只包含 PNG 图片的分支会给每次克隆增加数百 MB 的传输流量与仓库存储开销——而这些图片与产品源码树毫无关系。对每个 PR 都要克隆仓库的 CI 场景而言,这一成本会随 PR 数量线性放大。

设计目标

迁移方案确立了如下目标(原文 Goals 的完整转述):

  • 保留 Web Shell 视觉预览与/verify报告中的内联截图能力;
  • 停止自动化工作流在该仓库中创建或更新图片分支;
  • 复用现有、已经过测试的阿里云 OSS 上传器与仓库凭据;
  • 保留当前的校验逻辑、大小限制、重试与 fail-safe(降级)行为;
  • 通过可信发布者任务,将 PR 派生字节与 OSS 凭据彻底隔离。

架构总览:可信发布者与共享上传器

上传器脚本

两个可信发布者任务(Web-shell Visuals Publishqwen-triage.yml中的 verify 发布步骤)都配置现有的ossutil客户端,并调用同一个上传器脚本 scripts/upload-aliyun-oss-assets.js。该脚本的关键行为:

  • 三次重试MAX_UPLOAD_ATTEMPTS = 3,首次失败后退避INITIAL_BACKOFF_MS = 2000ms,之后每次翻倍(2s → 4s),全部失败后以fail()退出非零;
  • public-readACL:通过ossutil cp--acl public-read参数发布对象,保证评论里的图片 URL 可公开访问;
  • CLI 参数--bucket--config(ossutil 配置文件路径)、--prefix(目标对象前缀,尾部/会被剥离)、以及一个或多个ASSET本地文件路径;--help/-h可查看用法;
  • 对象键生成${prefix}/${path.basename(asset)},即上传者只负责把每个文件的 basename 拼接到前缀之后。

可选的单次尝试超时(防黑洞连接)

脚本默认不对 ossutil 调用做超时限制(OSS_UPLOAD_ATTEMPT_TIMEOUT_MS未设置时为 0,保持发布同步任务"不设限"的历史行为)。但 PR 发布任务运行在 10 分钟作业上限内,一个"黑洞 socket"(接受连接但永不推进)的上传会把整个报告/评论拖死,因此工作流通过环境变量OSS_UPLOAD_ATTEMPT_TIMEOUT_MS显式开启超时:

  • 工作流中设置为120000(120 秒/次);
  • 超时后以SIGKILL终止该次尝试,并视其为可重试的尝试失败(error.code === 'ETIMEDOUT'不会作为 spawn 错误抛出);
  • 最坏预算:单个图片 3 次尝试 × 120s + 两次退避 ≈ 6.1 分钟,仍落在 10 分钟作业上限内。

幂等去重与同一 PR 的发布串行化

Web-shell Visuals Publish工作流通过concurrency配置按"源仓库 + 分支"(workflow_run.head_repository.full_name+head_branch)为同一个 PR串行化发布,避免旧 run 覆盖新 run 的标记评论;不同 PR(包括恰好共享main分支名的 fork)则分到不同分组、并行发布,且cancel-in-progress: false保证不取消进行中的发布。

不可变对象前缀:为什么三个维度都要进键

Web Shell 预览前缀

pr-assets/web-shell-visuals/<pr>/<head-sha>/<run-id>/<run-attempt>/

例如工作流中实际拼出的ASSET_PREFIX="pr-assets/web-shell-visuals/${PR}/${RUN_HEAD_SHA}/${RUN_ID}/${RUN_ATTEMPT}"(见 .github/workflows/web-shell-visuals-publish.yml)。设计上保证同一个对象键永远不会被写两次,每个维度都有不可替代的作用:

  • head SHA把 URL 绑定到它描绘的代码版本——评论图片的字节所对应的就是该提交;
  • run id防止同一 head 的重跑覆盖掉已发布评论所引用的对象;
  • run attempt(run 序号)单独只有 run id 不够:run id 在重跑尝试之间是稳定的(只有 attempt 递增),所以 attempt 段还能把维护者对同一 workflow run 的手动重跑挡在上一 attempt 的键之外。

之所以必须如此严格,是因为GitHub 通过缓存型图片代理(camo proxy)按 URL 缓存评论图片:如果复用键,评论者会在"字节已被改变"的 URL 上继续看到上一轮 run 的截图。旧的 Git 方案靠 raw URL 中每次 run 唯一的提交 SHA 免费获得了这一属性;迁移到 OSS 后,必须把"每次发布写一个从未被服务过的前缀"作为显式约束落实。

/verify沙箱报告前缀

pr-assets/verify/pr<pr>-<run-id>-<run-attempt>/

实际实现中拼装为pr-assets/verify/pr${PR_NUMBER}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}(见 .github/workflows/qwen-triage.yml)。verify 通道用<run-id>-<attempt>的组合维持同样的不可变性。

校验与降级(fail-safe)行为

  • /verify通道沿用既有上限:至多 8 张 PNG、每张至多 2 MiB。实现上用find -size -2097153c做字节精确上限(-2M单位会先把文件大小向上取整到整 MiB,把文档化的 2 MB 上限静默变成 1 MiB,因此必须用字节单位),再用head -8截取最多 8 张(见 .github/workflows/qwen-triage.yml);
  • PNG magic bytes 校验head -c 889504e470d0a1a0a比对,确保字节确实匹配声称的类型——ossutil 会根据键的扩展名推导对象的Content-Type,因此.png白名单正是让对象以图片类型被服务的关键约束点;
  • 文件名净化tr -cd 'a-zA-Z0-9._-'清洗并截断至 80 字符,拒绝空名、以.开头、非.png结尾及净化后重名的文件(重名会静默覆盖并渲染两次);
  • 托管失败降级:上传失败时/verify报告退化为纯文本评论,并输出本地前置条件诊断(ossutil 二进制是否缺失、配置文件是否存在),以区分"工具/凭据缺口"与"瞬时上传抖动";Web Shell 预览则刷新评论但不带损坏的图片 URL(hostingFailed状态)。

与 untrusted PR 字节的隔离

发布者任务在基础仓库上下文中运行,绝不向执行 PR 代码的作业暴露 OSS 凭据;它们只消费有界、已校验的图片制品。以Web-shell Visuals Publish为例:它通过workflow_run触发,permissions仅声明actions: read,评论用CI_BOT_PAT发布,OSS 用自己的仓库 secrets;checkout 只做 sparse-checkout(package.json、发布脚本与上传器),且把 ref 显式 pin 到github.sha,从步骤层面保证"绝不 checkout/运行 PR 代码"。来自 PR 的pr.txt被视为不可信输入,先净化再与认证的 head SHA、repo、branch 三重绑定校验(validate_pr),并在上传前后与写评论前反复执行gate,压缩 validate→write 的 TOCTOU 窗口。

Bucket 选择与凭据边界

解析顺序

两个发布者都把目标解析为:

ALIYUN_OSS_PR_ASSETS_BUCKET → ALIYUN_OSS_BUCKET → qwen-code-assets

公共基础 URL 由胜出的 bucket 推导,或通过ALIYUN_OSS_PR_ASSETS_PUBLIC_BASE_URL显式设置。工作流中的实际表达式(两个发布者一致,见 .github/workflows/web-shell-visuals-publish.yml):

ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_PR_ASSETS_BUCKET || vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}" ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PR_ASSETS_PUBLIC_BASE_URL || (vars.ALIYUN_OSS_PR_ASSETS_BUCKET == '' && vars.ALIYUN_OSS_PUBLIC_BASE_URL) || format('https://{0}.oss-cn-hangzhou.aliyuncs.com', vars.ALIYUN_OSS_PR_ASSETS_BUCKET || vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets') }}"

两点设计意图值得注意:

  1. 默认行为不变:未设置任何变量时,解析回当前共享 bucketqwen-code-assets
  2. 独立开关的意义:PR 证据是不可信、PR 派生的内容,而共享 bucket 同时服务 release、desktop、live-host 下载;只设置一个变量即可把它们分开,无需改动工作流。默认公共 URL 从最终解析出的 bucket 推导,是为了防止只覆盖两个变量之一时,评论链接指向另一个 bucket 上的 404 或陈旧对象。若通过ALIYUN_OSS_ENDPOINT跨区域覆盖,也必须同步覆盖ALIYUN_OSS_PUBLIC_BASE_URL,因为默认 host 固定为杭州(oss-cn-hangzhou.aliyuncs.com)。

凭据边界:不缩小密钥,只隔离职责

该开关不会收窄凭据:这些任务持有与 release 同步相同的 OSS key。将专用 RAM 用户按pr-assets/前缀做权限收敛属于本 PR 之外的运维工作——这正是目标被设计成变量而非常量的原因:为未来按前缀做最小权限授权留出挂载点。

自动评审的第三条图片路径:qwen review publish-assets

除上述两个 OSS 发布者外,自动化评审工作流还有第三条独立的图片路径:CLI 命令qwen review publish-assets(实现见 packages/cli/src/commands/review/publish-assets.ts)。

命令语义与约束

该命令仍可用于明确指定的资产仓库QWEN_REVIEW_ASSETS_REPO(owner/repo,必须由用户手工设置,绝不由模型自选;未设置则不发布、以退出码 3 结束)。文件通过 GitHub Contents API 落到资产仓库的pr-assets/<pr>-review分支上(无本地 clone、无 SSH),URL 固定到最终提交 SHA,保证已发布评论的证据即使分支后续移动也保持不可变。

自指向目标被拒绝

迁移后的工作流拒绝指向"被评审仓库自身"的目标设定

  • 目标未设置或指向自身时,降级为纯文本评论 + 可下载的 run 制品;
  • 指向外部图片托管仓库仍然受支持。

兼容性与遗留清理

旧评论与历史分支

已有 PR 评论继续保留其 Git 托管的 URL,不强制迁移。PR 关闭清理工作流 .github/workflows/web-shell-visuals-cleanup.yml 继续删除历史pr-assets/*引用,但被迁移的工作流不再产生任何新引用。清理任务在基础上下文(pull_request_target)运行,只按名字删除三类历史分支,绝不 checkout 或运行 PR 代码:

pr-assets/web-shell-visuals-<PR_NUMBER> pr-assets/<PR_NUMBER>-verify pr-assets/<PR_NUMBER>-review

删除逻辑刻意不用set -e:单个分支缺失或删除失败不应中断其余分支,且"大多数 PR 两个分支都不产生"是常态。

OSS 对象保留策略

OSS 对象的保留策略有意放在工作流之外pr-assets/前缀可以配置 bucket 生命周期规则(lifecycle rule)来统一过期清理,不需要改动仓库历史或克隆行为——这正是"对象托管与仓库彻底解耦"的收益。

验证矩阵:如何证明迁移正确

设计文档给出了四项验收手段,均能在仓库中找到对应实现:

  1. 工作流级/verify图片托管 harness:针对一个 fake OSS 上传器执行,覆盖合法图片、被拒图片、重名、精确大小边界、首次发布与上传失败(VERIFY_ASSETS_UPLOADER环境变量即为此测试缝而设,见 .github/workflows/qwen-triage.yml);
  2. 断言两个发布者工作流都调用共享上传器、且不含 Git 图片推送路径:对应 .github/scripts/web-shell-visuals-publish.test.mjs 中workflow hosts visuals on OSS without writing Git refs测试——断言工作流匹配scripts/upload-aliyun-oss-assets.js、匹配pr-assets/web-shell-visuals/${PR}/${RUN_HEAD_SHA}/${RUN_ID}/${RUN_ATTEMPT}键模板、匹配ALIYUN_OSS_PUBLIC_BASE_URLRUN_ATTEMPT注入;
  3. 断言自动化评审工作流不能把本仓库作为图片分支目标:对应qwen review publish-assets的自指向拒绝逻辑;
  4. 共享上传器单元测试与工作流解析器测试:上传器的可测表面(parseUploadArgsresolveAttemptTimeoutMsuploadAssets)被显式导出(见 scripts/upload-aliyun-oss-assets.js),配合web-shell-visuals-publish.test.mjssanitizeNameclassifyMagicselectImagesbuildComment等纯函数的覆盖——这些函数正是消费不可信 PR 输出、此前完全无测试覆盖的部分(一个 shell 净化 bug 曾给每个文件名追加_并静默产生空预览)。

实施要点速览

维度取值 / 行为
共享上传器scripts/upload-aliyun-oss-assets.js,3 次尝试、2s 起指数退避、public-readACL
单次尝试超时环境变量OSS_UPLOAD_ATTEMPT_TIMEOUT_MS,PR 任务设为 120000ms;release 同步默认不设限
Web Shell 预览前缀pr-assets/web-shell-visuals/<pr>/<head-sha>/<run-id>/<run-attempt>/
verify 报告前缀pr-assets/verify/pr<pr>-<run-id>-<run-attempt>/
verify 图片上限至多 8 张 PNG、每张 ≤2 MiB(字节精确)、PNG magic bytes 校验、净化文件名
bucket 解析ALIYUN_OSS_PR_ASSETS_BUCKETALIYUN_OSS_BUCKETqwen-code-assets
公共 URLALIYUN_OSS_PR_ASSETS_PUBLIC_BASE_URL,默认按 bucket 推导(杭州区域)
评审 CLIqwen review publish-assets,仅写入QWEN_REVIEW_ASSETS_REPO,拒绝指向被评审仓库
遗留清理PR 关闭时删除pr-assets/web-shell-visuals-<pr>pr-assets/<pr>-verifypr-assets/<pr>-review三类历史分支
对象保留由 bucket 生命周期规则管理,工作流不再负责

总结

这次迁移的核心价值在于把"PR 证据的存储"从仓库的版本历史中剥离:通过 head SHA、run id、run attempt 三元素构造不可变对象键,在 GitHub camo 代理按 URL 缓存的现实约束下保证了评论截图的字节一致性;通过可信发布者与 untrusted 字节的严格隔离、字节级校验与纯文本降级,维持了与旧 Git 方案同等的安全与可用性;而独立的 bucket 变量则为未来按pr-assets/前缀收敛 RAM 权限留下了明确的运维挂载点。对维护者而言,理解这套前缀与回退规则,是排查"评论图片不更新""图片 404"或"克隆体积"问题的第一把钥匙。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询