PostHog 分布式代码所有权模型:从 CODEOWNERS-soft 到可编程的 owners.yaml 单一事实源
PostHog 分布式代码所有权模型从 CODEOWNERS-soft 到可编程的 owners.yaml 单一事实源【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog导读PostHog 是一个横跨 PythonDjango、TypeScript前端、Rust采集/消费管道等多个技术栈的超大 monorepo其代码所有权此前分散在.github/CODEOWNERS-soft、products/*/product.yaml两份手工维护的数据源中并被四个各自实现 YAML 解析的消费者重复读取事实早已开始漂移。本篇基于 docs/internal/ownership-model-proposal.md 与仓库中已经落地的 tools/owners 实现完整讲解 PostHog 如何用一套分布式的owners.yaml格式统一所有权语义从 schema 字段、Slack 频道推导、nearest-file-wins 解析模型到owners:resolve/owners:lint/owners:fmt一整套 CLI 工具与一个 resolver、多个消费者的架构。读完你既能直接复用这套格式管理自己的仓库所有权也能理解其底层源码如何把谁拥有这条路径变成可查询、可校验、可证明等价的数据。说明该文档状态标记为 implemented随引入它的 PR 一并落地因此文中所有格式、命令与路径均以当前仓库实际内容为准。1. 为什么重构中央式所有权文件的病灶提案开篇先做了一次行业先行者prior art调研覆盖五种代表性系统系统放置方式匹配解析角色继承控制元数据GitHub CODEOWNERS单个中央文件整文件 last-match-wins无无无GitLab CODEOWNERS单个中央文件按 section last-match-winssections role引用section 级无Gerrit/Chromium OWNERS每目录祖先并集无Gerrit labelsset noparent、per-file同级 DIR_METADATA组件、团队联系人Kubernetes/Prow OWNERS每目录、YAML祖先并集approversvsreviewersoptions.no_parent_ownerslabels、emeritus_approvers、根OWNERS_ALIASESBackstage catalog-info.yaml每组件显式引用、无 glob类型化spec.ownerGroup不适用开放式 annotations由此沉淀出几条直接塑造本提案的教训中央单文件必然因模式顺序 bug而腐烂last-match-wins 让行序变成承载语义的东西一个后加的宽泛 glob 会悄悄吞掉之前更具体的规则。PostHog 自己的CODEOWNERS-soft就已经依赖刻意排序——managed-reverse-proxy 块必须保持在最后。每目录文件匹配谁拥有_这个_目录的心智模型且能随 monorepo 扩展但需要一个覆盖率审计工具——没有任何机制强制新目录必须获得 owner。approver/reviewer 的角色拆分Kubernetes是最有价值的一对区分能否阻止合并与是否应看到这个 PR是两个不同的集合。PostHog 其实早已拥有这个拆分硬CODEOWNERSvs 软文件只是编码成了两份格式不同、消费者不同的文件。Kubernetes 用可 PR 评审的OWNERS_ALIASES文件避开 GitHub Teams成员变更无审计轨迹。PostHog 反其道而行确实需要 GitHub team slug自动分配器需要它们因此刻意跳过 alias 层个体以handles形式内联出现与product.yaml既有约定一致。编译到 CODEOWNERS模式结构化 YAML 为源、扁平 CODEOWNERS 为构建产物很常见能免费保留平台原生集成。2. 重构前的仓库现状两份数据源、一个共享匹配器、四个消费者提案对迁移前的状态做了精确盘点数据源.github/CODEOWNERS58 行阻塞式刻意保持极小只覆盖基础设施/安全关键目录header 注明extraordinary justification。利用无 owner 的行作为reset为子树清除阻塞 owner如posthog/hogql/database/schema/**。.github/CODEOWNERS-soft383 行非阻塞评审标记承载products/之外共享代码的全部产品映射约 25 个团队。其 header 已声明产品级所有权属于product.yaml本文件只用于子文件夹覆盖、次级评审者与products/之外的路径。products/*/product.yaml68 个文件恰好两个字段name和owners团队 slug 列表偶尔individual。拥有products/name/**全部内容且在所有权 skill 使用的解析中胜过任何 CODEOWNERS 条目。消费者及其解析器消费者读取内容解析器.github/scripts/assign-reviewers.jsCI 自动分配soft product.yamlvendored matcher 自研 YAML 迷你解析器hogli product:lint:ownersproduct.yaml自研 loaderproduct_yaml.py对照 GitHub 实时团队校验 slug.agents/skills/establishing-code-ownership/ownership.jshard soft product.yamlvendored matcher 自研 YAML 迷你解析器products/stamphog/packages/pr-approval-agent/gates.py仅 soft完全独立的重复实现其中 vendored matcher 是.github/scripts/codeowners.jshmarr/codeowners的移植忠实于 GitHub 语义。被确认的缺口同一份文件的四个解析器只有两个共享匹配器漂移只是时间问题只有product.yaml被 CI 校验没有任何机制检查CODEOWNERS-soft的 glob 是否仍匹配现存文件、团队 slug 是否仍然存在分配器只能通过运行时的 422 错误发现死团队product.yaml没有 schema也没有 oncall/Slack/升级路由消费者对作用域认识不一致分配器与 pr-approval-agent 忽略硬文件只有 skill 叠加三层生成/供应商/废弃代码没有结构化表达——分配器硬编码了一份忽略清单frontend/src/generated/**、*.ambr、锁文件等。3. 规范格式owners.yaml完整 schema提案给出的核心格式是一个 schema、按目录分布式放置。在products/下现有product.yaml本身就是所有权文件见别名规则无需新增文件。完整形式# owners.yaml — 完整形式除 owners 外每个字段都可选 version: 1 # 唯一必填字段与 product.yaml 相同的键和语义。 # 有序的 GitHub 团队 slug省略 PostHog/ 前缀与 handles 混合列表 # 第一个条目是主要 owner用于默认值如派生的 Slack 频道见下方 teams: 注册表。 # 单个 owner 可以裸写owners: team-error-tracking。 # Owners 会被标记评审非阻塞并回答谁拥有这条路径。 owners: [team-error-tracking, pauldambra] # 本目录下代码的生命周期。 # active默认 | deprecated | generated | vendored status: active # 为 true默认时本文件未设置的字段回退到最近的祖先 owners.yaml。 # false Gerrit 的 set noparent。 inherit: true # 本目录内的按路径覆盖仅在本文件内按 last-match-wins 求值。 # owners、status、inherit 均可按规则覆盖。match 是一个 glob 或 glob 列表 # 每个 glob 是独立的边界——列表只是重复 owners/status 的简写。 rules: - match: generated/** status: generated - match: vendor/** status: vendored owners: null # 显式重置设计上无主lint 豁免 - match: [migrations/**, legacy/**] owners: [team-error-tracking, team-data-modeling]90% 的场景只需两行# rust/owners.yaml version: 1 owners: team-ingestion仓库中真实的 rust/owners.yaml 正是这种风格——顶层owners: []表示本层不贡献再用锚定的目录规则把各子目录分派给对应团队version: 1 owners: [] rules: - match: [/behavioral_cohorts_migrations/, /capture/] owners: team-ingestion - match: [/common/symbol_data/, /cymbal-proto/, /cymbal/] owners: team-error-tracking - match: /embedding-worker/ owners: oliverb123 - match: [/feature-flags/, /flags-consumer/, /flags_read_store_migrations/] owners: team-feature-flags - match: /hogql/ owners: team-data-tools - match: - /replay-anonymizer/ - /replay-anonymizer-node/ owners: ai-research这里可以看到两个值得注意的用法个体 owneroliverb123与团队 slug 混排以及ai-research这种早于team-命名约定的真实组织 slugGitHub 上不存在team-ai-research团队slug 无法修复只能靠根注册表把频道映射到位见下文。3.1product.yaml作为受认可的别名products/name/product.yaml若带有owners:键会被 resolver 当作拥有同样owners:列表的owners.yaml读取。product.yaml中其他所有字段今天的name:以及将来新增的任何字段对所有权解析一律忽略——product.yaml可以继续自由增长产品元数据而不触碰所有权 schema。三条硬性规则一个目录可以存在带owners的product.yaml或owners.yaml二者不能共存——lint 报错产品内部的子文件夹覆盖与别处一样使用嵌套owners.yaml如products/x/backend/migrations/owners.yamlteam-CHANGEME脚手架占位符解析为 unowned与所有消费者既有处理一致。这让全部 68 个产品零迁移成本且hogli product:lint:owners原样保留。schema 层的对应实现见 tools/owners/posthog_owners/schema.py 中的parse_product_yaml_as_owners只读owners:键并对team-CHANGEME做normalize_product_owners归一化以及_validate_owners_value对空字符串条目的拒绝逻辑——owners: []会被视为已覆盖而分配器却会丢弃 falsy owner 导致无人被请求因此宁可报错也不过滤。3.2 Slack 推导实测而非假设提案记录了一次对全部在用团队 slug 的 Slack 频道实测首轮 2026-07-15。按根owners.yaml注册表统计30 个在用 slug 中22/30#slug原样存在无需任何条目8/30需要根teams:注册表中的条目ai-research、batch-exports、clickhouse、conversations、mcp-analytics、platform-ux——早于team-约定的真实组织 slugteam-clickhouse等 GitHub 团队并不存在slug 无法修复频道是#team-slugteam-data-stack→#group-data-stackteam-posthog-desktop→#team-desktop频道创建时的名字。即派生默认值对绝大多数团队是对的其余团队各带一条条目即可。没有任何机制校验派生的频道真实存在——错误或死亡的频道会静默失败一个可选的 Slack-API 检查镜像现有可选的 GitHub 团队实时校验被记入owners:lint的待办尚未实现。teams:注册表仅限仓库根频道到团队的映射只在根声明一次绝不逐文件重复——一个横跨多目录拥有路径的团队不应在每个目录重申自己的频道。仓库根的owners.yaml可以携带teams:注册表——团队 slug 到该团队声明的频道的映射。每个值都是以#开头的字符串或false表示无频道、不派生slack—— 人在的地方。这是指给人看的频道notifications—— 自动化发布的位置。它回退到slack所以从不分开两者的团队只保留一个条目notifications: false把自动化挡在门外同时不向人隐藏团队频道。notifications也可以是生产者名 → 频道的映射让团队可以单独静默或重定向某一只 bot。映射未命名的生产者回退到slack。只有 schema 已知的生产者才能被命名所以拼写错误是 lint 错误而不是永远不生效的退出开关。# owners.yaml仅仓库根 teams: clickhouse: slack: #team-clickhouse team-data-stack: slack: #group-data-stack notifications: #group-data-stack-bots quiet-team: slack: #team-quiet notifications: false digest-free-team: slack: #team-digest-free notifications: stamphog: false some-retired-team: slack: false一个团队只有在声明了至少一个频道时才被注册因此 slug 出现在注册表中只意味着该仓库为这个团队做了应答。消费者按自己需要的 purpose 提问这正是让 bot 的目标频道与人被指给的频道相互隔离的机制。约束任何非根owners.yaml携带teams:都是 schema 错误product.yaml别名永远不会携带它不存在按路径或按文件的 Slack 覆盖——注册表加上派生默认值是设置频道的唯一途径。路径有效 Slack 频道的优先级链先命中者胜注册表中主要 ownerowners[0]的条目但仅当它是团队 slug而非handle时——false条目抑制派生当主要 owner 是团队 slug 时的派生#owners[0]否则None。注册表只改变 resolver 计算频道的方式线上格式不变——slack仍以最终字符串或null流出。在 tools/owners/posthog_owners/resolver.py 中team_channel()实现声明 vs 派生两种结果TeamChannel.declared区分这是团队的决定与这是派生的猜测路由真实消息的调用方通常会把前者当作必须遵守的决定、后者当作需要验证的猜测。真实仓库根的注册表见 owners.yaml包含team-replay的notifications: { stamphog: false }这类频道对了、只为去掉每日 digest的条目。3.3 个体 owner无别名层不存在OWNERS_ALIASES文件。owners:直接接受 GitHub 团队 slug 或handles——product.yaml在user_interviews上已经在用的同一约定。lint 用校验团队 slug 的同样方式校验 handle 的组织成员身份。自动分配器通过 API 的reviewers字段请求个体评审者当前版本完全跳过条目个体只能通过CODEOWNERS-soft生效——这正好补上了这个缺口并让软文件的接线随文件一起消亡。3.4 设计抉择逐条论证分布式而非中央仓库已经用脚投票——product.yaml是每目录的软文件自己的 header 也在把所有权推向它。中央文件正是要逃离的东西顺序 bug、单热点文件的合并冲突。给我看全貌的审计叙事是工具的活hogli owners:map不是文件布局的。最近文件胜出 按字段回退而非祖先并集Kubernetes 向上并集 approver 是因为它的标准是必须有人批准PostHog 的软模型是标记对的那个团队别给五个都发垃圾。并集继承会让posthog/的 owner 出现在每个深层 PR 上。覆盖语义也与所有权 skill 既有的实现一致product.yaml 胜过 CODEOWNERS。回退是按字段的只设了owners的子文件仍会从祖先继承status。rules:是文件局部的跨文件的 glob 交互正是 CODEOWNERS 的脚枪这里一个 glob 只能覆盖自己目录的默认值因此读一个文件加其祖先就能完整解释任意路径。owners: null是显式的不是缺席无主必须是决策vendored 代码、草稿目录绝不能是默认。覆盖率检查把未解析当作错误把owners: null当作有据可查的豁免。在 resolver 中这个区分由Resolution.unowned_by_design承载is_unowned只有在无 owner 且非设计豁免时才为真——这正是覆盖率检查失败的条件。单一owners列表无角色拆分Kubernetes 式的teamvsreviewers区分被考虑过然后放弃了——在阻塞语义不在范围内的情况下谁负责与谁被标记是同一个集合且product.yaml已经说owners的语言。顺序承载了唯一需要的额外信号——第一个条目是主要 owner。未来若需要阻塞角色是新增字段而不是现在拆。status:取代硬编码忽略清单分配器的生成文件忽略列表、评审噪音抑制以及未来工具如把 vendored 代码排除出 lint都锚定这一个字段而不是 N 份拷贝。这在实际根文件 owners.yaml 中清晰可见——它不再需要维护一份忽略清单而是直接对Dockerfile、docker-compose*.yml、owners.yaml、/bin/、/cli/等路径分派 owner注意owners.yaml那条非锚定规则每个 owners.yaml 都路由给 devex所有权策略的编辑绝不能无人评审地流出。4. 解析模型nearest-file-wins对路径P解析按序进行从仓库根向P行走沿途收集每个owners.yaml或别名product.yaml。若某文件设了inherit: false丢弃其上方收集到的一切。有效配置 浅合并最近文件按字段胜出列表是替换绝不合并——可预测性胜过聪明。在最近的、带rules:的文件内应用其matchglobgitignore 风格语义随 schema 一起文档化匹配P相对该文件目录的最后一条规则。规则字段覆盖合并后的配置。评审标记 解析出的owners。主要 owner 其第一个条目。行走中没有所有权文件且无规则命中 →unowned除非路径位于owners: null规则之下否则覆盖率检查失败。集中 vs 分布式是可读性选择不是语义差异父owners.yaml可以通过锚定目录规则- match: /rust/capture/拥有子树而更深的owners.yaml仍按最近文件解析胜出。因此一组单行子文件与一个带等价规则的父文件解析结果完全相同选哪个读起来更舒服就用哪个。owners:lint在某个目录积累够多单一用途子文件时会给出折叠建议。硬.github/CODEOWNERS完全在行走之外保留其 GitHub 原生语义手工维护、GitHub 强制resolver不读它——需要时手动或通过 GitHub 自身 UI查阅阻塞所有权。它永不影响解析出的owners本提案也不向它写入任何内容。在源码里这一模型的实现要点resolver.py_ancestor_dirs生成从根到目标父目录的目录序列_load_dir_file对每个目录先找owners.yaml找不到才尝试product.yaml别名两者同存时 resolver 偏向owners.yamllint 报冲突_collect_files收集时不应用inherit裁剪——因为匹配的规则可能以任意方向覆盖文件级标志裁剪必须推迟到resolve中逐贡献进行_file_contribution用文件内 last-match-wins 找到匹配规则产出贡献副本resolve中inherit: false是唯一的set noparent站点文件级裁剪在此生效而规则级inherit: true可以为匹配路径恢复祖先测试test_rule_level_inherit_true_restores_ancestors_under_file_level_cut专门验证了这个方向。全局匹配语义由 tools/owners/posthog_owners/matcher.py 实现——它是.github/scripts/codeowners.jshmarr/codeowners的移植的 Python 移植忠实复刻 GitHub 的段语义前导斜杠根锚定、无斜杠名字按**/前缀处理、尾随斜杠表示该目录及其下一切、*绝不跨越/、字面末段拥有其整个子树。tools/owners/tests/test_owners.py中的test_matcher_vectors给出了完整的行为矩阵如/foo/bar匹配foo/bar及其子树但不匹配x/foo/bardocker-compose*.yml匹配任意深度的a/b/docker-compose.dev.yml。5. 工具架构一个 resolver众多消费者稳定性保证是架构性的消费者从不自行解析所有权文件。一个 resolver 库独占语义其余一切都调用它。Resolver单一实现位于可安装包posthog-ownerstools/owners/以库形式暴露resolve(path)、map()、unowned()并以 CLI 形式暴露hogli owners:resolve --json path...路径也可从 stdin 读取。理由是hogli 已经做所有权 lint且四个消费者中有两个是 Python这让 lint、查询与 pr-approval agent 成为零子进程跳转的原生库调用方。rules:的 glob 匹配使用在此实现并文档化的 gitignore 风格语义——vendored JS matcher 只保留给硬 CODEOWNERS 叠加解析或换成等价的 Python CODEOWNERS 解析器。JS 消费者调用 CLI 并读 JSON——assign-reviewers.js把 PR 的变更文件喂进去拿回解析出的 ownersestablishing-code-ownershipskill 做同样的事。自动分配工作流因此增加了一个 Python/uv 安装步骤今天它只有 node。gates.py直接导入库。无提交的 lockfile因此也就没有新鲜度检查机制若将来需要离线消费者、Backstagecatalog-info.yaml发射器那只是对map()的一次平凡折叠。Validatorhogli owners:lint—— schema 检查、团队 slug 与handles对照实时 GitHub 组织复用product/gh.py、死rules:glob匹配零文件、同目录product.yaml/owners.yaml冲突、保留位置拒绝见下、以及全树覆盖率每个git ls-files路径要么已解析要么owners: null。它还会打印建议性的合并建议——当它发现一组可以被单个父文件吸收的单一用途 owners.yaml 文件时绝不影响退出码。保留位置owners.yaml不能放在目录自身工具会 glob 该目录下所有 YAML 文件的位置——GitHub Actions/actionlint 把.github/workflows/下的一切都当工作流处理services/mcp的 generate-tools 步骤会 globproducts/*/mcp/下的 YAML 配置。lint 拒绝这些位置的owners.yaml把所有权提升到父级的rules:中如.github/owners.yaml里的/workflows/规则。对应实现是 cli.py 的_reserved_location_error。Lookuphogli owners:who path/owners:team slug/owners:unowned—— 库的薄包装skill 的ownership.js变成 CLI 之上的 shim或被其取代。一个需要承认的权衡迁移前.github/scripts/位于阻塞式CODEOWNERSteam-security之下分配逻辑的改动需要其批准。把 resolver 移出.github/scripts/就脱离了那道闸门——完成resolver 包tools/owners/posthog_owners/现在正位于阻塞文件之下这是对别碰 CODEOWNERS的刻意例外因为自动分配器在pull_request_target上执行它。如果第一个消费者比如自动分配器将来被替换resolver、schema 与 lint 都不受影响——只有那一个调用方改变。这就是事实源而非工具配置属性。5.1 可移植性仓库数据应用resolverresolver 中没有任何 PostHog 特有内容——owners.yaml是仓库无关的格式库只需要 pyyaml 和加载文件的能力。这正是它作为独立可安装的tools/owners包posthog-owners发布的原因任何仓库都可以不 vendoring 直接运行uvx --from githttps://github.com/PostHog/posthog#subdirectorytools/owners owners lint为 CI 固定到某个 commit让 resolver 语义不会在脚下漂移在 URL 后追加sha。包本身只依赖pyyaml6.0与click8.0要求 Python 3.10见 tools/owners/pyproject.toml。对非 Python 消费者python -m posthog_owners以 JSON 回答同样的问题无 click、无项目同步——stdlib 加 pyyaml 就够echo posthog/models/team.py | PYTHONPATH/fetched/tools/owners python3 -m posthog_owners --repo-root /fetchedPYTHONPATH指向持有posthog_owners包的目录--repo-root指向持有所有权文件的树两者可独立。输出示例{ posthog/models/team.py: { owners: [team-x], status: active, slack: #team-x, source: posthog/owners.yaml } }不带--repo-root时 resolver 用git rev-parse定位仓库resolver.py 的_git_repo_root所以需要真实工作树当所有权文件位于稀疏拉取、导出或仅含owners.yaml/product.yaml的草稿目录时传入该标志。加--purpose notifications可把slack解析为各团队的自动化频道而非人频道——WireResolution是hogli owners:resolve --json与python -m posthog_owners共享的同一 JSON 形态resolution_to_wire保证只有一种格式。5.2 关键 seam抽象文件映射解析行走的是抽象文件映射OwnershipSource协议而不是文件系统——owners:fmt的等价性证明已经用真实 resolver 在内存布局上运行。这带来两层意义测试可以用_InMemoryResolver注入任意布局而当一个消费者如 stamphog变成托管应用、其他仓库无需加任何代码即可启用时模型是仓库只贡献所有权数据resolver 随应用一起发布。托管评审者一次 API 调用拉取默认分支树只取所有权文件几十个 blob可按 commit SHA 缓存进程内解析。没有owners.yaml的仓库回退到同一 resolver 接口背后的CODEOWNERSloader——这里的 glob 语义本来就是 CODEOWNERS 语义——两个都没有的仓库就是 unowned。两条属性原样保留所有权永远读默认分支绝不读 PR headPR 无法重写所有权来批准自己所有权_内容_即使启用方式移到 UI 也留在 git 中——远处的所有权存储正是会腐烂的东西§2UI 只配置用哪个源和哪个分支绝不配置数据。6.owners:fmt规范放置的只读预言机合并是可读性选择而非语义选择§4因此树里存在大量解析结果相同的布局并在人们手工折叠/拆分中漂移。owners:fmt为这种漂移命名它把所有权建模为目录树上的分段常数函数——每条match/owners语句是一个_边界_变化点承载它的物理文件纯属表现形式。把语句最优地放置到文件上是一个带容量限制的设施选址问题在边界簇让专用文件比把所有语句上提为祖先规则更便宜的地方打开一个文件否则折入最近的既有载体。成本模型放在模块常量中所以canonical只相对它们成立见 fmt.pyALPHA 8——一个专用简单owners.yaml存在的代价钉住的载体product.yaml清单、非简单文件、glob 文件、仓库根免费因为它们本来就存在GAMMA 1——把一条语句作为祖先规则携带的每层代价MAX_RULES 100——文件可容纳的语句软上限载体过密时强制拆分出专用子文件。glob 规则是横切crosscutting而非树边界所以任何携带 glob 的文件都被钉住、原样通过。每次运行都以内置等价性证明收尾把提议布局在内存中模拟对每个跟踪路径重新解析必须与当前解析完全一致——不一致是fmt的 bug而不是建议。owners:fmt是只读预言机——它从不写入也没有--write标志。分工是owners:lint的折叠/拆分建议是日常增量机制带滞回小簇不会震荡fmt是刻意咨询的漂移预言机真正的文件回流是人类决策不是格式化器该在保存时应用的东西。CLI 输出形如Create (N): posthog/foo/owners.yaml Delete (M) — statements fold into an ancestor: - posthog/bar/owners.yaml cost: current X → canonical Y (constants: ALPHA8, GAMMA1, MAX_RULES100) ✓ canonical layout resolves identically7. 落地一个 PR 完成迁移所有内容原子性落地。下面的交付顺序是评审指南不是合并顺序Resolver schematools/owners/按 §4 解析、hogli owners:resolve --json、owners.yaml的 JSON-schema。把CODEOWNERS-soft转成分布式owners.yaml383 行机械翻译为posthog/、frontend/src/scenes/、nodejs/、rust/、services/、ee/、plugin-server 等目录下的逐目录文件。凡是软文件依赖 last-match 排序的地方如尾部的 managed-reverse-proxy 块覆盖宽泛的 settings-scene 规则该意图显式变成一个嵌套文件或rules:条目——顺序无关性正是要点。翻译由 PR 期间运行的 one-shot converter 驱动转换落地后即被移除保留在分支历史中。等价性证明一个 differ 对每个git ls-files路径分别在旧soft product.yaml与新owners.yaml product.yaml下解析断言评审者集合完全一致。它在 PR 期间运行以验证迁移转换落地后移除有意的分歧确有少数——死 glob、422 回退本就跳过的过时团队在 PR 描述中显式列出而不是悄悄溜过。切换消费者assign-reviewers.js把软文件解析 loadProductYamlRules换成对hogli owners:resolve --json的调用工作流增加 Python 安装步骤。实质性 owner 阈值、5 团队上限、评论/标签行为不变handleowner 变成个体评审请求而不是被跳过。status: generated取代硬编码忽略清单。gates.py把私有 CODEOWNERS-soft 解析器换成直接库导入。ownership.jsskill变成 CLI 之上的 shimSKILL.md 更新。删除.github/CODEOWNERS-soft同时清理所有引用它的条目。Lint 接线hogli owners:lint 一个 CI job扩展现有validate-product-yamlsjob 而非新增。覆盖率先 warn-only待owners:unowned干净或显式owners: null后在后续步骤中改为 fail。原子切换的安全性质auto-assign-reviewers.yml运行在pull_request_target上且总是检出master所以新流程只在合并时激活——没有半迁移窗口进行中未 rebase 的 PR 不受影响仓库的 CI 向后兼容规则构造性地成立硬CODEOWNERS与 GitHub 原生强制不受触碰所以 resolver bug 的最坏情况是_软_标记错误可恢复且非阻塞等价性 differ 是评审产物评审者批准的是已被证明一致的解析 一份显式有意差异清单而不是靠信仰审 400 行 glob 翻译。8. 仓库现状落地后的分布式布局迁移后的当前仓库正是该模型的全尺寸实例。owners.yaml已按目录分布到 29 个位置覆盖从前端到后端的各个子树例如owners.yaml根version: 1、owners: []、根级teams:注册表8 个特殊 slug 映射以及一大批锚定/非锚定的横切规则Dockerfile、/cli/、/docs/onboarding/ai-observability/、/tools/owners/→team-devex、/services/llm-gateway/→team-ai-gateway、/packages/quill/→[platform-ux, sampennington]等posthog/owners.yaml、posthog/models/owners.yaml、posthog/temporal/owners.yaml、posthog/hogql/database/schema/owners.yaml 等——Django 主树的分层文件展示逐层 fallthroughfrontend/src/scenes/owners.yaml、frontend/src/lib/components/owners.yaml —— 前端 scene 级规则其约 60 条规则块正是MAX_RULES的规模参照nodejs/src/owners.yaml 与 nodejs/src/ingestion/pipelines/ai/costs/owners.yaml —— 深度嵌套文件展示文件可以只覆盖单一字段rust/owners.yaml —— 纯规则文件顶层owners: []不贡献 锚定目录规则ee/owners.yaml、services/mcp/src/owners.yaml、.github/owners.yaml保留位置规则的承接者等。这套布局由 tools/owners/tests/test_owners.py813 行测试覆盖 matcher 向量、解析优先级、规则级inherit双向覆盖、teams:注册表、consolidation 建议、保留位置拒绝等持续守护——例如test_resolver_precedence直接断言了文件级inherit: false裁剪与规则级inherit: true恢复祖先这两个互补方向的正确性。9. 留给维护者的开放问题提案结尾保留了几个刻意悬置的问题如实记录Oncall 路由已解决——从 v1 中移除。Slack 频道零成本按团队 slug 约定派生根teams:注册表在需要处覆盖或设slack: false但没有消费者消费 oncall 引用因此不携带。一旦有消费者再加回是纯增量。Resolver 归属已解决——resolver 包位于硬CODEOWNERS覆盖之下见 §5。覆盖率门禁节奏PR 之后多久把owners:lint覆盖率从 warn 翻到 fail——立即对_新_目录生效ratchet还是等整棵树干净再说硬 CODEOWNERS 的未来明确不在当前范围内如果阻塞门禁将来进入 schemaapprover 继承可能应该沿树向上并集而非最近者胜——搁置到team-security想再议时。总结PostHog 的owners.yaml模型把代码所有权从两份手工维护、四个解析器各说各话的平铺文件收敛为一个 schema、一套解析语义、一个 resolver 库。它的设计取舍——按字段 fallthrough 的 nearest-file-wins、文件局部规则、显式owners: null、根级teams:注册表、内置等价性证明的fmt预言机——共同构成了一个对中央文件腐烂问题有完整回应的所有权基础设施。对于任何正在与谁负责这段代码漂移作斗争的 monorepo 团队这份文档、tools/owners 的实现及其测试是一份可以直接借鉴的完整蓝本。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考