Skip to content

feat: ownership oracles (externally computed reviewer requirements) - #178

Open
zbedforrest wants to merge 9 commits into
mainfrom
feature/ownership-oracles
Open

zbedforrest wants to merge 9 commits into
mainfrom
feature/ownership-oracles

Conversation

@zbedforrest

@zbedforrest zbedforrest commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

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-files input. The rules are AND-merged into .codeowners-derived ownership through the existing MergeCodeOwners path, so review requesting, approval tracking, smart dismissal, and the status check apply unchanged.

Design properties:

  • 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.
  • Fail-closed: a missing or malformed oracle file (including an invalid glob pattern) fails the check rather than silently dropping reviews.
  • A file matched by an oracle rule counts as owned for unowned-file reporting.

Code Changes

  • pkg/oracle: JSON rule format, Parse/Load with strict validation, RuleSet.ToCodeOwners
  • pkg/codeowners: NewFromFileOwners constructor for computed ownership
  • internal/app: applyOracles merge step; main.go/action.yml: oracle-files input
  • README: "Ownership Oracles" section; coverage badge regenerated
  • Tests: oracle unit tests, app-level merge/error tests (multi-file, unowned interaction, case-insensitive approvals), splitOracleFiles

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/oracle/oracle.go Outdated
Comment thread pkg/oracle/oracle.go
Comment thread pkg/oracle/oracle.go Outdated
@zbedforrest
zbedforrest force-pushed the feature/ownership-oracles branch from ddcf8b6 to 5b75238 Compare July 22, 2026 19:26
@zbedforrest zbedforrest changed the title feature: ownership oracles — computed ownership from external tooling feature: ownership oracles (computed ownership from external tooling) Jul 22, 2026
@zbedforrest
zbedforrest force-pushed the feature/ownership-oracles branch from 5b75238 to dbb86fe Compare July 22, 2026 20:00
@zbedforrest zbedforrest changed the title feature: ownership oracles (computed ownership from external tooling) feat: ownership oracles (externally computed reviewer requirements) Jul 22, 2026
@zbedforrest
zbedforrest force-pushed the feature/ownership-oracles branch from dbb86fe to 980c869 Compare July 22, 2026 20:15
zbedforrest and others added 8 commits July 22, 2026 13:27
- 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
@zbedforrest
zbedforrest marked this pull request as ready for review August 10, 2026 21:21
@github-actions
github-actions Bot requested a review from BakerNet August 10, 2026 21:22
@github-actions

Copy link
Copy Markdown

Codeowners approval required for this PR:

@BakerNet BakerNet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/oracle/oracle.go
// 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM Behavior change

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.

Suggested change
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.

Fix with agent

Comment thread pkg/oracle/oracle.go
return nil, fmt.Errorf("oracle rule %d has no owners", i)
}
for _, owner := range rule.Owners {
if strings.TrimSpace(owner) == "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM Behavior change

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.

Suggested change
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 @: splitReviewers and InitUserReviewerMap do reviewer[1:] to strip the @ (internal/github/gh.go:409, :626). An owner of "data-platform" becomes the user ata-platform, which either 404s at review-request time or silently requests the wrong person, and "o/team" becomes team team in org o... 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.

Fix with agent

@prism-pr-review-server

Copy link
Copy Markdown

PRism review: merge confidence 4/5

Minor findings worth a look before merge.

  • MEDIUM 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 — oracle.go:35
  • MEDIUM 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 — oracle.go:56
2 lower-severity notes
  • LOW No runtime impact; the README sentence may lead adopters to overestimate the tamper resistance of oracle-enforced policies — README.md:419
  • LOW Repos using require_both_branch_reviewers with optional-only ownership on some files will start seeing those files reported as unowned after this change — merger.go:43

Full report

Reviews (1) · reviewed 59df5ff · 11 changed files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants