diff --git a/README.md b/README.md index ee43bd4..3822a3e 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,14 @@ On the next run, it finds and deletes any review containing its own sentinel (ex If `reviewer-name` is empty, cleanup is skipped (reviews stack like before). +### Shared Token: Worst-Wins Behavior + +When multiple review types share the same Gitea bot account (e.g. code-quality and security), Gitea determines the user's approval state from their **most recent review**. This creates a race condition: if security finds issues (REQUEST_CHANGES) but code-quality finishes last (APPROVE), the PR appears approved. + +review-bot handles this automatically with **worst-wins reconciliation**: before posting, each job checks whether any sibling review from the same user already has REQUEST_CHANGES. If so and this job would post APPROVE, it posts as REQUEST_CHANGES instead — maintaining the block. This ensures the PR stays blocked until all checks pass, regardless of execution order. + +**If you need independent approval/block per review type**, use separate Gitea bot accounts with their own tokens. + ## Custom Review Prompts Use `system-prompt-file` to specialize the review focus. The file contents are appended to the base system prompt as "Additional Review Instructions." diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index 8684cac..109436f 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -226,6 +226,26 @@ func main() { return } + // Worst-wins: if we're about to APPROVE but a sibling review from the same + // user already has REQUEST_CHANGES, post as REQUEST_CHANGES too so we don't + // override the blocking state. + if event == "APPROVED" && *reviewerName != "" { + existing, err := giteaClient.ListReviews(ctx, owner, repoName, prNumber) + if err == nil { + for _, r := range existing { + if !r.Stale && r.State == "REQUEST_CHANGES" { + // Check it's from the same user (same token) but a different role + sentinelCheck := fmt.Sprintf("", *reviewerName) + if !strings.Contains(r.Body, sentinelCheck) { + log.Printf("Sibling review %d has REQUEST_CHANGES; escalating to REQUEST_CHANGES", r.ID) + event = "REQUEST_CHANGES" + break + } + } + } + } + } + log.Printf("Posting review (event=%s)...", event) posted, err := giteaClient.PostReview(ctx, owner, repoName, prNumber, event, reviewBody) if err != nil { @@ -249,6 +269,7 @@ func main() { } } } + } } }