diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index d003585..9f85d73 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -344,6 +344,18 @@ func main() { } } + // Self-request as reviewer (ensures we appear in required-reviewer checks) + authUser, err := giteaClient.GetAuthenticatedUser(ctx) + if err != nil { + slog.Warn("could not determine authenticated user for reviewer self-request", "error", err) + } else if authUser != "" { + if err := giteaClient.RequestReviewer(ctx, owner, repoName, prNumber, authUser); err != nil { + slog.Warn("could not self-request as reviewer", "user", authUser, "error", err) + } else { + slog.Debug("self-requested as reviewer", "user", authUser, "pr", prNumber) + } + } + // POST new review slog.Info("posting review", "event", event, "pr", prNumber) posted, err := giteaClient.PostReview(ctx, owner, repoName, prNumber, event, reviewBody, inlineComments) diff --git a/gitea/client.go b/gitea/client.go index 1f5f44e..c53822d 100644 --- a/gitea/client.go +++ b/gitea/client.go @@ -460,3 +460,56 @@ func (c *Client) EditComment(ctx context.Context, owner, repo string, commentID } return nil } + +// GetAuthenticatedUser returns the login of the user authenticated by the token. +func (c *Client) GetAuthenticatedUser(ctx context.Context) (string, error) { + reqURL := fmt.Sprintf("%s/api/v1/user", c.baseURL) + body, err := c.doGet(ctx, reqURL) + if err != nil { + return "", fmt.Errorf("get authenticated user: %w", err) + } + var result struct { + Login string `json:"login"` + } + if err := json.Unmarshal(body, &result); err != nil { + return "", fmt.Errorf("parse user response: %w", err) + } + return result.Login, nil +} + +// RequestReviewer adds the given user as a requested reviewer on a pull request. +// This is idempotent — requesting an already-requested reviewer is a no-op. +func (c *Client) RequestReviewer(ctx context.Context, owner, repo string, number int, reviewer string) error { + reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", + c.baseURL, + url.PathEscape(owner), + url.PathEscape(repo), + number) + + payload := struct { + Reviewers []string `json:"reviewers"` + }{Reviewers: []string{reviewer}} + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal reviewer request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("create reviewer request: %w", err) + } + req.Header.Set("Authorization", "token "+c.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.http.Do(req) + if err != nil { + return fmt.Errorf("request reviewer: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("request reviewer failed (status %d): %s", resp.StatusCode, body) + } + return nil +}