last30days-skill:Towncrier 变更日志分片 + 自动化 Lockstep 发布 PR 的工程实践
【免费下载链接】last30days-skillAI agent skill that researches any topic across Reddit, X, YouTube, HN, Polymarket, and the web - then synthesizes a grounded summary项目地址: https://gitcode.com/GitHub_Trending/la/last30days-skill
本文围绕 towncrier-lockstep-release.md 这一解决方案文档展开,讲解 last30days-skill 如何用 towncrier 变更日志分片(changelog fragments)加一套 GitHub Actions 工作流,彻底消除CHANGELOG.md的合并冲突,并把一次版本发布需要同步的十余个"lockstep 版本面"(skill 元数据、pyproject、各插件与市场 manifest)收敛成一条可复现的自动化流水线。读完本文,你能理解多 Agent 协作仓库中"谁来写 changelog、谁不能动版本号、发布 PR 如何自动生成与打标"的完整机制,并掌握可直接迁移到同类项目的配置与脚本设计。
问题背景:两条发布痛点
该文档(frontmatter 中applies_when列出的适用场景)描述了三类典型症状,正是本方案要解决的问题:
- 多 PR 同改
## [Unreleased]造成合并冲突:早期每个功能 PR 都直接编辑CHANGELOG.md的 Unreleased 小节,每次"发布列车"合并时都会冲突; - 手工发布漏改 marketplace JSON 版本号:一次正确发布必须让同一个semver 同时出现在 skill frontmatter + H1、
pyproject.toml、uv.lock、Claude/Codex/Grok/Gemini 插件 manifest、以及两个 marketplace JSON 文件中——这些一致性由 tests/test_plugin_contract.py 的 lockstep 测试强制校验; - Agent 自由发挥发布步骤导致漂移:该仓库的功能 PR 主要由 AI Agent 而非人类撰写,如果发布规则不明确,Agent 会"发明"出与
test_plugin_contractlockstep 约定不一致的发布步骤。
文档给出的根因分类是missing_workflow_step(缺失工作流步骤),解决类型是workflow_change(流程变更)。一个值得注意的权衡记录在文档中:release-please 并非不能做,但它要求维护一大片extra-files配置面,并依赖 conventional-commit 纪律,而 Agent 提交流量并不能可靠地提供这种纪律——因此项目选择了"自写脚本 + 强守卫"的路线。
解决方案总览:五个组件
文档给出的方案由五个组件构成,下面逐一结合仓库中的真实实现展开。
1. towncrier:PR 只加分片,CHANGELOG 只在发布时生成
PR 不再触碰CHANGELOG.md,而是向changelog.d/目录添加<编号>.<类型>.md分片;CHANGELOG.md只在发布时刻由 towncrier 汇总写入。贡献者指南见 changelog.d/README.md,其核心规则:
- 不需要安装 towncrier CLI 即可贡献:分片就是普通 Markdown 文件,towncrier 只在发布准备时运行;
- 命名约定:优先用 PR 或 issue 编号——
changelog.d/<number>.<type>.md;尚无关联 issue/PR 时用孤儿命名——changelog.d/+.<type>.md或changelog.d/+short-slug.<type>.md; - 类型表(Keep a Changelog):
| 后缀 | 章节 |
|---|---|
security | Security |
removed | Removed |
deprecated | Deprecated |
added | Added |
changed | Changed |
fixed | Fixed |
- 内容要求:一到两句"使用者会在 release notes 里关心的话"(行为、文档或安装层面的影响);分片正文可链接 issue,towncrier 也会自动从文件名链接编号。
- 跳过规则:纯杂务(注释错字、无 release notes 价值的 CI 版本 pin 更新)可以不加分片,改为在 PR 模板勾选Skip changelog,或添加
skip-changelog标签。
towncrier 的完整配置在 pyproject.toml 的[tool.towncrier]段:
[tool.towncrier] name = "last30days-skill" directory = "changelog.d" filename = "CHANGELOG.md" start_string = "<!-- towncrier release notes start -->\n" underlines = ["", "", ""] title_format = "## [{version}] - {project_date}" issue_format = "[#{issue}](https://github.com/mvanhorn/last30days-skill/issues/{issue})"其后依次为security、removed、deprecated、added、changed、fixed六个[[tool.towncrier.type]]段,每个都设置showcontent = true。towncrier 本身放在 dev 依赖组中(towncrier>=25.8.0,<26),即日常开发不引入、发布准备时由uv sync --group dev提供。start_string机制意味着CHANGELOG.md只在<!-- towncrier release notes start -->标记之后被 towncrier 管理,历史内容不会被重写。
2..github/scripts/prepare_release.py:一次 towncrier build + 全部版本面 bump
发布准备脚本 .github/scripts/prepare_release.py 的职责是"先运行towncrier build,再把所有 lockstep 路径上的版本号统一抬升"。用法(仓库根目录执行):
python3 .github/scripts/prepare_release.py --bump patch python3 .github/scripts/prepare_release.py --version 3.19.0 python3 .github/scripts/prepare_release.py --bump minor --dry-run从源码可以确认它的行为细节:
- 参数:
--bump major|minor|patch与--version X.Y.Z二选一(互斥组,必选其一);--dry-run只打印目标版本和 towncrier 草稿(加--draft),不写任何文件;--skip-towncrier表示 changelog 已准备好、只 bump 版本面(见 prepare_release.py#L168-L183); - 安全闸:拒绝把版本降级(
Refusing to downgrade),拒绝在非 dry-run 下"同版本重发"(Refusing to re-release)(见 prepare_release.py#L188-L197); - 版本面清单:
JSON_VERSION_FILES覆盖.claude-plugin/plugin.json、.codex-plugin/plugin.json、.grok-plugin/plugin.json、gemini-extension.json;MARKETPLACE_FILES覆盖.claude-plugin/marketplace.json、.grok-plugin/marketplace.json;加上pyproject.toml、skills/last30days/SKILL.md(frontmatterversion:与# last30days vX.Y.Z:H1 两处)、uv.lock中name = "last30days-skill"的 package 段,共9 个文件(见 prepare_release.py#L28-L38 与 bump_all#L151-L165); - 精确替换:SKILL.md 的 frontmatter 版本与 H1 版本各要求恰好一次匹配,
uv.lock要求恰好一个 last30days-skill package stanza,任何"多于一次或零次"匹配都会SystemExit失败——这种 fail-closed 设计避免正则误伤(见 bump_skill_md#L97-L108)。
3. GitHub Actions 三段式:Prepare release → Tag release → Release
发布链路由三个工作流串成,对应文档中"opens the release PR → createsvX.Y.Zon merge → existing Release workflow attaches artifacts":
(a)Prepare release(.github/workflows/prepare-release.yml):由workflow_dispatch手动触发,输入为bump(choice:patch/minor/major,默认 patch)与可选的显式version。流程为:
uv python install 3.12+uv sync --group dev装好 towncrier;- 运行
prepare_release.py(显式 version 优先于 bump),并从pyproject.toml读回新版本号; - 创建
release/vX.Y.Z分支(若远端已存在同名分支则中止,防止覆盖),git add白名单内的 12 个发布相关文件(CHANGELOG.md、changelog.d、pyproject.toml、uv.lock、SKILL.md、各 plugin/marketplace JSON、gemini-extension.json);暂存区为空则报错退出(提示"changelog.d 可能是空的"); - 以
chore(release): bump version to X.Y.Z提交并推送,用gh pr create建 PR,打release标签,PR 描述中自带测试计划(含uv run pytest与tests/test_plugin_contract.py::test_versions_match_across_manifests检查项)。
(b)Tag release(.github/workflows/tag-release.yml):监听 main 分支 push,但只处理提交信息含chore(release): bump version to的 commit(注意if:表达式必须整体加引号并用contains而非startsWith——裸冒号会让 YAML 解析失败,且 merge commit 把 PR 标题放在 body 里,需要逐行扫描)。其内部校验链值得注意:
- 从提交信息中提取 VERSION 后,再与
pyproject.toml实际版本比对,不一致则拒打 tag; - 反查该 commit 对应的 PR,要求 PR 携带仓库管控的
release标签——"仅有匹配的标题不能铸出 tag"; - 打 annotated tag
vX.Y.Z并推送,随后显式gh workflow run release.yml -f tag=vX.Y.Z派发 Release 工作流(因为GITHUB_TOKEN触发的 tag push 不会自动启动其他 workflow)。
(c)Release(.github/workflows/release.yml):现有工作流,在 tag 就位后附加.skill/.mcpb等发布产物。
4. changelog-guard:CI 层面的双向守卫
.github/workflows/changelog-guard.yml 在每个 PR 的 opened/synchronize/reopened/labeled/unlabeled 事件上执行,落实"非发布 PR 不许动 CHANGELOG 和版本串"这条规则。从 workflow 脚本可确认四条逻辑:
- 带
release标签的 PR 直接放行(版本与 CHANGELOG 编辑均允许); - 非 release PR 修改
CHANGELOG.md→ 失败,报错提示"Add changelog.d/<n>.<type>.md instead"。有一个历史豁免:一次性 towncrier 迁移(用 start marker 替换旧的## [Unreleased]且未新增+###小节)被允许; - 版本串比对:对 9 个版本面文件(pyproject.toml、uv.lock、SKILL.md、4 个 plugin JSON、2 个 marketplace JSON、gemini-extension.json),用辅助脚本 .github/scripts/read_manifest_version.py 分别解析 base 与 head 两侧的版本,任何差异即报
Non-release PRs must not bump lockstep version strings。脚本注释里记录了一个真实教训:此前内联的python3 -c版本解析块因缩进到 0 列,导致 Actions 拒绝解析整个 workflow(每次运行都是空 jobs 失败),解析逻辑因此被抽到独立脚本; - 引擎改动必须有分片:当改动触及
skills/last30days/scripts/*、skills/last30days/SKILL.md或mcp/*(引擎/技能代码),而 PR 既没有changelog.d/*.md分片(README.md 除外)也没有skip-changelog标签时,守卫失败。
5. PR 模板:changelog 检查单 + Agent 披露
.github/PULL_REQUEST_TEMPLATE.md 把上述规则固化进每个 PR 的表单:
- Changelog 小节:明确"要出现在下次 release notes 就在
changelog.d/加分片,不要编辑CHANGELOG.md或在功能 PR 中 bump 版本/manifest",并提供两个勾选项——加changelog.d/<pr-or-issue>.<type>.md(列出全部 6 种类型),或勾选 Skip changelog(纯杂务,同时加skip-changelog标签); - Agent disclosure 小节:要求总结编码 Agent 做了哪次 review(查了哪些风险、标记了什么、据此改了什么),以及安全审查项(输入处理、命令执行、路径处理、认证、密钥、依赖风险,无则写
N/A); - Relationship to this change:要求披露雇佣/合同/股权等与被集成厂商或产品的付费关联(例如"你在被集成的 API 厂商任职")。
Agent 规则(文档原文核心,逐条继承)
文档给出的"Agent rules (short)"是整套方案对 Agent 的契约,原文三条必须原样执行:
- 写分片,不写
CHANGELOG.md(Write fragments, notCHANGELOG.md); - 功能 PR 中不许 bump 版本(Do not bump versions in feature PRs);
- 通过 Prepare release 发布,而不是手工编辑十个文件(Cut releases via Prepare release, not by editing ten files)。
AGENTS.md 的 "Changelog and releases (agents)" 一节把这三条扩展为可操作的五步规范:功能/修复 PR 在变更属于下次 release notes 时加changelog.d/<pr-or-issue>.<type>.md;永不在功能 PR 中编辑CHANGELOG.md或在pyproject.toml、SKILL.md、plugin/marketplace JSON、uv.lock中 bump 版本(CI 的 changelog-guard 会拦截);无 release notes 内容时加分片豁免 +skip-changelog标签;发布走 Actions →Prepare release(patch/minor/major),合并后 Tag release 推送vX.Y.Z、既有 Release 工作流发布产物,"不要手编十个版本文件";lockstep 闸门是tests/test_plugin_contract.py::test_versions_match_across_manifests,工作流契约由tests/test_changelog_workflow.py锁定。本地等价命令为uv run python .github/scripts/prepare_release.py --bump patch(需 Python 3.12+,环境用uv管理,venv 在.venv/)。
契约测试:把发布流程本身当成被测对象
tests/test_changelog_workflow.py 是这套工作流的"契约测试",它证明了上述组件不是文档声明而是被持续验证的事实:
test_towncrier_config_present校验[tool.towncrier]段、分片目录、输出文件与 6 种分片类型齐备;test_changelog_has_towncrier_start_marker要求CHANGELOG.md含 start marker 且不再包含## [Unreleased](旧冲突源被彻底移除);test_release_workflows_exist断言三个 workflow 文件存在;test_tag_release_workflow_yaml_parses/test_tag_release_workflow_version_extraction直接回放 tag-release.yml 里的 sed 表达式,验证它能同时解析"直接 push"与"merge commit(标题在 body)"两种提交信息形态;test_changelog_guard_run_blocks_stay_indented用_assert_run_blocks_indented断言所有run: |块内不存在 0 列(或欠缩进的)行——这正是 workflow 脚本注释中提到的那次"空 jobs 失败"事故的回归防护;test_next_version_bumps(3.18.1 + patch/minor/major → 3.18.2/3.19.0/4.0.0)、test_main_refuses_equal_version_outside_dry_run、test_bump_all_updates_lockstep_surfaces(在临时目录里搭出完整的 lockstep 布局,bump 到 9.9.9 后逐一断言 9 个文件全部到位)。
小结与参考
这套方案的设计要点可以概括为:把 changelog 写入权收敛到单一发布时刻(towncrier),把版本号写入权收敛到单一自动化 PR(prepare_release.py + release 标签),再用 CI 守卫(changelog-guard)与契约测试(test_changelog_workflow.py)把两条收敛线变成不可绕过的规则——在多 Agent 贡献的仓库里,这比依赖 Agent 自觉遵守纪律可靠得多。
延伸阅读(文档 "See also" 一节指向的仓库内资源,均为仓库根目录相对路径):
- AGENTS.md § "Changelog and releases (agents)" —— Agent 视角的五步发布规范;
- changelog.d/README.md —— 分片命名、类型表与 skip 规则;
- tests/test_changelog_workflow.py —— 工作流契约测试;
- 相关配套:.github/scripts/prepare_release.py、.github/scripts/read_manifest_version.py、.github/workflows/prepare-release.yml、.github/workflows/tag-release.yml、.github/workflows/changelog-guard.yml、.github/PULL_REQUEST_TEMPLATE.md、pyproject.toml(
[tool.towncrier]配置段)、tests/test_plugin_contract.py(版本 lockstep 测试)。
【免费下载链接】last30days-skillAI agent skill that researches any topic across Reddit, X, YouTube, HN, Polymarket, and the web - then synthesizes a grounded summary项目地址: https://gitcode.com/GitHub_Trending/la/last30days-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考