fix: write CODEOWNERS annotations after glob-based sections - #130
Conversation
dduugg
left a comment
There was a problem hiding this comment.
Thank you for tracking this down — a subtle one, and the write-up made it easy to review.
One ask for the release: every consumer's CODEOWNERS changes on upgrade. Plain bin/codeownership validate autocorrects it, but CI that runs validate --skip-autocorrect will fail until someone regenerates and commits, so it's worth calling out in the release notes.
| } | ||
|
|
||
| #[test] | ||
| fn test_codeowners_file_agrees_with_for_file() -> Result<(), Box<dyn Error>> { |
There was a problem hiding this comment.
This test and test_for_file_from_codeowners_returns_annotated_owner read the committed fixture CODEOWNERS, which is already in the fixed order, so both pass on main too — only test_validate_accepts_generated_order fails there. Could these two regenerate into a temp copy first, so they exercise the generator? Otherwise it'd help to describe them as fixture-consistency checks.
GitHub applies the last matching CODEOWNERS line, but the generator wrote the file-annotation section first. A file that a team glob excludes via unowned_globs and an annotation assigns to another team is valid, and for-file resolves it to the annotated team, yet the broader team glob came later in CODEOWNERS and won on GitHub (and in --from-codeowners lookups). Write the annotations section last. Resolution and validation keep their existing mapper order; only the generated file changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The crosscheck and for-file tests read the committed fixture CODEOWNERS, which is already in the fixed order, so they passed without the generator change. Regenerate into the temp copy first so all three tests fail on the old order.
a04ef70 to
314ba94
Compare
dduugg
left a comment
There was a problem hiding this comment.
Thanks — regenerating into the temp copy closes the gap; all three tests now fail on the old ordering.
A minor bump, because both changes since 0.4.0 are visible to consumers:
- The generated CODEOWNERS now writes the "Annotations at the top of file"
section last (#130), so GitHub's last-match-wins routing agrees with
`for-file`. Every consumer's CODEOWNERS changes on upgrade, so `validate`
reports it out of date until it's regenerated.
- `find_file_owners` caches loaded teams for the life of the process (#129),
so team files added or edited mid-process aren't seen until
`clear_team_cache()` runs or the process restarts.
Summary
validate. A team glob excludes the file throughunowned_globs, and an annotation assigns it to another team.for-filecorrectly reports the annotated team. But in the generated CODEOWNERS, the team glob comes after the annotation line, so GitHub requests the wrong team's review.for-file --from-codeownersand the Ruby gem's CODEOWNERS-based lookups (teams_for_files_from_codeowners, andfor_filewith its defaultfrom_codeowners: true) return the wrong team too, because the CODEOWNERS reader also gives the last match priority.Why only annotations need to move
In a configuration that passes
validate, a file has one owner. The only way a broader pattern can also match it is a team glob that excludes it throughunowned_globs, which CODEOWNERS can't express. Each possible owner of that excluded file:.codeownerdirectorypackage.yml/package.json)Moving annotations to the end fixes the one wrong row. Annotation lines are exact file paths, so writing them last can only affect the file each one names.
Measured
On a large monorepo, I compared the per-file resolver (
runner::file_owner_for_file) against CODEOWNERS-derived owners (runner::teams_for_files_from_codeowners) for every owned file (95,776):maingithub.teamhandle, which CODEOWNERS can't tell apart when you read ownership back from it. Those don't affect GitHub routing, since the handle is the same, so after this change GitHub review routing matchedfor-filefor every file in that repo.validatewarning or by making the reader flag ambiguous handles.I also bucketed every tracked file (136,621) against both orderings, including the unowned cases:
mainproject.filesis filtered byowned_globs/unowned_globs), which is also whenfor-filehonours an annotation. So writing annotations last can't assign an owner to a file the tool treats as unowned.Tests
annotation_in_unowned_team_glob. Team Alpha ownsruby/app/**/*but excludesbeta_owned.rbthroughunowned_globs;beta_owned.rbis# @team Beta. New tests:validateaccepts the committed fixture CODEOWNERSgeneratein a temp copy,crosscheck-ownersreports "Success! All files match between CODEOWNERS and for-file command."generatein a temp copy,for-file --from-codeownersreturns Betamain's generator, all three fail; crosscheck reportsruby/app/services/beta_owned.rb: CODEOWNERS=Alpha fast=Beta.main's output, and updated four inline expectations. Each change only moves the annotations block to the end.generate, thenvalidateandcrosscheck-owners, on every upstream fixture withmain's binary and with this branch gives identical exit codes and mismatch counts. The only difference is the new fixture, which goes from 1 mismatch to 0.cargo clippy --all-targets --all-features -- -D warnings,cargo test(166 passed, 0 failed, rebased on currentmain), andcargo fmt -- --check, all on the pinned 1.97.1 toolchain.Things to be aware of
validateautocorrects it, but CI that runsvalidate --skip-autocorrectfails until someone regenerates and commits the file. The diff is a pure section move.for-teamreport order. Thefor-teamreport lists sections in CODEOWNERS order, so its "Annotations at the top of file" section now appears last (seetest_for_team).valid_project_with_overridestests still fail atvalidate, as they do onmain. It does make the generated file agree with the priority order Ownership overrides #69 describes, for the overlaps that are valid today.🤖 Generated with Claude Code