diff --git a/approve.go b/approve.go new file mode 100644 index 0000000..19a69e4 --- /dev/null +++ b/approve.go @@ -0,0 +1,130 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" +) + +type commitStatusPayload struct { + State string `json:"state"` + TargetURL string `json:"target_url,omitempty"` + Description string `json:"description"` + Context string `json:"context"` +} + +func sendApprovalStatus(token, commitSHA, repository, prNumber, runURL string, approvalCount int) error { + if token == "" { + return fmt.Errorf("github token is empty, skipping approval status update") + } + + current, err := getCurrentOopstestStatus(token, commitSHA, repository) + if err != nil { + log.Printf("could not fetch current status, skipping: %v", err) + return nil + } + + var state, description string + + if approvalCount >= 2 { + if current == "success" { + log.Printf("ci/oopstest already success, skipping (approvals=%d)", approvalCount) + 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) + } else { + if current != "success" { + log.Printf("ci/oopstest is '%s', no revert needed (approvals=%d)", current, approvalCount) + return nil + } + state = "failure" + description = fmt.Sprintf("Approval override removed — approvals dropped to %d", approvalCount) + log.Printf("approval count dropped (%d), reverting ci/oopstest: repo=%s sha=%s", + approvalCount, repository, commitSHA) + } + + return postCommitStatus(token, commitSHA, repository, runURL, state, description) +} + +func getCurrentOopstestStatus(token, commitSHA, repository 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) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("http request failed: %w", err) + } + defer resp.Body.Close() + + var statuses []struct { + Context string `json:"context"` + State string `json:"state"` + } + + body, _ := io.ReadAll(resp.Body) + if err := json.Unmarshal(body, &statuses); err != nil { + return "", fmt.Errorf("unmarshal statuses: %w", err) + } + + for _, s := range statuses { + if s.Context == "ci/oopstest" { + return s.State, nil + } + } + + return "", nil +} + +func postCommitStatus(token, commitSHA, repository, targetURL, state, description string) error { + payload := commitStatusPayload{ + State: state, + TargetURL: targetURL, + Description: description, + Context: "ci/oopstest", + } + + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("json.Marshal: %w", err) + } + + url := fmt.Sprintf("https://api.github.com/repos/%s/statuses/%s", repository, commitSHA) + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("http.NewRequest: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("http request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + respBody, _ := io.ReadAll(resp.Body) + 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) + return nil +} \ No newline at end of file diff --git a/main.go b/main.go index 0c884a1..7edc568 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,7 @@ type ScenarioProgressMessage struct { MissingTestsInPR bool `json:"missing_tests_in_pr,omitempty"` ShouldRunTests bool `json:"should_run_tests,omitempty"` PRNumber string `json:"pr_number,omitempty"` + ApprovalCount int `json:"approval_count,omitempty"` } func runE(cmd *cobra.Command, args []string) error { @@ -505,80 +506,94 @@ func handleScenarioCompletion(ctx any, data []byte) error { log.Printf("scenario progress: run_id=%s code=%s progress=%s", msg.RunID, msg.Code, msg.TotalScenarios) - if msg.Code != "completed" { - return nil - } + switch msg.Code { + case "approve": + log.Printf("received approve event: repo=%s sha=%s approvals=%d", + msg.Repository, msg.CommitSHA, msg.ApprovalCount) - 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) + if msg.CommitSHA == "" || msg.Repository == "" { + log.Printf("approve: missing commit_sha or repository, skipping") + return nil + } - if err := sendRepositoryDispatch(githubtoken, &msg); err != nil { - log.Printf("sendRepositoryDispatch failed: %v", err) - } + if err := sendApprovalStatus(githubtoken, msg.CommitSHA, msg.Repository, msg.PRNumber, msg.RunURL, msg.ApprovalCount); err != nil { + log.Printf("sendApprovalStatus failed: %v", err) + } - if repslack != "" { - color := "good" - title := "Tests Done." - var text string + 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) - parts := strings.SplitN(msg.TotalScenarios, "/", 2) - total := parts[len(parts)-1] - successCount := int64(0) - if len(parts) == 2 { - var t int64 - fmt.Sscanf(parts[1], "%d", &t) - successCount = t - msg.FailedCount + if err := sendRepositoryDispatch(githubtoken, &msg); err != nil { + log.Printf("sendRepositoryDispatch failed: %v", err) } - env := "dev" - if strings.Contains(pubsub, "prod") { - env = "prod" - } else if strings.Contains(pubsub, "next") { - env = "next" - } + if repslack != "" { + color := "good" + title := "Tests Done." + var text string + + parts := strings.SplitN(msg.TotalScenarios, "/", 2) + total := parts[len(parts)-1] + successCount := int64(0) + if len(parts) == 2 { + var t int64 + fmt.Sscanf(parts[1], "%d", &t) + successCount = t - msg.FailedCount + } - header := fmt.Sprintf("*Environment:* %s\n", env) + env := "dev" + if strings.Contains(pubsub, "prod") { + env = "prod" + } else if strings.Contains(pubsub, "next") { + env = "next" + } - if msg.OverallStatus == "failure" || msg.FailedCount > 0 { - color = "danger" - title = "Test Run Complete (With Failures)" - var sb strings.Builder - sb.WriteString(header) - fmt.Fprintf(&sb, "*Run Summary*\nTotal: %s\nPassed: %d\nFailed: %d", total, successCount, msg.FailedCount) - if len(msg.FailedScenarios) > 0 { - sb.WriteString("\n\n*Failed scenarios:*") - for _, name := range msg.FailedScenarios { - fmt.Fprintf(&sb, "\n• %v", name) + header := fmt.Sprintf("*Environment:* %s\n", env) + + if msg.OverallStatus == "failure" || msg.FailedCount > 0 { + color = "danger" + title = "Test Run Complete (With Failures)" + var sb strings.Builder + sb.WriteString(header) + fmt.Fprintf(&sb, "*Run Summary*\nTotal: %s\nPassed: %d\nFailed: %d", total, successCount, msg.FailedCount) + if len(msg.FailedScenarios) > 0 { + sb.WriteString("\n\n*Failed scenarios:*") + for _, name := range msg.FailedScenarios { + fmt.Fprintf(&sb, "\n• %v", name) + } + } + if msg.RunURL != "" { + fmt.Fprintf(&sb, "\n\n<%s|View run>", msg.RunURL) + } + text = sb.String() + } else { + title = "Test Run Complete" + text = header + fmt.Sprintf("*Run Summary*\nTotal: %s\nPassed: %s\nFailed: 0", total, total) + if msg.RunURL != "" { + text += fmt.Sprintf("\n\n<%s|View run>", msg.RunURL) } } - if msg.RunURL != "" { - fmt.Fprintf(&sb, "\n\n<%s|View run>", msg.RunURL) - } - text = sb.String() - } else { - title = "Test Run Complete" - text = header + fmt.Sprintf("*Run Summary*\nTotal: %s\nPassed: %s\nFailed: 0", total, total) - if msg.RunURL != "" { - text += fmt.Sprintf("\n\n<%s|View run>", msg.RunURL) - } - } - payload := SlackMessage{ - Attachments: []SlackAttachment{ - { - Color: color, - Title: title, - Text: text, - Footer: fmt.Sprintf("oops • runid: %v", msg.RunID), - Timestamp: time.Now().Unix(), - MrkdwnIn: []string{"text"}, + payload := SlackMessage{ + Attachments: []SlackAttachment{ + { + Color: color, + Title: title, + Text: text, + Footer: fmt.Sprintf("oops • runid: %v", msg.RunID), + Timestamp: time.Now().Unix(), + MrkdwnIn: []string{"text"}, + }, }, - }, - } + } - if err := payload.Notify(repslack); err != nil { - log.Printf("Notify (slack) failed: %v", err) + if err := payload.Notify(repslack); err != nil { + log.Printf("Notify (slack) failed: %v", err) + } } + + default: } return nil