Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions approve.go
Original file line number Diff line number Diff line change
@@ -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
}
135 changes: 75 additions & 60 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down