feat: ownership oracles (externally computed reviewer requirements) - #178
zbedforrest wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces 'Ownership Oracles' (computed ownership) to Codeowners Plus, allowing external tools to feed JSON-based reviewer requirements into the action. The changes include adding the oracle-files input, parsing and merging oracle rules with standard .codeowners requirements, and introducing the pkg/oracle package with accompanying tests. The review feedback focuses on improving robustness in pkg/oracle/oracle.go by validating glob patterns during parsing to fail-closed, and adding nil checks for warningWriter and overlays in Merge to prevent potential panics.
ddcf8b6 to
5b75238
Compare
5b75238 to
dbb86fe
Compare
…eviewer requirements)
dbb86fe to
980c869
Compare
- Parse rejects leading-slash patterns, which are valid per doublestar but silently never match repo-relative diff paths - Parse rejects whitespace-only owners, not just empty strings - MergeCodeOwners no longer treats optional-only reviewers as conferring ownership, so an optional oracle rule cannot suppress the unowned-file warning (matches .codeowners semantics) - README: guidance on protecting the oracle generator script from PR tampering, and clarified ownership notes
|
Codeowners approval required for this PR: |
BakerNet
left a comment
There was a problem hiding this comment.
I am completely good with Oracles as presented - but I think the important question is:
Do want a model of overriding ownership rules at the start of the process (ownership Oracles as presented) or do we want a plugin system with a set of hooks at different stages of the COP lifecycle?
This would me more flexible to do the kinds of changes @asyncawaitpromise had in mind (e.g. filtering "trivial" changes out of diffs being analyzed) but also eliminates this design decision:
Add-only: oracle rules can add requirements but never remove or weaken .codeowners rules, so a tampered oracle file can at worst request extra reviews.
| // Parse decodes oracle JSON; rules that cannot take effect are errors, not skipped. | ||
| func Parse(data []byte) (*RuleSet, error) { | ||
| var ruleSet RuleSet | ||
| if err := json.Unmarshal(data, &ruleSet); err != nil { |
There was a problem hiding this comment.
An oracle file with a misspelled or unexpected top-level key is accepted as an empty rule set, so all oracle-derived reviewer requirements are silently dropped and the status check passes.
Whether an intentionally empty {"rules": []} should remain valid is a product decision; the suggestion keeps it valid.
| if err := json.Unmarshal(data, &ruleSet); err != nil { | |
| dec := json.NewDecoder(bytes.NewReader(data)) | |
| dec.DisallowUnknownFields() | |
| var raw struct { | |
| Rules *[]Rule `json:"rules"` | |
| } | |
| if err := dec.Decode(&raw); err != nil { | |
| return nil, fmt.Errorf("invalid oracle JSON: %w", err) | |
| } | |
| if dec.More() { | |
| return nil, fmt.Errorf("invalid oracle JSON: unexpected data after top-level object") | |
| } | |
| if raw.Rules == nil { | |
| return nil, fmt.Errorf("invalid oracle JSON: missing required \"rules\" array") | |
| } | |
| ruleSet := RuleSet{Rules: *raw.Rules} |
Reasoning and how to verify
Parse is documented (and the README promises) as fail-closed, but json.Unmarshal silently ignores unknown top-level keys and accepts {} / {"rules": null} as an empty rule set. A generator that emits {"rule": [...]} or {"Rules": [...]} therefore passes validation, applyOracles logs "Oracle files contain no rules" at debug level only, and every oracle requirement is dropped while the check goes green. The "no rules key" test case currently locks in this behaviour.
Recommend decoding with a strict decoder, requiring rules to be present (an explicit [] is still a valid "nothing matched" result), and rejecting trailing JSON values:
(suggestion above)
Then flip the "no rules key" test to expect an error and add cases for an unknown top-level key, an unknown per-rule key, and "rules": null.
How to verify: Call Parse with {"rule": [{"files": ["a.go"], "owners": ["@t"]}]} or {} and observe whether an error is returned. Expected: Parse currently returns a RuleSet with zero rules and nil error for both inputs, and TestParse's "no rules key" case asserts exactly that.
Agent prompt:
PRism finding on pkg/oracle/oracle.go:35 in multimediallc/codeowners-plus#178: An oracle file with a misspelled or unexpected top-level key is accepted as an empty rule set, so all oracle-derived reviewer requirements are silently dropped and the status check passes. Read the review comment marked <!-- prism:finding:pkg/oracle/oracle.go:3:e773d224d1ce --> on that PR, decide whether it is valid, and fix it if so; otherwise explain why not.
| return nil, fmt.Errorf("oracle rule %d has no owners", i) | ||
| } | ||
| for _, owner := range rule.Owners { | ||
| if strings.TrimSpace(owner) == "" { |
There was a problem hiding this comment.
Oracle owners lacking the @ prefix are silently truncated by one character before reaching the GitHub API, and owners with stray whitespace create requirements no approval can ever satisfy.
The exact GitHub slug grammar to enforce is a judgement call; the regex above is conservative and may need loosening for unusual team slugs.
| if strings.TrimSpace(owner) == "" { | |
| for _, owner := range rule.Owners { | |
| if !ownerPattern.MatchString(owner) { | |
| return nil, fmt.Errorf("oracle rule %d has an invalid owner %q (expected @user or @org/team)", i, owner) | |
| } | |
| } |
Reasoning and how to verify
Owner validation only rejects blank strings, so several malformed values pass Parse and are then mangled or misused downstream:
- Missing
@:splitReviewersandInitUserReviewerMapdoreviewer[1:]to strip the@(internal/github/gh.go:409,:626). An owner of"data-platform"becomes the userata-platform, which either 404s at review-request time or silently requests the wrong person, and"o/team"becomes teamteamin orgo... minus its first letter. - Surrounding/embedded whitespace:
" @org/team "survives as a distinct slug that never matches an approving reviewer, so the requirement is permanently unsatisfiable and the check can never pass. - Newlines / control characters: owner names flow verbatim into bot-authored comments via
ToCommentString, so"@team\n..."injects arbitrary markdown into the PR comment.
.codeowners has the same gap, but that file is repo-reviewed source, whereas oracle output is generated at runtime and is the new trust boundary this PR introduces. Since the package already claims strict validation, validate owner syntax here:
(suggestion above)
with something like var ownerPattern = regexp.MustCompile(^@A-Za-z0-9?(?:/[A-Za-z0-9_.-]+)?$) at package level, plus test cases for a bare name, padded whitespace, and an embedded newline.
How to verify: Parse a rule with owners ["data-platform"] and trace the value through RequestReviewers, or parse [" @org/team "] and apply an approval from @org/team. Expected: Parse accepts both values; splitReviewers yields "ata-platform" as an individual reviewer, and the padded slug's normalized form never equals the approver's slug so the group stays unapproved.
Agent prompt:
PRism finding on pkg/oracle/oracle.go:56 in multimediallc/codeowners-plus#178: Oracle owners lacking the @ prefix are silently truncated by one character before reaching the GitHub API, and owners with stray whitespace create requirements no approval can ever satisfy. Read the review comment marked <!-- prism:finding:pkg/oracle/oracle.go:5:3638eb4d419c --> on that PR, decide whether it is valid, and fix it if so; otherwise explain why not.
PRism review: merge confidence 4/5Minor findings worth a look before merge.
2 lower-severity notes
Reviews (1) · reviewed 59df5ff · 11 changed files |
Related PR(s)
#179 builds on this (inline ownership). Supersedes the seam explored in #45.
Summary / Background
Some ownership requirements depend on what changed inside a file, not which file changed (e.g. "telemetry event schema changes need data-platform review"). Path patterns can't express that, so such policies live in advisory bots that comment on PRs but enforce nothing.
This PR adds ownership oracles: an earlier workflow step computes reviewer requirements from the PR's content and hands them to codeowners-plus as a JSON file via the new
oracle-filesinput. The rules are AND-merged into.codeowners-derived ownership through the existingMergeCodeOwnerspath, so review requesting, approval tracking, smart dismissal, and the status check apply unchanged.Design properties:
.codeownersrules, so a tampered oracle file can at worst request extra reviews.Code Changes
pkg/oracle: JSON rule format,Parse/Loadwith strict validation,RuleSet.ToCodeOwnerspkg/codeowners:NewFromFileOwnersconstructor for computed ownershipinternal/app:applyOraclesmerge step;main.go/action.yml:oracle-filesinputsplitOracleFiles