Easydict 发布实现聚合重构:将 Release 能力统一收敛到 release-easydict Skill
【免费下载链接】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 曾在scripts/release/与.agents/skills/release-easydict/两处目录中分散维护发布实现,通过硬编码路径和 Python 导入互相依赖。本篇以仓库内执行计划 2026-09-20-consolidate-release-skill.md 为骨架,结合聚合后的实际目录与脚本源码,完整讲解这次"发布实现 Skill 化"重构的目标、设计决策、动作路由、工作流编排、状态隔离与验证体系。读完你可以掌握 Easydict 当前发布能力的权威目录结构、release-easydictSkill 的动作语义,以及asc workflow可恢复发布模型下的单入口用法。
背景:发布能力为何需要聚合
在本次重构之前,Easydict 的发布能力被拆散在两个位置:
scripts/release/:一批发布 shell/Python 脚本、测试和说明;.agents/skills/release-easydict/:项目专属 Skill,承载 Agent 的发布指令。
两者之间存在三类耦合问题:
- 硬编码路径互相依赖:两处代码通过写死的路径互相引用,移动任一目录都会破坏另一方;
- Python 导入跨目录:Skill helper 依赖对旧发布目录的
sys.path注入,模块边界不清; - 维护入口分散:发布入口、工作流、测试和说明需要跨目录同步维护,容易产生事实漂移。
用户明确要求:把发布实现全部聚合到项目专属 Skill,并删除不再需要的 legacy 脚本与流程图。这一决定的直接结果是仓库不再保留scripts/release/,发布相关的入口、工作流、脚本、静态配置与测试统一收敛到.agents/skills/release-easydict/。
目标、范围与验收标准
重构的目标结果定义得很干脆:发布入口、工作流、脚本、静态配置和测试统一位于.agents/skills/release-easydict/,仓库不再保留scripts/release/。
允许修改的路径被严格圈定为以下范围,避免迁移过程中越界改动无关模块:
| 路径 | 角色 |
|---|---|
.agents/skills/release-easydict/ | 聚合后的权威 Skill 目录 |
scripts/release/ | 待删除的 legacy 发布目录 |
.gitignore | 忽略运行状态临时目录 |
changelog/README.md | changelog 与发布关系说明 |
docs/releases/easydict.md | 公开发布用户指南 |
docs/design-docs/application-architecture.md | 应用架构说明 |
| 本执行计划及同任务 history | 归档记录 |
重构同时划定了明确的非目标:不执行真实的 Archive、公证、Git push、GitHub Release 或 Issue 写入;不改写历史计划和 history 中对旧路径的事实记录。这意味着本次迁移只动代码与文档的组织方式,不改变任何远程发布行为。
验收标准有四条,全部指向"迁移后的一致性":
- 现行代码和文档均使用 Skill 内路径;
- 发布测试和静态检查通过;
scripts/release/被删除;- 运行状态写入仓库临时目录(
.tmp/)而非 Skill 源码目录。
同任务的执行历史记录在 docs/histories/2026-09/2026-09-20-consolidate-release-skill.md,其中记载了本次变更的具体内容与验证结论,可作为审计入口。
聚合后的 Skill 目录结构
从当前仓库实际内容看,聚合完成后.agents/skills/release-easydict/呈现如下结构:
.agents/skills/release-easydict/ ├── SKILL.md # 动作路由、授权边界、默认值与完成条件 ├── assets/ │ └── export-options.plist # Developer ID 导出配置 ├── references/ │ ├── release-workflow.md # Release 生命周期执行契约 │ ├── issue-followup.md # 发布后 Issue 跟进流程 │ └── issue-followup-policy.md # Issue 关联与解决决策策略 ├── scripts/ │ ├── asc-workflow.json # asc 工作流图与检查点 │ ├── release-easydict.sh # 稳定命令行入口 │ ├── release-common.sh # 路径、发布配置与安全辅助函数 │ ├── release-preflight.sh # 环境、凭据与发布状态检查 │ ├── release-branch-sync.sh # worktree、临时分支与 Tag 同步 │ ├── release-publish-git.sh # Publish 合并预检与 lease 推送 │ ├── release-redraft.sh / release-redraft-git.sh # 同版本 Draft 安全替换 │ ├── release-build.sh # 版本更新、归档、导出与 build cache │ ├── release-package.sh # 公证、ZIP、DMG 与校验和 │ ├── release-appcast.sh / release-appcast.py # Sparkle 生成与严格校验 │ ├── release_notes.py # changelog 校验、快照、确定性渲染 │ ├── release-notes-sync.py # 已发布日志的预览/同步 │ ├── release_content.py # Draft 标题验证与更新(不编辑正文) │ ├── release_issues.py / release_pr_policy.py # Issue 跟进与 PR 过滤 │ ├── release-github.sh # 幂等 Draft/正式发布与资产验证 │ ├── release-verify.sh # 本地产物与远程状态验证 │ └── requirements.txt # 固定 Markdown 渲染器依赖(Markdown==3.8.1) └── tests/ # 正文、appcast、Git 流程、Draft 替换、Issue 等测试SKILL.md 的 front matter 声明了它的职责边界:
name: release-easydict description: 编排 Easydict macOS 的 draft、publish、release 和 resume, 整理英文 GitHub Release 内容,并处理发布后的 Issue 跟进。这套结构把"Agent 指令(SKILL.md + references)、可执行实现(scripts)、静态资源(assets)、行为测试(tests)"四类资产完整收拢到一个目录,是本次聚合的最终形态。
关键设计决策
单权威目录、单入口,不保留 wrapper 或符号链接
迁移刻意不保留 wrapper 或符号链接,避免形成双入口。设计意图在 history 中写得很明确:发布实现只保留一个权威目录和一个入口,迁移后的路径变化会同步更新所有现行引用。也就是说,不存在"旧的scripts/release/留一个壳指向新位置"这种过渡方案,删除就是彻底删除。
路径解析基于脚本位置与仓库根,而非 cwd
发布脚本会在临时 worktree中继续运行,因此路径定位必须基于脚本位置和明确的仓库根,而不是依赖调用时的当前工作目录。以入口脚本 release-easydict.sh 为例,开头就完成了自定位:
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)" WORKFLOW_SOURCE_PATH="$SCRIPT_DIR/asc-workflow.json" WORKFLOW_RUNTIME_DIR="$ROOT_DIR/.tmp/release/asc"release-common.sh 同样以BASH_SOURCE推导RELEASE_SKILL_ROOT与RELEASE_SOURCE_ROOT,并暴露一批可被环境变量覆盖的默认配置(远程名origin、仓库tisfeng/Easydict、分支dev/main、Team ID45Z6V4YD5U、签名身份、Sparkle Keychain accounted25519等)。这消除了 Skill helper 对旧发布目录的sys.path依赖,让脚本在任意 cwd 下都能正确定位仓库根、workflow 与 plist。
运行状态与源码目录隔离:.tmp/release/asc/runs/
asc工具固定把状态写到 workflow 文件旁的runs/目录。为避免污染 Skill 源码目录,入口脚本会把 workflow 的运行时副本放到仓库临时目录:
WORKFLOW_RUNTIME_DIR="$ROOT_DIR/.tmp/release/asc" WORKFLOW_PATH="$WORKFLOW_RUNTIME_DIR/asc-workflow.json" WORKFLOW_RUNS_DIR="$WORKFLOW_RUNTIME_DIR/runs"因此asc的原始运行状态落在.tmp/release/asc/runs/,而非 Skill 源码目录。.gitignore中已有.tmp/忽略规则,运行状态属于仓库临时数据,不进版本控制。
文档分工:Skill reference 与公开用户指南
- 原
scripts/release/README.md改为 Skill reference,承载 Agent 执行细节; - 公开用户指南继续由
docs/releases/easydict.md维护,面向发布维护者; changelog/README.md说明 changelog 与 GitHub Release 正文、Sparkle appcast 的发布关系;docs/design-docs/application-architecture.md同步更新架构描述。
发布动作路由:SKILL.md 定义的生命周期
聚合后的 Skill 以 SKILL.md 为动作总纲,Release 生命周期包含五个动作:
| 动作 | 语义 | 完成边界 |
|---|---|---|
draft <version> | 创建或恢复经过验证的 Draft,整理英文正文和重点标题后停止 | Draft、Tag、临时发布分支、changelog 和正文哈希全部验证后才算完成,不发布也不处理 Issue |
draft <version> --replace-draft | 从已同步并提交的本地dev安全重建最新且匹配的未发布 Draft | 废弃旧 Draft 并以新构建替换 |
publish <version> | 整理已有且经过验证的 Draft,发布并验证,然后运行内部 Issue 跟进 | Release、appcast、Git 引用和 Issue 跟进全部最终核验后才算完成 |
release <version> | 依次执行 Skill 的draft和publish | 不直接调用仓库脚本的一次性release动作 |
resume <version-or-run-id> | 使用现有 skill 和 asc 状态,只继续未完成的 Release 生命周期阶段 | 不启动新的替换或发布 |
已发布版本的日志修订走独立的sync-notes <version>动作:默认只预览,仅在显式--execute时同步已发布 GitHub Release 正文、远程main/dev的 appcast 以及本地分支。该动作不重建产物、不改 Tag/附件/版本号,也不替代resume。
发布后的 Issue 跟进有独立的三个动作:issue-followup plan <version>(生成本地计划,不评论/关闭 Issue)、issue-followup apply <version>(执行通知与关闭)、issue-followup resume <version>(恢复中断的 Issue 动作)。注意区分:issue-followup resume恢复 Issue 跟进,resume恢复 Release 生命周期。
授权边界
SKILL.md 对 Agent 的写权限做了精细约束:
- 普通规划、解释、检查保持只读;
issue-followup plan只查询 GitHub 并写入.tmp/release/<version>/state/issue-followup/下被忽略的本地状态,不评论或关闭 Issue;- 只有用户针对具体版本或运行明确请求
draft、publish、release、Releaseresume、sync-notes --execute、issue-followup apply/resume时才执行远程修改; - 用户明确请求
publish或release后,同一版本通过远程发布验证时,同时授权其内部issue-followup apply阶段。
默认值与完成条件
- 默认使用
betachannel,除非用户明确要求stable,所有底层命令沿用同一 channel; publish/release只有在 Release、appcast、Git 引用和 Issue 跟进都得到最终核验后才完成;Issue 阶段失败时不回滚已发布的 Release 或已完成动作,而是报告可恢复状态;- 最终报告必须包含 Release URL、标题、channel、notes 路径、Issue 摘要、底层 run ID 和可恢复状态路径;失败时说明准确阶段和已经发生的外部变更。
Release 生命周期执行契约
执行draft、publish、release或 Releaseresume时,Agent 必须遵守 references/release-workflow.md 定义的契约。
Git 与状态边界
draft生成、验证并提交 appcast,然后只推送指向 appcast 提交的release/sync-<version>临时分支和指向版本提交的版本 Tag,不得把 Draft 提交直接推送到origin/dev或origin/main;publish在 GitHub Release 公开前完成 merge 预检,公开后用 Draft 阶段冻结的 appcast 提交安全更新本地dev,再原子更新远程dev、main和临时发布分支;- 远程验证通过后删除临时发布分支;版本 Tag 停留在版本元数据提交,
main停留在 appcast 提交,dev停留在包含最新开发提交和 appcast 提交的集成结果; - Publish 失败时使用 asc run ID 恢复,不手工 rebase 或强推这些引用。
Release 内容状态保存在.tmp/release/<version>/state/release-notes.json,冻结版本、Markdown SHA-256、渲染器标识和 HTML SHA-256。Issue 状态只使用.tmp/release/<version>/state/issue-followup/的 schema v2;直接存放在state/下的 schema-v1 文件保留为审计数据,不自动复用、迁移或删除。
工作流 8 步
- 验证请求版本、channel、当前 GitHub Release、
.tmp/release/<version>/状态和关联 asc run ID; - 创建新 Draft 前,根据上一个版本以来的已合并 PR 创建或更新
changelog/<version>.md,应用统一 bot PR 过滤策略,提交到本地dev后运行"验证 changelog"; - 运行"创建 Draft";Draft 已存在则验证并复用,只有用户明确要求时才"替换 Draft";
- Draft 直接使用冻结的 changelog,创建后重新获取正文做一致性验证;根据真实 PR 选择重点并生成英文标题,先预览再用
--execute执行,helper 不编辑正文; draft报告经过验证的 Draft、changelog 路径和正文哈希后停止;publish/release运行"发布 Draft",先做 merge 预检,校验冻结 appcast 后安全更新本地dev,用 lease 原子更新远程引用;发布、appcast 安装和远程验证全部成功前不继续;- 执行
issue-followup apply <version>(在修改前创建新计划,不依赖此前独立运行的plan); - 报告 Release URL、标题、channel、notes 路径、Issue 和无关联 PR 摘要、底层 run ID 和可恢复状态路径。
内容决策
- changelog 只翻译每个变更条目中由人编写的 PR 标题部分,保持英文标题简洁;作者、PR 链接、贡献者和比较范围保持不变;
- 按以下顺序选择重点:安全/数据丢失/崩溃修复 → 重要用户可见功能 → 重要用户可见修复 → 较小产品改进;只有不存在产品变更时才选择维护项;
- 标题格式为
<version> <emoji> <type>: <concise English summary>,通常采用✨ feat、🐞 fix、🔒 security、🚀 perf或🔧 chore; - 存在用户可见功能或修复时,不选择文档、生成资源、依赖升级或内部重构作为重点。
已发布日志的独立同步
发布完成后人工修改了changelog/<version>.md时,不要重新运行resume、draft或publish——这些动作分别用于恢复中断的 ASC 工作流、重建 Draft 和发布 Draft,不会把发布后的日志修订当作新的构建发布。正确做法是先预览:
./.agents/skills/release-easydict/scripts/release-easydict.sh sync-notes <version>确认预览内容后才执行远程同步:
./.agents/skills/release-easydict/scripts/release-easydict.sh sync-notes <version> --execute可选参数包括--repo <owner/repo>、--notes-file <path>和--state <path>。该动作要求 Release 已公开且 Tag 与版本一致;--execute要求当前 worktree 干净,更新 Release 时使用 ETag,更新分支时使用 branch head 和 Git push lease 做乐观并发校验。状态摘要保存在.tmp/release/<version>/state/notes-sync.json,部分成功后再次执行会重新读取远程状态并跳过已经一致的目标。
asc workflow 编排:可恢复的检查点
发布引擎的核心是 scripts/asc-workflow.json,它把构建、公证、打包、GitHub 和 Sparkle 拆成可恢复的检查点。文件顶部声明了五个环境变量作为动作参数:
| 变量 | 默认值 | 含义 |
|---|---|---|
VERSION | 空 | 目标版本号(x.y.z) |
CHANNEL | beta | Sparkle 渠道,可选beta/stable |
BUILD_NUMBER | 空 | 覆盖下一个构建号 |
DRAFT_MODE | normal | 普通 Draft 或replace替换模式 |
FORCE_CLEAN | 0 | 是否强制 clean Archive |
顶层声明before_all/after_all/error钩子输出开始、完成与失败日志,失败提示语明确要求"用 asc 打印的 run ID 恢复"。四个公开工作流组合两个私有步骤组:
prepare:仅prepare_steps;draft:prepare_steps+draft_steps;publish:仅publish_steps;release:prepare_steps+draft_steps+publish_steps。
prepare_steps:本地产物准备
按顺序执行 14 个步骤:preflight_environment→snapshot_draft_replacement→archive_replaced_local_state(仅替换模式)→sync_local_dev→prepare_release_worktree→preflight_release→update_version→archive_application→export_application→notarize_application→create_sparkle_zip→create_notarized_dmg→generate_appcast_candidate→verify_local_release。
即:先做环境与凭据检查、同步分支并准备隔离 worktree,再更新版本号、用asc xcode archive归档、xcodebuild导出、提交 App 公证并 staple、生成 Sparkle ZIP 和 DMG、对 DMG 二次公证、生成候选 appcast,最后完成本地验证。prepare阶段只写 Apple 公证请求,不写 Git/GitHub。
draft_steps:创建 GitHub Draft
8 个步骤:revalidate_draft_replacement→prepare_channel_transition→install_appcast→push_draft_refs→delete_replaced_github_draft→create_github_draft→verify_github_draft→cleanup_draft_replacement。Draft 阶段冻结并提交候选 appcast,原子推送临时发布分支和版本 Tag,创建并验证 GitHub Draft,但不会公开 Release、更新主分支,也不评论或关闭 Issue。
publish_steps:公开与推广
9 个步骤:preflight_publish→prepare_publish_git→publish_github_release→verify_github_release→push_published_refs→promote_previous_github_release→verify_remote_release→cleanup_remote_release_branch→cleanup_release_worktree。公开 Release 前完成 merge 预检,公开后用 lease 原子更新dev/main与临时分支,对 beta 发布把上一 GitHub prerelease 提升为 stable,远程验证两代 Release、引用、资产和公开 Sparkle feed 后再清理临时分支与 worktree。
命令行入口
底层脚本的统一入口是:
./.agents/skills/release-easydict/scripts/release-easydict.sh <action> <version> [options]常用参数与约束(源码中均有校验逻辑):
--channel beta|stable:Sparkle 渠道,默认beta;分阶段执行时draft和publish必须一致;--build-number <value>:指定构建号,必须是正整数且高于公开 appcast 的最新构建号;--replace-draft:仅用于明确替换当前最新 Draft,不能与--build-number同用;--force-clean:仅prepare、draft、release支持,强制 clean Archive;--dry-run:只预览asc workflow,不执行步骤;resume <run-id>:只接受 run ID,从 run 文件中解析版本并推断 workflow 名称,不支持其他参数。
入口脚本会把asc的机器可读 stdout JSON 保存为logs/workflow-<run-id>.json,同时通过命名管道把人类可读的 stderr 输出到终端和logs/workflow-<run-id>.log,并在终端打印格式化摘要(工作流状态、失败步骤、错误、run ID、Draft/Publish 的 Git 引用、实际步骤耗时)。
状态、日志与恢复位置
所有运行状态都隔离在.tmp/下(已被.gitignore忽略),主要位置:
| 路径 | 内容 |
|---|---|
.tmp/release/<version>/state/ | 版本、正文、构建、Draft、Publish 和恢复状态 |
.tmp/release/<version>/state/publish-git.env | Publish 的 Git 集成状态 |
.tmp/release/<version>/state/issue-followup/ | Issue 候选、决策、计划、汇总和动作状态 |
.tmp/release/<version>/logs/workflow-<run-id>.json | asc机器可读结果 |
.tmp/release/<version>/logs/workflow-<run-id>.log | 完整工作流日志 |
.tmp/release/asc/runs/ | asc原始运行状态 |
.tmp/release/cache/worktree | 只用于本地 Archive 的长期构建 worktree |
失败时先保存终端给出的 run ID 和路径,不要手工 rebase、强推或删除现场;修复根因后执行:
./.agents/skills/release-easydict/scripts/release-easydict.sh resume <run-id>长期构建 worktree 使用带 fingerprint 的 Release DerivedData:兼容缓存会被复用,增量 Archive 失败时只清理当前 fingerprint 并自动回退一次 clean Archive;缓存命中不降低签名、公证、stapling、appcast 或远程验证要求。
验证与测试矩阵
本次迁移的验证覆盖了结构、行为、语法、配置和静态检查多个层面,全部通过:
| 检查 | 命令 | 结果 |
|---|---|---|
| Skill 结构 | quick_validate.py .agents/skills/release-easydict | 通过 |
| 行为测试 | python3 -m unittest discover -s .agents/skills/release-easydict/tests -p 'test_*.py' | 通过,71 tests passed,覆盖非仓库 cwd、运行时 workflow 路径和 dry-run 参数路由 |
| Shell 语法 | bash -n .agents/skills/release-easydict/scripts/*.sh | 通过 |
| Python 编译 | python3 -m py_compile ... | 通过 |
| 工作流 JSON | jq -e . .agents/skills/release-easydict/scripts/asc-workflow.json | 通过 |
| 导出配置 | plutil -lint .agents/skills/release-easydict/assets/export-options.plist | 通过 |
| 链接与旧路径 | 相对 Markdown 链接与旧现行路径扫描 | 通过 |
| 差异检查 | git diff --check | 通过 |
| Review | 基于初始 HEAD91aa7a6f5be8cfc4dca86f9a1117418d76802a4f审查变更 | 无 findings |
需要特别说明的验证边界:执行环境没有安装asc,因此未运行真实的asc workflow validate,而是通过jq校验、workflow 行为测试和 fake-ASC 路由测试覆盖迁移契约;按任务范围也未执行 Archive、公证或任何远程写入。作为后续事项,在安装asc的发布机器上、下一次真实发布前,应运行:
asc workflow validate --file .agents/skills/release-easydict/scripts/asc-workflow.json --pretty对发布维护者的影响
聚合完成后,Easydict 的发布维护入口与文档分工如下:
- 标准入口:
release-easydictSkill(在支持 Skills 的 Agent 中调用),日常 draft/publish/release/resume/sync-notes/issue-followup 全部由此进入; - 公开指南:
docs/releases/easydict.md仍是发布维护者的唯一开发者指南,覆盖本地环境、Apple 账号与签名凭据、Git/GitHub 配置、发布模型、主要命令、恢复和工具维护,且不保存真实密钥; - changelog 约定:changelog/README.md 明确了
<version>.md文件名约定、UTF-8/LF 要求、正文与 GitHub Release 及 Sparkle appcast description 的发布关系,以及"冻结后漂移即失败"的哈希校验机制; - 架构说明:
docs/design-docs/application-architecture.md同步更新了发布模块的架构描述。
修改发布 Skill、脚本或 helper 后,至少运行单元测试、bash -n和git diff --check做回归验证;xcodebuild、Archive、公证及远程 GitHub 写入必须按具体任务单独授权和验证——文档与单元测试通过不等于真实发布通过。
完成条件回顾
本次聚合以如下状态收尾:
- Skill 结构和全部现行引用完成迁移,legacy 资产与
scripts/release/已删除; - 风险匹配的测试、静态检查、dry-run 和 Review 通过;
- history 已创建(docs/histories/2026-09/2026-09-20-consolidate-release-skill.md),计划已归档,变更已创建本地提交。
至此,Easydict 的发布能力拥有了单一权威目录、单一入口和完全可恢复的asc workflow检查点模型:发布自动化与 Agent 指令、静态配置、测试同处一处,运行状态与源码彻底隔离,后续任何发布路径的演进都只需要在一个目录内完成。
【免费下载链接】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),仅供参考