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 Publish与qwen-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 8与89504e470d0a1a0a比对,确保字节确实匹配声称的类型——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') }}"两点设计意图值得注意:
- 默认行为不变:未设置任何变量时,解析回当前共享 bucket
qwen-code-assets; - 独立开关的意义: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)来统一过期清理,不需要改动仓库历史或克隆行为——这正是"对象托管与仓库彻底解耦"的收益。
验证矩阵:如何证明迁移正确
设计文档给出了四项验收手段,均能在仓库中找到对应实现:
- 工作流级
/verify图片托管 harness:针对一个 fake OSS 上传器执行,覆盖合法图片、被拒图片、重名、精确大小边界、首次发布与上传失败(VERIFY_ASSETS_UPLOADER环境变量即为此测试缝而设,见 .github/workflows/qwen-triage.yml); - 断言两个发布者工作流都调用共享上传器、且不含 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_URL与RUN_ATTEMPT注入; - 断言自动化评审工作流不能把本仓库作为图片分支目标:对应
qwen review publish-assets的自指向拒绝逻辑; - 共享上传器单元测试与工作流解析器测试:上传器的可测表面(
parseUploadArgs、resolveAttemptTimeoutMs、uploadAssets)被显式导出(见 scripts/upload-aliyun-oss-assets.js),配合web-shell-visuals-publish.test.mjs对sanitizeName、classifyMagic、selectImages、buildComment等纯函数的覆盖——这些函数正是消费不可信 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_BUCKET→ALIYUN_OSS_BUCKET→qwen-code-assets |
| 公共 URL | ALIYUN_OSS_PR_ASSETS_PUBLIC_BASE_URL,默认按 bucket 推导(杭州区域) |
| 评审 CLI | qwen review publish-assets,仅写入QWEN_REVIEW_ASSETS_REPO,拒绝指向被评审仓库 |
| 遗留清理 | PR 关闭时删除pr-assets/web-shell-visuals-<pr>、pr-assets/<pr>-verify、pr-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),仅供参考