news 2026/9/23 14:36:55

Easydict 发布流水线改造:以 changelog 版本文件统一 GitHub Release 与 Sparkle 应用内更新日志

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Easydict 发布流水线改造:以 changelog 版本文件统一 GitHub Release 与 Sparkle 应用内更新日志

Easydict 发布流水线改造:以 changelog 版本文件统一 GitHub Release 与 Sparkle 应用内更新日志

【免费下载链接】Easydict一个简洁优雅的词典翻译 macOS App。开箱即用,支持离线 OCR 识别,支持有道词典,🍎 苹果系统词典,🍎 苹果系统翻译,OpenAI,Gemini,DeepL,Google,Bing,腾讯,百度,阿里,小牛,彩云和火山翻译。A concise and elegant Dictionary and Translator macOS App for looking up words and translating text.项目地址: https://gitcode.com/gh_mirrors/ea/Easydict

Easydict 的发布团队在 2026-09-07 完成了一次发布内容体系重构:让仓库内的changelog/<version>.md成为 Release Notes 的唯一正文源,GitHub Release 保存其原始 Markdown,Sparkle 应用内更新日志(appcast)保存其确定性渲染的 HTML,并配套了校验、快照、漂移检测与回归测试。阅读本文,你将掌握这套「单源多表面」发布方案的完整设计:changelog 文件规范、Python-Markdown 固定渲染器与 SHA-256 快照机制、GitHub Draft 冻结门禁、Sparkle appcast 一致性校验,以及已发布版本修订的安全流程。本次重构只迁移验证了 2.22.0,未发布任何新版本。

背景:多正文源导致的发布不一致

在本次重构之前,Easydict 的发布流程存在多个「正文来源」:临时 notes 文件、GitHub 自动生成正文(--generate-notes)、Sparkle appcast 从文件或线上 Release 抓取正文。多个来源叠加不同的生成时序,会导致三个发布表面——GitHub Release 正文、Sparkle 应用内更新日志、人工编辑内容——彼此漂移且难以察觉。

为此,发布团队在 2026-09-07-changelog-release-notes.md 中明确了目标:

changelog/<version>.md成为 Release Notes 唯一正文来源,GitHub Release 与 Sparkle appcast 均验证其派生结果,并用 2.22.0 完成迁移验证。

整个改动限定在changelog/.agents/skills/release-easydict/(发布 Skill 与脚本)、scripts/docs/范围内,明确禁止发布新版本、编辑线上 2.22.0 Release、push 或移动 Tag。

changelog 目录:按版本保存的唯一可编辑正文

新增的 changelog/README.md 定义了文件约定:

  • 文件名必须为<version>.md,例如2.22.0.md,版本使用x.y.z格式;
  • 正文使用 UTF-8 编码和 LF 换行,不允许YAML front matter,也不需要Release 标题(GitHub Release 标题独立维护);
  • 正文内 PR 与比较范围链接使用简短 Markdown 标签(如[#1285](https://github.com/tisfeng/Easydict/pull/1285)),不直接展示完整裸 URL;
  • 发布开始前可以直接编辑 Markdown,编辑后必须重新运行验证;发布状态冻结后发生的改动会触发哈希不一致,必须重新开始或明确重建 Draft,不能静默沿用旧 appcast。

迁移后的 changelog/2.22.0.md 与线上 2.22.0 Release 正文逐字一致,包含## What's Changed(28 条 PR 条目)、## New Contributors(10 位新贡献者)与**Full Changelog**: 2.21.0...2.22.0三个区块,是后续版本正文的格式范本。

单一正文源的数据流架构

changelog/README.md 中用一张关系图明确了发布表面的派生关系:

changelog/<version>.md ├── GitHub Release body(原始 Markdown) └── Sparkle appcast description(确定性渲染后的 HTML)

设计意图(来自 history 文档)可以概括为三点:

  • GitHub 保存原始 Markdown,与 changelog 文件逐字一致;
  • Sparkle 保存固定渲染器产生的 HTML,保证应用内展示稳定;
  • 发布状态只保存哈希与渲染器身份,不再保存另一份可编辑 notes,从结构上消灭第二正文源。

版本 Tag 仍指向版本构建快照,不因正文展示格式产生额外移动;正文必须在启动发布前提交到dev分支。

确定性渲染与哈希快照:release_notes.py 核心实现

统一的正文工具是 release_notes.py,它同时承担校验、渲染、快照和远端比对四类职责。

严格的文件校验

read_notes()对每个 changelog 文件执行严格门禁:版本号必须匹配^[0-9]+\.[0-9]+\.[0-9]+$;文件名必须等于<version>.md;正文必须是合法 UTF-8、不含 BOM、不含 CR/LF 混合换行、不含 NUL 字节、非空、不以---\n开头。任何一条不满足都会抛出ReleaseNotesError并中断发布。

固定渲染器身份

require_renderer()强制要求 Python-Markdown 版本等于EXPECTED_MARKDOWN_VERSION = "3.8.1",缺失或版本不匹配立即失败,避免不同发布机器渲染出不同 HTML。依赖清单见 requirements.txt(Markdown==3.8.1)。渲染器身份字符串为:

Python-Markdown/3.8.1:extra,sane_lists,easydict_bare_url

render_markdown()使用extrasane_lists扩展,并注册了自定义的BareUrlExtension。该扩展专门处理裸 URL:BareUrlInlineProcessor只作用于 Markdown 文本节点,跳过位于<a><code><pre>原始 HTML 内部的链接,并会修剪 URL 尾部标点(.,;:!?]})以及多余的右括号,随后生成带href<a>元素。

SHA-256 快照

notes_metadata()输出包含schema_versionversionpathmarkdown_sha256rendererhtml_sha256六个字段。正文哈希只做「传输级」归一化——normalize_body()把 CRLF/CR 统一为 LF 并去掉末尾单个换行——其余空白与内容必须逐字节一致。快照通过snapshot子命令写入发布状态 JSON,verify-state子命令在 resume、Draft、publish 和验证阶段重新比对,防止人工编辑后继续使用旧 appcast。

release_notes.py暴露五个 CLI 子命令,全部通过--file--version定位正文:

子命令作用
validate严格校验文件并输出完整元数据 JSON
render渲染 HTML,可指定--output落盘
snapshot首次写入或校验发布状态快照
verify-state重算元数据并与状态文件逐字段比对
verify-release通过gh release view读取线上 Release,比对正文并输出markdown_sha256

其中verify-release会同时检查tagName与版本一致、Release body 与 changelog 归一化后完全相等,并支持--input-json离线注入 Release JSON 便于测试。

GitHub Release 接入:Draft 强制冻结 Markdown

release_content.py 负责把 changelog 固化到 GitHub Release 表面,并提供captureapplyvalidate-pr-policy三个子命令。

Release 标题规范

validate_release_title()强制标题格式:

<version> <emoji> <type>: <summary>

其中type与 emoji 一一对应:feat→✨、fix→🐞、security→🔒、perf→🚀、chore→🔧;标题不得超过 120 字符且只能使用英文(含非拉丁字母即报错)。

PR 条目解析与去重

parse_change_entries()从正文解析形如- title by @author in [#123](https://.../pull/123)的条目,同时兼容裸 PR URL 引用,并做三重校验:Markdown 链接的 label 数字与 URL 中的 PR 号必须一致;同一 PR 号不得重复出现;正文中至少存在一条 PR 条目。validate-pr-policy子命令还会逐个检查 PR 是否属于应忽略的 bot PR,若正文包含被忽略的 bot 条目则直接阻断(对应测试中的「缺失/格式」门禁)。

Draft 冻结与远程验证

apply子命令先校验标题与正文,再通过gh release view读取 Draft 并调用validate_draft()比对 body 与 changelog;--execute才会真正调用gh release edit更新标题,随后重新抓取Draft 做二次远程验证,确保标题写入成功。发布脚本从此不再回退到--generate-notes

Sparkle appcast:从 changelog 渲染 description

release-appcast.py 把 Sparkle 应用内更新日志完全收敛到 changelog:

  • set-link:将目标版本 item 的<description>替换为render_markdown(read_notes(...))渲染结果,移除旧的releaseNotesLink,并写入fullReleaseNotesLink
  • set-description:仅替换目标 item 的 description;
  • find-previous-beta/promote-previous-beta:处理 beta 版本升级为稳定版时对前序 beta item 的 channel 清理;
  • validate:对生成的 appcast 做全字段严格校验。

validate子命令的校验面相当完整,它要求:

  • appcast 版本集合与顺序不变(新 item 置于首位,旧 item 除 beta 转正外字节级不变,通过canonical_item()归一化空白后比对);
  • itemtitle等于版本号、存在pubDate
  • fullReleaseNotesLinkreleaseNotesLink等于预期的 Release URL;
  • <description>render_markdown(read_notes(...))完全相等
  • minimumSystemVersion13.0;channel 与目标(beta或稳定)一致;
  • enclosure的 URL 等于下载地址、length等于归档文件真实字节数、typeapplication/octet-stream,且必须存在 Sparkle EdDSA 签名(sparkle:edSignature)。

这些约束可以在仓库 appcast.xml 中直接对照验证——例如 2.22.0 item 的<description>就是 2.22.0 changelog 渲染后的 HTML,fullReleaseNotesLink指向对应 Release Tag,enclosure携带签名与长度。

已发布版本的修订与同步:release-notes-sync.py

针对「已发布版本如何修订正文」的问题,release-notes-sync.py 提供了端到端的同步工具,核心约束是不允许单边修改

sync-notes <version>命令默认读取changelog/<version>.md,流程为:

  1. 读取并渲染 changelog,校验 UTF-8/LF/文件名;
  2. 通过gh api --include抓取线上 Release(拒绝仍为 Draft 的版本,并要求 ETag 用于条件更新);
  3. 抓取maindev两个分支上的远端appcast.xml(读取前后各取一次分支 head,防止读取期间远端变更);
  4. 生成预览 JSON:包含 Release body 的 unified diff、release_update_required、两个分支 appcast 是否需更新、本地分支是否落后远端;
  5. --execute阶段:校验origin确为 GitHub 仓库、在临时 worktree 中生成 main 分支的 appcast 提交、合并进 dev、--force-with-lease原子推送两个分支、按 ETag 条件更新 Release body,最后重新拉取远端验证三个表面全部一致。

同步成功后,状态 JSON(默认写入.tmp/release/<version>/state/notes-sync.json)记录notes_sha256html_sha256status与各阶段结果;任一步骤失败都会把failed_stage与错误写入状态文件后中止。因此文档给出的修订路径是:先修改并提交对应 changelog,再在一次明确授权的维护任务中同步 GitHub Release 与 appcast——单边手动修改 GitHub 或 appcast 会被校验识别为「内容漂移」并停止发布。

测试与验证结果

本次重构的验证横跨发布脚本测试、Skill 测试与线上只读比对(数据来自 history 文档 与 exec-plan 文档):

  • 发布脚本测试 26 个通过(python3 -m unittest discover -s scripts/release/tests -p 'test_*.py');
  • Release Skill 测试 23 个通过(python3 -m unittest discover -s .agents/skills/release-easydict/tests -p 'test_*.py'),覆盖缺失/格式、快照漂移、远端正文漂移、复杂 Markdown、appcast 漂移和 Draft--notes-file行为;
  • 2.22.0 线上 Release 只读比对通过,正文哈希为0f5dcd6d3cd492a2a484aec80687176638dc9a6d65bb636032d913182f958799
  • 仓库appcast.xml的 2.22.0 description 与 changelog 渲染结果完全一致,并使用公开条目的 build 65、ZIP 长度、URL、channel 与签名完成严格校验;
  • bash -njq -e . scripts/release/asc-workflow.jsonasc workflow validatepython3 -m py_compileskill-creatorquick validation、git diff --check全部通过;
  • 独立 reviewer 三轮复核确认冻结门禁、Git index/工作树漂移和 Markdown/raw HTML 链接边界均已闭环;独立 tester 在最终快照上重复执行全部测试通过;
  • xcodebuild未运行——本次只修改发布脚本、测试与文档,不涉及 Xcode 编译源码。

变更影响与边界

本次改动影响的仓库范围(来自 history 文档):

  • changelog/:新增 README 与 2.22.0 迁移文件;
  • .agents/skills/release-easydict/:发布 Skill 脚本、测试与文档;
  • docs/exec-plans/docs/histories/2026-09/:计划与执行记录;
  • scripts/(依 exec-plan 允许修改路径,同时涉及相关脚本目录)。

远程状态保持零写入:未创建或发布新版本、未编辑 2.22.0 GitHub Release、未上传资产、未创建或移动 Tag、未 push。需要留意的是,本次迁移仅覆盖 2.22.0 及以后版本,2.22.0 之前的旧版本不在 changelog 迁移范围内。

如果你需要在本地复现这套机制的校验效果,可以基于 release_notes.py 对 changelog/2.22.0.md 运行validaterender子命令,再对照 appcast.xml 中 2.22.0 item 的<description>,即可直观理解「原始 Markdown 进、确定性 HTML 出、哈希锁状态」的完整链路。

【免费下载链接】Easydict一个简洁优雅的词典翻译 macOS App。开箱即用,支持离线 OCR 识别,支持有道词典,🍎 苹果系统词典,🍎 苹果系统翻译,OpenAI,Gemini,DeepL,Google,Bing,腾讯,百度,阿里,小牛,彩云和火山翻译。A concise and elegant Dictionary and Translator macOS App for looking up words and translating text.项目地址: https://gitcode.com/gh_mirrors/ea/Easydict

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 14:33:50

免费小游戏平台怎么选?CrazyGames、itch.io、Poki深度推荐

1. 为什么我推荐这三款免费小游戏平台1.1 免费游戏平台的生态现状这些年周围朋友经常问我一个问题&#xff1a;想找个地方玩游戏&#xff0c;不想下载几十个G的大型客户端&#xff0c;也不想一打开就跳充值弹窗&#xff0c;到底有什么靠谱的选择&#xff1f;说实话&#xff0c;…

作者头像 李华
网站建设 2026/9/23 14:33:10

Spring Boot + MySQL + ECharts 实现路口流量调查统计分析系统

1. 这套系统到底在干什么1.1 交叉路口流量调查的真实痛点先别急着打开源码&#xff0c;把需求吃透比什么都重要。交叉路口行人、非机动车流量调查&#xff0c;说白了就是交通管理部门、城市规划部门要搞清楚一个路口到底有多少人走路、多少辆电动车和自行车经过、集中在什么时间…

作者头像 李华
网站建设 2026/9/23 14:32:55

国台酒经销商代理合作模式全解析

2026年&#xff0c;对于正在寻找酱酒代理机会的酒类经销商而言&#xff0c;贵州国台数智酒业集团股份有限公司&#xff08;品牌简称&#xff1a;国台酒&#xff09;是一个值得认真评估的合作对象。作为政府授牌的茅台镇第二大酿酒企业&#xff0c;国台酒历经二十余年发展&#…

作者头像 李华
网站建设 2026/9/23 14:32:39

ZCode静默上传Git历史的技术真相与风险解析

1. 项目概述&#xff1a;一场被代码提交记录戳穿的信任裂痕“智谱 ZCode 静默上传 Git 历史”——这十个字不是技术文档的标题&#xff0c;而是一份事故通报的导语。它背后没有炫酷的AI模型演示&#xff0c;没有流畅的IDE插件动画&#xff0c;只有一行被开发者反复翻查、最终在…

作者头像 李华
网站建设 2026/9/23 14:31:41

Vim 从入门到实践:一篇文章理清模式、命令与配置

我得先讲个真实观察&#xff1a;如果你去翻各搜索引擎里 vim 相关的高频问题&#xff0c;常年霸榜的一定是"vim 如何保存退出""vim 怎么到底端""linux vim 保存和退出"这一类最基础的操作。一个编辑器的基础操作成了大家最常搜索的内容&#xff…

作者头像 李华