feat: worst-wins reconciliation for shared-token review types
CI / test (pull_request) Successful in 13s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 24s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 57s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m38s
CI / test (pull_request) Successful in 13s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 24s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 57s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m38s
When multiple review types share a Gitea bot account, Gitea uses the latest review to determine the user's approval state. This creates a race: if security finds issues but code-quality finishes last with APPROVE, the PR appears approved. Now after posting, each job checks if any sibling review from the same user has REQUEST_CHANGES. If so and we posted APPROVE, we delete our review and re-post as REQUEST_CHANGES. This ensures the PR stays blocked until ALL checks pass, regardless of execution order. Documented the behavior in README under "Shared Token: Worst-Wins."
This commit is contained in:
@@ -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).
|
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**: after posting, each job checks whether any sibling review from the same user has REQUEST_CHANGES. If so and this job posted APPROVE, it deletes its review and re-posts as REQUEST_CHANGES. 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
|
## 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."
|
Use `system-prompt-file` to specialize the review focus. The file contents are appended to the base system prompt as "Additional Review Instructions."
|
||||||
|
|||||||
@@ -249,6 +249,28 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Worst-wins: if any sibling review from the same user has REQUEST_CHANGES
|
||||||
|
// and we posted APPROVE, re-post as REQUEST_CHANGES to ensure Gitea's
|
||||||
|
// "last review wins" behavior reflects the most restrictive state.
|
||||||
|
if event == "APPROVED" {
|
||||||
|
for _, r := range reviews {
|
||||||
|
if r.ID != posted.ID && r.User.Login == posted.User.Login && !r.Stale && r.State == "REQUEST_CHANGES" {
|
||||||
|
log.Printf("Sibling review %d has REQUEST_CHANGES; escalating our review", r.ID)
|
||||||
|
if err := giteaClient.DeleteReview(ctx, owner, repoName, prNumber, posted.ID); err != nil {
|
||||||
|
log.Printf("Warning: could not delete review for escalation: %v", err)
|
||||||
|
} else {
|
||||||
|
escalated, err := giteaClient.PostReview(ctx, owner, repoName, prNumber, "REQUEST_CHANGES", reviewBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: could not re-post escalated review: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Review escalated to REQUEST_CHANGES (new id=%d)", escalated.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user