Files
claude-botandClaude Opus 5.5 04f1f1d639
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
review-bot reviewed: approve
ci/woodpecker/pr/review-bot-test Pipeline was successful
fix(review-bot): approve with Gitea's APPROVED event
The bot posted approvals with event "APPROVE". Gitea knows APPROVED,
REQUEST_CHANGES and COMMENT and stores any other value as a pending review
that only its author sees (routers/api/v1/repo/pull_review.go,
preparePullReviewType, default branch). Every approval since cdba8b62 is such
a pending draft: homelab #1270, #1278, #1287, #1318, homeassistant #385.

The bot also counted its own pending review as "reviewed at this head", so
those PRs were never reviewed again. A pending review no longer counts; the
sweep reviews them again, and Gitea submits the existing pending review with
the new event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:09:47 +02:00

98 lines
2.8 KiB
Go

package main
import (
"regexp"
"strings"
)
var emptySection = regexp.MustCompile(`(?i)^(none|nothing|n/a|-|—)\.?$`)
// cleanReport drops everything before the first "## " heading and every section that has no
// content or only "None". It returns "" when the answer has no heading at all.
func cleanReport(answer string) string {
var out []string
var heading string
var body []string
started := false
flush := func() {
if heading == "" {
return
}
var content []string
for _, l := range body {
if t := strings.TrimSpace(l); t != "" {
content = append(content, t)
}
}
if len(content) == 0 || (len(content) == 1 && emptySection.MatchString(content[0])) {
heading, body = "", nil
return
}
out = append(out, heading)
out = append(out, body...)
heading, body = "", nil
}
for _, line := range strings.Split(strings.ReplaceAll(answer, "\r\n", "\n"), "\n") {
if strings.HasPrefix(line, "## ") {
flush()
started = true
heading = line
continue
}
if started {
body = append(body, line)
}
}
flush()
return strings.TrimSpace(strings.Join(out, "\n"))
}
func hasHeading(report, prefix string) bool {
for _, line := range strings.Split(report, "\n") {
if strings.HasPrefix(line, prefix) {
return true
}
}
return false
}
var verdictLine = regexp.MustCompile(`(?i)^verdict:\s*(request[-_ ]changes|comment|approve)\s*$`)
// splitVerdict takes the agent's explicit verdict, its last non-empty line "Verdict: <value>",
// off the answer. The verdict is "" when the line is missing, which later reads as a plain comment.
func splitVerdict(answer string) (rest, verdict string) {
lines := strings.Split(strings.TrimRight(strings.ReplaceAll(answer, "\r\n", "\n"), " \t\n"), "\n")
if len(lines) == 0 {
return answer, ""
}
m := verdictLine.FindStringSubmatch(strings.TrimSpace(lines[len(lines)-1]))
if m == nil {
return answer, ""
}
return strings.Join(lines[:len(lines)-1], "\n"), strings.ToLower(strings.NewReplacer("_", "-", " ", "-").Replace(m[1]))
}
// Gitea's review events. Gitea stores any other value as a pending review only its author sees.
const (
eventApproved = "APPROVED"
eventRequestChanges = "REQUEST_CHANGES"
eventComment = "COMMENT"
)
// reviewEvent maps the agent's verdict to the Gitea review event. Changes are requested and
// comments posted as the agent said; an approval is only honoured for a non-empty change whose
// report holds no red or yellow section, and anything else is a comment.
func reviewEvent(verdict, report string, nonEmptyChange bool) string {
switch verdict {
case "request-changes":
return eventRequestChanges
case "approve":
if nonEmptyChange && !hasHeading(report, "## 🔴") && !hasHeading(report, "## 🟡") {
return eventApproved
}
}
return eventComment
}