Easydict 发布流水线改造:以 changelog 版本文件统一 GitHub Release 与 Sparkle 应用内更新日志
Easydict 发布流水线改造以 changelog 版本文件统一 GitHub Release 与 Sparkle 应用内更新日志【免费下载链接】Easydict一个简洁优雅的词典翻译 macOS App。开箱即用支持离线 OCR 识别支持有道词典 苹果系统词典 苹果系统翻译OpenAIGeminiDeepLGoogleBing腾讯百度阿里小牛彩云和火山翻译。A concise and elegant Dictionary and Translator macOS App for looking up words and translating text.项目地址: https://gitcode.com/gh_mirrors/ea/EasydictEasydict 的发布团队在 2026-09-07 完成了一次发布内容体系重构让仓库内的changelog/version.md成为 Release Notes 的唯一正文源GitHub Release 保存其原始 MarkdownSparkle 应用内更新日志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 正文逐字一致包含## Whats Changed28 条 PR 条目、## New Contributors10 位新贡献者与**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.txtMarkdown3.8.1。渲染器身份字符串为Python-Markdown/3.8.1:extra,sane_lists,easydict_bare_urlrender_markdown()使用extra、sane_lists扩展并注册了自定义的BareUrlExtension。该扩展专门处理裸 URLBareUrlInlineProcessor只作用于 Markdown 文本节点跳过位于a、code、pre原始 HTML 内部的链接并会修剪 URL 尾部标点.,;:!?]}以及多余的右括号随后生成带href的a元素。SHA-256 快照notes_metadata()输出包含schema_version、version、path、markdown_sha256、renderer、html_sha256六个字段。正文哈希只做「传输级」归一化——normalize_body()把 CRLF/CR 统一为 LF 并去掉末尾单个换行——其余空白与内容必须逐字节一致。快照通过snapshot子命令写入发布状态 JSONverify-state子命令在 resume、Draft、publish 和验证阶段重新比对防止人工编辑后继续使用旧 appcast。release_notes.py暴露五个 CLI 子命令全部通过--file与--version定位正文子命令作用validate严格校验文件并输出完整元数据 JSONrender渲染 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 强制冻结 Markdownrelease_content.py 负责把 changelog 固化到 GitHub Release 表面并提供capture、apply、validate-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 渲染 descriptionrelease-appcast.py 把 Sparkle 应用内更新日志完全收敛到 changelogset-link将目标版本 item 的description替换为render_markdown(read_notes(...))渲染结果移除旧的releaseNotesLink并写入fullReleaseNotesLinkset-description仅替换目标 item 的 descriptionfind-previous-beta/promote-previous-beta处理 beta 版本升级为稳定版时对前序 beta item 的 channel 清理validate对生成的 appcast 做全字段严格校验。validate子命令的校验面相当完整它要求appcast 版本集合与顺序不变新 item 置于首位旧 item 除 beta 转正外字节级不变通过canonical_item()归一化空白后比对itemtitle等于版本号、存在pubDatefullReleaseNotesLink或releaseNotesLink等于预期的 Release URLdescription与render_markdown(read_notes(...))完全相等minimumSystemVersion为13.0channel 与目标beta或稳定一致enclosure的 URL 等于下载地址、length等于归档文件真实字节数、type为application/octet-stream且必须存在 Sparkle EdDSA 签名sparkle:edSignature。这些约束可以在仓库 appcast.xml 中直接对照验证——例如 2.22.0 item 的description就是 2.22.0 changelog 渲染后的 HTMLfullReleaseNotesLink指向对应 Release Tagenclosure携带签名与长度。已发布版本的修订与同步release-notes-sync.py针对「已发布版本如何修订正文」的问题release-notes-sync.py 提供了端到端的同步工具核心约束是不允许单边修改。sync-notes version命令默认读取changelog/version.md流程为读取并渲染 changelog校验 UTF-8/LF/文件名通过gh api --include抓取线上 Release拒绝仍为 Draft 的版本并要求 ETag 用于条件更新抓取main、dev两个分支上的远端appcast.xml读取前后各取一次分支 head防止读取期间远端变更生成预览 JSON包含 Release body 的 unified diff、release_update_required、两个分支 appcast 是否需更新、本地分支是否落后远端--execute阶段校验origin确为 GitHub 仓库、在临时 worktree 中生成 main 分支的 appcast 提交、合并进 dev、--force-with-lease原子推送两个分支、按 ETag 条件更新 Release body最后重新拉取远端验证三个表面全部一致。同步成功后状态 JSON默认写入.tmp/release/version/state/notes-sync.json记录notes_sha256、html_sha256、status与各阶段结果任一步骤失败都会把failed_stage与错误写入状态文件后中止。因此文档给出的修订路径是先修改并提交对应 changelog再在一次明确授权的维护任务中同步 GitHub Release 与 appcast——单边手动修改 GitHub 或 appcast 会被校验识别为「内容漂移」并停止发布。测试与验证结果本次重构的验证横跨发布脚本测试、Skill 测试与线上只读比对数据来自 history 文档 与 exec-plan 文档发布脚本测试 26 个通过python3 -m unittest discover -s scripts/release/tests -p test_*.pyRelease 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 -n、jq -e . scripts/release/asc-workflow.json、asc workflow validate、python3 -m py_compile、skill-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 运行validate与render子命令再对照 appcast.xml 中 2.22.0 item 的description即可直观理解「原始 Markdown 进、确定性 HTML 出、哈希锁状态」的完整链路。【免费下载链接】Easydict一个简洁优雅的词典翻译 macOS App。开箱即用支持离线 OCR 识别支持有道词典 苹果系统词典 苹果系统翻译OpenAIGeminiDeepLGoogleBing腾讯百度阿里小牛彩云和火山翻译。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),仅供参考