Ghost 开源仓库贡献工作流全指南:从本地开发环境到可合并的 Pull Request
本文是 Ghost monorepo(根目录 package.json)贡献者路径的实战手册,系统讲解从"选一个 Issue"到"PR 通过 CI 合并进 main"的完整闭环,覆盖分支策略、pnpm check 一站式校验、Oxfmt 格式化、Changeset 发布意图记录、提交信息规范,以及维护者/外部贡献者两种分支发布方式。读完本文,你将掌握 Ghost 社区接受变更的全部质量门槛与操作命令,能够独立走完一次标准贡献流程。
本文的位置:开发文档的第二站
Ghost 的 docs 贡献者文档索引把贡献路径拆成了两段:先在 开发环境搭建指南 中把 Ghost Core 与前后端 watcher 跑起来,再按照本指南(docs/contributing/workflow.md)完成从写代码到提 PR 的收尾动作。与此同时,.github/CONTRIBUTING.md 提供精简版的规则摘要,而本文对应的完整工作流则面向仓库中 ghost/core(核心服务端)、apps/(Admin、Portal、Comments UI 等前端应用)、koenig/(Lexical 编辑器内核)与 packages/(@tryghost/* 可发布包)这几大类工作区。
挑选合适的任务:从 Issue 标签到社区讨论
在动手前,仓库用两类 Issue 标签引导贡献者:
good first issue:面向首次贡献者的"上手友好"任务,改动范围可控、上下文清晰;help wanted:项目欢迎的更大范围贡献清单。
两条规则决定了贡献的"自由度":
- 新功能与较大的产品/架构变更,应当先在 Ghost Forum 上与维护者充分讨论方案后再实现——这与
.github/CONTRIBUTING.md中"generally don't merge new features and larger changes without prior discussion with the core product team"的原则互相印证; - 聚焦的 bug 修复或已被认可的小改进,可以直接上手实现,无需前置讨论。
从最新的 main 起步:同步 + 建分支
任何贡献都要建立在干净的、与上游一致的 main 之上。在已克隆的 canonical 仓库中依次执行:
git fetch origin
git switch main
git pull --ff-only origin main
pnpm setup
git switch -c concise-change-name
git pull --ff-only保证本地main只做快进式更新,避免产生意外的合并提交;pnpm setup不是普通的包安装:查根 package.json 可以看到它实际执行pnpm install && git submodule update --init --recursive && git config --local blame.ignoreRevsFile .git-blame-ignore-revs,即一次性完成依赖安装、子模块初始化(Ghost 的默认主题等以 submodule 形式存在)与 git blame 忽略文件配置;- 分支名应当简短且描述变更意图(示例中的
concise-change-name),既便于维护者快速理解,也符合 Git 分支命名的可读性要求。
分支卫生的两条铁律:互不相关的改动必须放在不同分支、不同 PR 中;若较大的工作确需共享分支或发布分支,须先与维护者达成一致。普通 PR 一律以 main 为目标分支。
修改并验证:pnpm check 一站式质量门
行为发生变化时,必须同步新增或更新自动化测试。Ghost 是一个 monorepo,每个工作区(如 ghost/core、apps/admin)都有自己的 README.md,其中列有该区域专属的聚焦命令,改动时优先运行最贴合的那组命令,能显著缩短反馈回路。
改动完成、提交之前,在仓库根目录运行全仓的一站式检查:
pnpm check
从根 package.json 可以看到它的真实构成是三个连续步骤:
"check": "pnpm format:check && pnpm lint && pnpm test"
展开来看:
| 步骤 | 对应脚本 | 实际执行内容 |
|---|---|---|
| 格式检查 | format:check |
oxfmt --check(只报告不写入) |
| 静态检查 | lint |
Nx 批量 lint + lint:boundaries(依赖边界)、lint:packages、lint:docs(含 Markdown 链接校验) |
| 单元测试 | test |
Nx 批量 test,排除 @tryghost/e2e 与 ghost-admin 两个项目 |
其中 test 的排除项非常关键——这正是文档强调"pnpm check 不包含浏览器 E2E 与 Ember Admin 测试"的源码出处:这两类套件代价高昂,需要在你改动的区域涉及它们时单独运行(如 pnpm test:e2e、对 ghost-admin 单独跑 nx run ghost-admin:test)。而 CI 侧(见 .github/workflows/ci.yml)通过 Nx affected 图 + 路径过滤器自动挑选 PR 相关的 lint、unit、integration、acceptance、build 与 browser-test 任务,实现"只测改动影响面"的高效流水线。
格式化:全仓统一的 Oxfmt
Ghost 的源码格式由 Oxfmt(oxc 工具链 的格式化器)统一接管,唯一配置在根目录 .oxfmtrc.json:
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"embeddedLanguageFormatting": "off",
"ignorePatterns": ["**/build/**", "**/coverage/**", "**/dist/**", "..."]
}
两个要点解释了大多数格式化差异的来源:singleQuote: true 表示在 Oxfmt 默认基础上改用单引号;embeddedLanguageFormatting: "off" 则禁止改写字符串内嵌语言的内容,保证注释、模板字符串等内部文本永不被意外重排。ignorePatterns 覆盖了构建产物、fixtures、快照以及 Ember Admin、Koenig 编辑器等历史包袱较重的目录。
实际使用遵循"通常无事可做"的原则,因为质量门已前置到提交之前:
- CI 拒绝任何未格式化代码(见 ci.yml 中
pnpm format:check步骤); - pre-commit 钩子自动格式化暂存文件。查看
.lintstagedrc.cjs可以发现它不只跑格式化——对*.{js,ts,tsx,jsx,cjs}会执行scripts/format.js(Oxfmt)+ 按工作区归组的 ESLint;对ghost/core与apps/*的源码还会额外跑 dependency-cruiser 依赖边界检查;对**/*.md则叠加 markdownlint 与 remark 死链校验。
需要手动格式化时:
pnpm format path/to/file.ts # 只格式化指定文件
pnpm format # 无参数时格式化整个仓库
pnpm format:check # 只报告差异,不写入
另外两条纪律值得记住:不要为单个包添加格式化配置(根配置是唯一权威);为统一换用 Oxfmt 而做的一次性全仓重排提交被记录在 .git-blame-ignore-revs 中,GitHub 的 blame 视图会自动跳过它。若你的本地仓库还没配好,可以手动执行一次:
git config --local blame.ignoreRevsFile .git-blame-ignore-revs
(对全新 checkout,pnpm setup 已自动完成该配置。)
记录发布意图:pnpm change 与 Changeset
Ghost 会把 koenig/ 与 packages/ 下的 @tryghost/* 编辑器、适配器包发布到 npm。因此,任何影响可发布包的改动——包括改动该包所消费的 catalog 条目——都需要一个 changeset,以便包在发布时获得正确的版本号与 changelog 条目:
pnpm change
运行后选择 patch / minor / major 三档之一,判断依据是该包对外的兼容性影响:
| 版本档位 | 适用场景(按语义化版本约定) |
|---|---|
patch |
向后兼容的 bug 修复 |
minor |
向后兼容的新功能/行为增强 |
major |
破坏性变更、公共 API 移除 |
你在交互式提示中填写的 summary 会成为该包的 changelog 条目,所以要站在包使用者的视角描述变更结果,而不是罗列内部改动细节。生成的 changeset 文件落在仓库 .changeset/ 目录下(如 warm-hotels-make.md 这类随机命名文件,配合 ledger.yaml 记账)。
如果确实不需要发布——例如纯测试改动或内部工具链改动——就显式记录这一意图,而不是什么都不做:
pnpm change --bump none
用 pnpm change status 随时查看当前待处理的发布意图。这套规则由 CI 强制执行:ci.yml 中名为 Check app version bump 的 job 会运行 scripts/check-change.js,凡改动可发布包却缺少覆盖性 changeset 的 PR 都会被直接打回;而仓库内 Markdown(AGENTS.md、CLAUDE.md、changelog、包内 docs/)不影响发布行为,则无需 changeset。注意一个边界:包的 README.md 会随包一起发布,改它同样需要一次 release。
提交信息:让 main 可读、让发布说明可用
GitHub 上所有提交信息遵循 .github/CONTRIBUTING.md 中定义的规范格式:
<optional release-note emoji> <past-tense summary, at most 80 characters>
<optional issue relationship, or "no ref">
<why this change was made>
要点包括:
- 摘要以
Fixed/Changed/Updated/Improved/Added/Removed/Reverted/Moved/Released/Bumped/Cleaned等过去式动词开头,且不超过 80 字符; - 第二行保持空白;
- 有关联 Issue 时,用
ref <issue URL>、fixes <issue URL>、closes <issue URL>等受支持的关系表述并附 URL;显式写no ref表示"确无关联 Issue",也可以留空; - 正文解释 why——为什么做这个改动、为什么是现在、为什么采用这种方案,因为"改了什么"已经由 diff 体现了。
规范还定义了发布说明 emoji,仅当改动是对用户有意义的显著变更时才在摘要最前方添加,并以用户视角书写:
- ✨ 新功能
- 🎨 改进或变更
- 🐛 Bug 修复
- 💡 其他值得用户注意的变更
- 🌐 翻译提交(仅凭该 emoji 不会被选入生成的发布说明)
仓库的 commit-msg 本地钩子(.github/hooks/commit-msg.bash)会对大部分违规形式给出警告而不阻断提交,但 refs ...、ref: ... 这类非法形式会被强制修正为受支持的 ref ... 写法。
发布分支:维护者与外部贡献者两条路径
Ghost 仓库的一个特点是 任何人都可以直接 clone、运行和修改 canonical 仓库,无需先 fork(fork 只在准备提交 PR 时才需要)。发布方式取决于你是否拥有 TryGhost/Ghost 的推送权限。
维护者:直接推送
git push -u origin HEAD
gh pr create --base main
外部贡献者:fork + 独立 remote
在改动就绪后创建 fork,并利用 GitHub CLI 在现有 canonical checkout 上直接把 fork 挂成独立 remote:
gh repo fork --remote --remote-name fork
git push -u fork HEAD
gh pr create --repo TryGhost/Ghost --base main
--remote-name fork将 fork 以名为fork的 remote 添加到本地,与origin(canonical 仓库)区分开;--repo TryGhost/Ghost告诉gh pr createPR 的目标仓库是 canonical 仓库而非本地 remote 推断出的地址。
使用 GitHub Web 或桌面客户端的等价流程同样可行。文档特别提醒一个现代协作场景下的安全细节:如果由工具或编码 Agent 代为执行上述步骤,推送前务必核对目标 remote、分支与 PR 对象,确认无误后再授权。
打开 Pull Request:内容与协作规范
除维护者指定其他基准分支外,PR 一律以 main 为基准。参考 .github/PULL_REQUEST_TEMPLATE.md 的检查项,PR 描述应说清四件事:
- 为什么需要这个变更;
- 行为或契约发生了什么变化(包括破坏性影响);
- 它是如何被测试的(PR 模板要求"编写自动化测试证明改动有效");
- 任何兼容性、发布、数据库迁移或灰度上线方面的考量。
配套的协作要求:有关联 Issue 时在 PR 中链接它;可见的 UI 改动应附截图;如被要求则保持分支与 main 同步;用追加的新提交而非强推来响应评审意见(在 .github/CONTRIBUTING.md 中被明确为"we may ask you to rebase"的补充);CI 全绿是合并的前置条件,而与改动路径无关的任务被跳过属于预期现象,不必担心。
一次标准贡献的完整时间线
把以上各节串起来,一次符合 Ghost 社区预期的贡献大致是:
- 在 开发环境搭建指南 中完成本地环境(
pnpm setup+pnpm dev)并确认站点与 Admin 可访问; - 从
good first issue/help wanted选题,较大改动先在论坛对齐方案; git fetch origin && git switch main && git pull --ff-only origin main && pnpm setup,然后git switch -c concise-change-name;- 实现改动并补充/更新测试,运行工作区 README 建议的聚焦命令;
- 提交前运行
pnpm check(format:check + lint + test)确保本地全绿,必要时单独跑 E2E 或 Ember Admin 测试; - 若改动涉及
koenig/或packages/下的可发布包,运行pnpm change(或显式pnpm change --bump none)并提交 changeset; - 遵循 commit message 规范 提交(pre-commit 钩子会代劳格式化与部分 lint);
- 维护者
git push -u origin HEAD、外部贡献者gh repo fork --remote --remote-name fork && git push -u fork HEAD,随后gh pr create --base main并填写完整描述; - 响应评审、保持分支同步,等待 CI 全绿后由维护者合并。
至此,一条从本地开发环境通向 main 的完整贡献链路便打通了——这既是文档定义的标准流程,也是每次 CI 与钩子都在强制执行的仓库纪律。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00