From 5404e2de989d786afef8e0374c74f40865a8b1fd Mon Sep 17 00:00:00 2001 From: Ynah537 Date: Wed, 18 Mar 2026 15:31:30 +0800 Subject: [PATCH] feat: track PR reviewers and notify on approval events --- approve.go | 41 ++++++++++++++++++++++++++--------------- main.go | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/approve.go b/approve.go index 19a69e4..9f82344 100644 --- a/approve.go +++ b/approve.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "strings" ) type commitStatusPayload struct { @@ -16,12 +17,12 @@ type commitStatusPayload struct { Context string `json:"context"` } -func sendApprovalStatus(token, commitSHA, repository, prNumber, runURL string, approvalCount int) error { +func sendApprovalStatus(token, commitSHA, repository, prNumber, runURL string, approvalCount int, reviewers string) error { if token == "" { return fmt.Errorf("github token is empty, skipping approval status update") } - current, err := getCurrentOopstestStatus(token, commitSHA, repository) + current, currentDesc, err := getCurrentOopstestStatus(token, commitSHA, repository) if err != nil { log.Printf("could not fetch current status, skipping: %v", err) return nil @@ -35,16 +36,24 @@ func sendApprovalStatus(token, commitSHA, repository, prNumber, runURL string, a return nil } state = "success" - description = fmt.Sprintf("Overridden by approval — approved by %d reviewers", approvalCount) - log.Printf("approval threshold met (%d), setting ci/oopstest to success: repo=%s sha=%s", - approvalCount, repository, commitSHA) + desc := fmt.Sprintf("Overridden by approval — reviewed by: %s (%d reviewers)", reviewers, approvalCount) + if len(desc) > 140 { + desc = fmt.Sprintf("Overridden by approval — %d reviewers approved", approvalCount) + } + description = desc + log.Printf("approval threshold met (%d), setting ci/oopstest to success: repo=%s sha=%s reviewers=%s", + approvalCount, repository, commitSHA, reviewers) } else { if current != "success" { log.Printf("ci/oopstest is '%s', no revert needed (approvals=%d)", current, approvalCount) return nil } + if !strings.Contains(currentDesc, "Overridden by approval") { + log.Printf("ci/oopstest success was from real test run, not reverting (desc=%q)", currentDesc) + return nil + } state = "failure" - description = fmt.Sprintf("Approval override removed — approvals dropped to %d", approvalCount) + description = fmt.Sprintf("Approval override removed — reviewer count dropped to %d", approvalCount) log.Printf("approval count dropped (%d), reverting ci/oopstest: repo=%s sha=%s", approvalCount, repository, commitSHA) } @@ -52,12 +61,12 @@ func sendApprovalStatus(token, commitSHA, repository, prNumber, runURL string, a return postCommitStatus(token, commitSHA, repository, runURL, state, description) } -func getCurrentOopstestStatus(token, commitSHA, repository string) (string, error) { +func getCurrentOopstestStatus(token, commitSHA, repository string) (string, string, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/commits/%s/statuses", repository, commitSHA) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - return "", fmt.Errorf("http.NewRequest: %w", err) + return "", "", fmt.Errorf("http.NewRequest: %w", err) } req.Header.Set("Authorization", "Bearer "+token) @@ -66,27 +75,28 @@ func getCurrentOopstestStatus(token, commitSHA, repository string) (string, erro resp, err := http.DefaultClient.Do(req) if err != nil { - return "", fmt.Errorf("http request failed: %w", err) + return "", "", fmt.Errorf("http request failed: %w", err) } defer resp.Body.Close() var statuses []struct { - Context string `json:"context"` - State string `json:"state"` + Context string `json:"context"` + State string `json:"state"` + Description string `json:"description"` } body, _ := io.ReadAll(resp.Body) if err := json.Unmarshal(body, &statuses); err != nil { - return "", fmt.Errorf("unmarshal statuses: %w", err) + return "", "", fmt.Errorf("unmarshal statuses: %w", err) } for _, s := range statuses { if s.Context == "ci/oopstest" { - return s.State, nil + return s.State, s.Description, nil } } - return "", nil + return "", "", nil } func postCommitStatus(token, commitSHA, repository, targetURL, state, description string) error { @@ -125,6 +135,7 @@ func postCommitStatus(token, commitSHA, repository, targetURL, state, descriptio return fmt.Errorf("github API returned %d: %s", resp.StatusCode, string(respBody)) } - log.Printf("ci/oopstest updated: state=%s repo=%s sha=%s target_url=%s", state, repository, commitSHA, targetURL) + log.Printf("ci/oopstest updated: state=%s repo=%s sha=%s target_url=%s", + state, repository, commitSHA, targetURL) return nil } \ No newline at end of file diff --git a/main.go b/main.go index 7edc568..cfffede 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,7 @@ type ScenarioProgressMessage struct { ShouldRunTests bool `json:"should_run_tests,omitempty"` PRNumber string `json:"pr_number,omitempty"` ApprovalCount int `json:"approval_count,omitempty"` + Reviewers string `json:"reviewers,omitempty"` } func runE(cmd *cobra.Command, args []string) error { @@ -508,18 +509,55 @@ func handleScenarioCompletion(ctx any, data []byte) error { switch msg.Code { case "approve": - log.Printf("received approve event: repo=%s sha=%s approvals=%d", - msg.Repository, msg.CommitSHA, msg.ApprovalCount) + log.Printf("received approve event: repo=%s sha=%s approvals=%d reviewers=%s", + msg.Repository, msg.CommitSHA, msg.ApprovalCount, msg.Reviewers) - if msg.CommitSHA == "" || msg.Repository == "" { - log.Printf("approve: missing commit_sha or repository, skipping") - return nil - } + if msg.CommitSHA == "" || msg.Repository == "" { + log.Printf("approve: missing commit_sha or repository, skipping") + return nil + } - if err := sendApprovalStatus(githubtoken, msg.CommitSHA, msg.Repository, msg.PRNumber, msg.RunURL, msg.ApprovalCount); err != nil { + if err := sendApprovalStatus(githubtoken, msg.CommitSHA, msg.Repository, msg.PRNumber, msg.RunURL, msg.ApprovalCount, msg.Reviewers); err != nil { log.Printf("sendApprovalStatus failed: %v", err) } + if repslack != "" { + reviewerMentions := "" + if msg.Reviewers != "" { + var mentions []string + for _, r := range strings.Split(msg.Reviewers, ",") { + mentions = append(mentions, "@"+strings.TrimSpace(r)) + } + reviewerMentions = strings.Join(mentions, " ") + } + + color := "good" + title := "PR Approved" + text := fmt.Sprintf("*Repository:* %s\n*PR:* #%s\n*Reviewers:* %s\n*Approval Count:* %d", + msg.Repository, msg.PRNumber, reviewerMentions, msg.ApprovalCount) + + if msg.RunURL != "" { + text += fmt.Sprintf("\n\n<%s|View run>", msg.RunURL) + } + + payload := SlackMessage{ + Attachments: []SlackAttachment{ + { + Color: color, + Title: title, + Text: text, + Footer: "oops • approval", + Timestamp: time.Now().Unix(), + MrkdwnIn: []string{"text"}, + }, + }, + } + + if err := payload.Notify(repslack); err != nil { + log.Printf("Notify (slack) failed: %v", err) + } + } + case "completed": log.Printf("run completed: run_id=%s overall_status=%s failed=%d repo=%s sha=%s", msg.RunID, msg.OverallStatus, msg.FailedCount, msg.Repository, msg.CommitSHA)