HF Hub分享如何用AI工具和开源软件优化发布流水线,代表开源项目现代化DevOps的实践范例。
长期以来,我们每隔 4 到 6 周发布一个版本。现在,我们通过一个 GitHub Actions workflow,每周发布一次。整套流程使用开源工具和开放权重模型构建,并且在真正需要判断的环节保留了人工介入。本文介绍的所有内容都不依赖厂商合同、闭源模型,也不需要你无法自行运行的基础设施。这是我们从一开始就确立的设计目标,因为我们希望其他维护者也能直接采用并按需调整这套 workflow。
读完本文,你将掌握构建自己发布流程所需的一切。
过去的流程只有一部分实现了自动化,大部分仍依赖手动操作。
已经自动化的部分包括:
但以下工作每次仍然需要手动完成:
__init__.py 中的版本号、提交、打 tag、推送。dev0。于是,我们决定精简整个流程。回头看上面的工作清单,可以将其分成两类。
有些步骤纯粹是机械操作,可以实现自动化:更新版本号、提交、打 tag、推送、创建下游测试分支、创建发布后的 PR。这些事情不需要任何人思考,只需保证每次都按照正确顺序执行,而这正是 CI workflow 擅长的事情。
其余工作则不同。编写 release notes、决定重点突出哪些内容、为真实读者组织公告措辞,这些都需要动脑和判断。正是这类判断,让发布流程多年来一直无法完全自动化。AI 可以在这里发挥作用:只需几秒,就能把一张白纸变成一份扎实的初稿。但这里同样需要谨慎,因为一份看起来信心十足、实际却暗藏错误的草稿,比完全没有草稿更加糟糕。
决定改造这套流程时,我们预先设定了一项约束:其中的每一个活动组件,都必须是任何维护者都能自行运行的东西。不能依赖无法替换的闭源 API 模型,不能使用专有的发布平台,也不能藏有任何秘方。
以下就是完整的技术栈:
第二项原则是:由模型起草,由人类决策。Language Model 很擅长把 30 个简短的 PR 标题整理成可读的 release notes,但它们并不适合被盲目信任。因此,这套 workflow 由人类监督:模型完成第一稿,确定性脚本检查它的工作,然后由人类审阅和修改,之后才会发布任何内容。下文还会详细介绍这一点。
完整的 workflow 只有一个文件:.github/workflows/release.yml,通过 Actions UI 手动触发。它只接受一个输入:
on:
workflow_dispatch:
inputs:
release_type:
type: choice
options:
- minor-prerelease # cut an RC from main
- minor-release # promote the RC to final
- patch-release # bugfix on an existing release branch
从这里开始,各个 job 大致按照以下顺序运行:
__version__,然后提交、打 tag 并推送。huggingface_hub。与此同时,将 hf CLI 作为独立的 PyPI package 进行构建和上传。transformers、datasets、diffusers 和 sentence-transformers 中创建分支,将依赖固定到该 RC。这样,如果我们破坏了什么,它们的 CI 就能快速反馈。dev0。剩下的手动步骤只有两个:审阅并发布 draft release notes,以及审阅并发布内部 Slack 消息。我们希望人类介入的正是这两个环节。
说到由 AI 生成 release notes,所有人都会担心一种失败模式:模型悄悄漏掉了某个 PR,或者凭空编造了一个不属于本次 release 的 PR。一份“几乎正确”的 changelog 比没有 changelog 更糟,因为人们往往不会再重新核对它。
我们不会相信生成的 release notes 第一次就能做到完整无误,而是通过确定性方式进行验证。在模型运行之前,一个 Python 脚本会获取属于本次 release 的所有 PR,并将其保存为 ground truth。
# Deterministic: extract PR numbers from squash-merge commits in the range.
PR_NUMBER_PATTERN = re.compile(r"\(#(\d+)\)$")
pr_numbers = [
int(m.group(1))
for commit in commits_since_last_tag
if (m := PR_NUMBER_PATTERN.search(commit.title))
]
save_manifest(pr_numbers) # the source of truth
接下来,模型根据这些 PR 起草 release notes。完成后,我们会将模型的输出与最初的 PR 列表进行核对:
expected = set(load_manifest()) # what should be there
found = extract_pr_refs(notes_md) # what the model wrote (#1234 -> 1234)
missing = expected - found # silently dropped
extra = found - expected # belongs to a different release
如果发现缺失或多余的内容,我们既不会直接判定失败,也不会发布错误的文件。我们会把差异交还给 Agent,要求它精确修正这些 PR:
for _ in range(MAX_ITERATIONS):
missing, extra = validate(notes)
if not missing and not extra:
break # matches the manifest exactly
run_agent_fix(missing_prs=missing, extra_prs=extra)
正是这种模式让整套流程变得可信:在非确定性模型之外包裹一层确定性的 guardrail。模型很擅长撰写文字,却无法可靠地做到面面俱到。因此,我们让模型负责写作,再让代码负责确保一致性。
完整性只是一方面,准确性则是另一方面。如果只根据 PR 标题进行总结,模型会兴高采烈地编造一段与真实 API 完全不符的代码示例。
为了避免这种情况,我们在获取 PR metadata 时,还会拉取每个 PR 中真实的文档 diff,也就是该 PR 修改过的 docs/ 目录下所有 .md 文件的 unified diff。
def fetch_doc_diffs(pr):
return [
{"filename": f.filename, "status": f.status, "patch": f.patch}
for f in pr.get_files()
if f.filename.startswith("docs/") and f.filename.endswith(".md") and f.patch
]
这些 diff 会被放入模型的上下文。这样,当模型写出“这是新的 CLI 命令”时,它引用的是 PR 作者实际写进文档的示例。背后的逻辑与之前相同:向模型提供真实的源材料,并交给它一项范围明确的任务。
prompt 本身以 Skills 的形式存在:它们是提交到 repo 中的小型 Markdown 文件,由 SKILL.md 和参考模板组成。release-notes Skill 明确规定了如何选择重点、如何组织章节、何时添加文档链接等。它读起来就像一份入职指南,而这正是理解它的正确方式。
RC 发布后,draft GitHub release 会保留在那里,其中已经写入 AI 的第一版草稿。接下来轮到人类介入:
minor-release,将 RC 提升为最终版本。审阅者的时间不再花在从头写作上,而是用于打磨内容,将原本需要半天的写作任务缩短为 15 分钟的编辑工作。
我们还会保留完整记录,以便不断改进。两个文件会并排归档到 Hugging Face Bucket:一份是原始 AI 草稿,在 RC 阶段、任何人修改之前上传;另一份是经过人工编辑的版本,在最终 release 发布时上传。
# at RC time: straight from the model, untouched
hf cp release_notes_raw.txt "hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_raw.txt"
# at release time: after the human review
hf cp release_notes_edited.txt "hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_edited.txt"
每周收集这两个版本,让我们拥有了一份不断增长的 dataset:一边是“模型写了什么”,另一边是“我们希望它写成什么样”。随后,我们可以复用这份 dataset,持续更新 Agent 的 Skill。
重新设计发布流程,也是强化安全性的好机会,尤其是在防范供应链攻击方面。
不使用 PyPI token。发布过程采用 Trusted Publishing:PyPI 会验证由 GitHub 专门为这次 workflow 签发的短期 OIDC token,并为每个 artifact 签发 PEP 740 attestations / Sigstore provenance。这里不存在可能泄漏或需要轮换的长期 secret。
permissions:
id-token: write # mint the OIDC token for PyPI
attestations: write # generate Sigstore provenance
# ...
- uses: pypa/gh-action-pypi-publish@v1.14.0
with:
attestations: true # no password, no API token, just OIDC
Agent runtime 也会固定版本并进行验证。我们不会直接对最新版 OpenCode 执行 curl | bash,然后祈祷一切顺利。我们会固定具体版本,并在运行前检查其 SHA256:
curl -fsSL https://opencode.ai/install | bash -s -- --version "${OPENCODE_VERSION}"
echo "${OPENCODE_SHA256} $(which opencode)" | sha256sum -c -
采用开放工具,并不意味着可以草率地使用工具。
几乎为零。一次完整的 release,包括 release notes 和 Slack 公告,涉及 20~40 个 PR 以及几轮 prompt,在 Inference Providers 上的成本大约为 0.25 美元。开放权重模型按使用量计费,因此每周唯一真正需要考虑的问题就是:“有没有值得发布的内容?”而答案总是肯定的。
发布节奏从每 4~6 周一次,变成了每周一次。更有意思的是它带来的连锁效应:
这是我们最在意的部分。这套 workflow 围绕 huggingface_hub 设计,但整体结构是通用的。
几乎可以原样复用的部分包括:
minor-prerelease、minor-release 和 patch-release。要适配这套流程:fork workflow 文件和脚本,将其指向你的 package;按照项目自身的口吻重写 Skill Markdown;设置两个 repo variable,也就是 model ID 和 OpenCode 版本;在 PyPI 上配置 Trusted Publishing;如果没有下游项目,就删除下游测试 job。“信任但验证”的循环值得原样复用。正是这一部分,让生成的 artifact 可以被安全发布。
自动分流下游测试失败。目前,workflow 会创建测试分支,再由人类阅读 CI。一个显而易见的下一步,是检查失败日志,并将结果写入内部 Slack 消息。
扩展这种模式。这里的大部分内容都具有通用性。我们预计会在生态系统中的其他 Python library 里复用其中的大部分组件。
过去,一次 release 中需要人类集中工作半天的部分,包括编写 release notes、起草公告、协调下游检查,恰好都是模型擅长起草的内容。其他一切都是机械操作,都可以放进一个 YAML 文件。真正的诀窍从来不只是“让 AI 来做”,而是让模型起草、让确定性代码验证,再让人类作出决定。整套流程完全由开放工具和开放权重模型构建,因此成本约等于零,而且任何人都能运行。
完整的 workflow 文件已经公开。如果你正在维护一个 Python library,不妨 fork 它、按需调整,然后告诉我们使用效果如何!
感谢分享!这会帮助到很多人 🤗
· 注册或登录后发表评论