diff --git a/github.go b/github.go index 5aac178..8d613ba 100644 --- a/github.go +++ b/github.go @@ -33,6 +33,8 @@ func sendRepositoryDispatch(token string, msg *ScenarioProgressMessage) error { "overall_status": msg.OverallStatus, "failed_count": msg.FailedCount, "total_scenarios": msg.TotalScenarios, + "missing_tests_in_pr": msg.MissingTestsInPR, + "should_run_tests": msg.ShouldRunTests, }, } @@ -64,8 +66,8 @@ func sendRepositoryDispatch(token string, msg *ScenarioProgressMessage) error { return fmt.Errorf("github API returned %d: %s", resp.StatusCode, string(respBody)) } - log.Printf("repository_dispatch sent: repo=%s sha=%s run_id=%s overall_status=%s", - msg.Repository, msg.CommitSHA, msg.RunID, msg.OverallStatus) + log.Printf("repository_dispatch sent: repo=%s sha=%s run_id=%s overall_status=%s missing_tests_in_pr=%v should_run_tests=%v", + msg.Repository, msg.CommitSHA, msg.RunID, msg.OverallStatus, msg.MissingTestsInPR, msg.ShouldRunTests) return nil } \ No newline at end of file diff --git a/go.mod b/go.mod index f292d01..e49420f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/alphauslabs/oops go 1.25.0 require ( + cloud.google.com/go/secretmanager v1.16.0 github.com/aws/aws-sdk-go v1.55.8 github.com/dchest/uniuri v1.2.0 github.com/flowerinthenight/longsub v1.6.0 @@ -20,7 +21,6 @@ require ( cloud.google.com/go/compute/metadata v0.8.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/pubsub v1.49.0 // indirect - cloud.google.com/go/secretmanager v1.16.0 // indirect github.com/NYTimes/gizmo v1.3.6 // indirect github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect github.com/ajg/form v1.7.1 // indirect diff --git a/go.sum b/go.sum index 80297b7..4d74bc7 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg= cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -24,8 +22,6 @@ cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbf cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -189,8 +185,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -559,8 +553,6 @@ google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0= -google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4= google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -597,12 +589,8 @@ google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -617,8 +605,6 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -630,8 +616,6 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/DataDog/dd-trace-go.v1 v1.22.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= diff --git a/main.go b/main.go index d6e5886..8d9eb53 100644 --- a/main.go +++ b/main.go @@ -80,17 +80,20 @@ type cmd struct { } type ScenarioProgressMessage struct { - Status string `json:"status"` - Scenario string `json:"scenario"` - RunID string `json:"run_id"` - Data string `json:"data"` - TotalScenarios string `json:"total_scenarios"` - Code string `json:"code"` - OverallStatus string `json:"overall_status,omitempty"` - FailedCount int64 `json:"failed_count,omitempty"` - CommitSHA string `json:"commit_sha,omitempty"` - Repository string `json:"repository,omitempty"` - RunURL string `json:"run_url,omitempty"` + Status string `json:"status"` + Scenario string `json:"scenario"` + RunID string `json:"run_id"` + Data string `json:"data"` + TotalScenarios string `json:"total_scenarios"` + Code string `json:"code"` + OverallStatus string `json:"overall_status,omitempty"` + FailedCount int64 `json:"failed_count,omitempty"` + FailedScenarios []string `json:"failed_scenarios,omitempty"` + CommitSHA string `json:"commit_sha,omitempty"` + Repository string `json:"repository,omitempty"` + RunURL string `json:"run_url,omitempty"` + MissingTestsInPR bool `json:"missing_tests_in_pr,omitempty"` + ShouldRunTests bool `json:"should_run_tests,omitempty"` } func runE(cmd *cobra.Command, args []string) error { @@ -277,6 +280,9 @@ func filterScenariosByAffectedServices(files []string, affectedServices []string func distributePubsub(app *appctx, runID string, tagFilters []string, metadata map[string]interface{}, forceAll bool) bool { id := runID + if metadata == nil { + metadata = make(map[string]interface{}) + } final := combineFilesAndDir() if !forceAll { affectedServices := extractAffectedServices(metadata) @@ -296,11 +302,6 @@ func distributePubsub(app *appctx, runID string, tagFilters []string, metadata m metadata["total_scenarios"] = fmt.Sprintf("%d", len(filtered)) - if len(filtered) > 0 { - app.runSummary[id] = &runSummary{} - app.runTracker[id] = len(filtered) - } - for _, f := range filtered { nc := cmd{ Code: "process", @@ -333,6 +334,9 @@ func distributeSQS(app *appctx, runID string, tagFilters []string, metadata map[ } id := runID + if metadata == nil { + metadata = make(map[string]interface{}) + } final := combineFilesAndDir() if !forceAll { affectedServices := extractAffectedServices(metadata) @@ -351,11 +355,6 @@ func distributeSQS(app *appctx, runID string, tagFilters []string, metadata map[ log.Printf("distributing %d/%d scenarios matching tags %v", len(filtered), len(final), tagFilters) metadata["total_scenarios"] = fmt.Sprintf("%d", len(filtered)) - if len(filtered) > 0 { - app.runSummary[id] = &runSummary{} - app.runTracker[id] = len(filtered) - } - for _, f := range filtered { nc := cmd{ Code: "process", @@ -381,19 +380,11 @@ func distributeSQS(app *appctx, runID string, tagFilters []string, metadata map[ return true } -type runSummary struct { - Success int - Failed int - FailedScenarios []string -} - type appctx struct { - pub *lspubsub.PubsubPublisher // starter publisher topic - rpub *lspubsub.PubsubPublisher // topic to publish reports - mtx *sync.Mutex - topicArn *string - runSummary map[string]*runSummary - runTracker map[string]int + pub *lspubsub.PubsubPublisher // starter publisher topic + rpub *lspubsub.PubsubPublisher // topic to publish reports + mtx *sync.Mutex + topicArn *string } // Our message processing callback. @@ -498,58 +489,6 @@ func process(ctx any, data []byte) error { Verbose: verbose, Metadata: c.Metadata, RunID: c.ID, - OnScenarioDone: func(scenario, status string) { - if c.ID == "" { - return - } - rs, ok := app.runSummary[c.ID] - if !ok { - return - } - if status == "success" { - rs.Success++ - } else { - rs.Failed++ - rs.FailedScenarios = append(rs.FailedScenarios, filepath.Base(scenario)) - } - app.runTracker[c.ID]-- - if app.runTracker[c.ID] <= 0 { - if repslack != "" { - var summaryText strings.Builder - summaryTitle := "Test Run Complete" - summaryColor := "good" - total := rs.Success + rs.Failed - if rs.Failed > 0 { - summaryTitle = "Test Run Complete (With Failures)" - summaryColor = "danger" - } - fmt.Fprintf(&summaryText, "*Run Summary*\nTotal: %d\nPassed: %d\nFailed: %d", total, rs.Success, rs.Failed) - if len(rs.FailedScenarios) > 0 { - summaryText.WriteString("\n\n*Failed scenarios:*") - for _, name := range rs.FailedScenarios { - fmt.Fprintf(&summaryText, "\n• %v", name) - } - } - summaryPayload := SlackMessage{ - Attachments: []SlackAttachment{ - { - Color: summaryColor, - Title: summaryTitle, - Text: summaryText.String(), - Footer: fmt.Sprintf("oops • run: %v", c.ID), - Timestamp: time.Now().Unix(), - MrkdwnIn: []string{"text"}, - }, - }, - } - if err := summaryPayload.Notify(repslack); err != nil { - log.Printf("Notify (slack summary) failed: %v", err) - } - } - delete(app.runSummary, c.ID) - delete(app.runTracker, c.ID) - } - }, }) } @@ -592,12 +531,25 @@ func handleScenarioCompletion(ctx any, data []byte) error { if msg.OverallStatus == "failure" || msg.FailedCount > 0 { color = "danger" - title = "Tests Done." - text = fmt.Sprintf("Test completed with %d/%s success scenarios. Please check the errors <%s|here>.", - successCount, total, msg.RunURL) + title = "Test Run Complete (With Failures)" + var sb strings.Builder + 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 { - text = fmt.Sprintf("Test completed with %s/%s success scenarios.", - total, total) + title = "Test Run Complete" + text = 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{ @@ -606,8 +558,9 @@ func handleScenarioCompletion(ctx any, data []byte) error { Color: color, Title: title, Text: text, - Footer: "oops", + Footer: fmt.Sprintf("oops • run: %v", msg.RunID), Timestamp: time.Now().Unix(), + MrkdwnIn: []string{"text"}, }, }, } @@ -640,9 +593,7 @@ func run(ctx context.Context, done chan error) { } app := &appctx{ - mtx: &sync.Mutex{}, - runSummary: make(map[string]*runSummary), - runTracker: make(map[string]int), + mtx: &sync.Mutex{}, } ctx0, cancelCtx0 := context.WithCancel(ctx) defer cancelCtx0() @@ -721,7 +672,10 @@ func run(ctx context.Context, done chan error) { } } - if scenariopubsub != "" && githubtoken != "" && pubsub != "" { + if scenariopubsub != "" && pubsub != "" { + if githubtoken == "" { + log.Printf("WARNING: githubtoken is empty; scenario progress listener will run, but GitHub repository_dispatch will be skipped") + } log.Printf("starting scenario progress listener on %v", scenariopubsub) _, st, err := lspubsub.GetPublisher(project, scenariopubsub) @@ -742,8 +696,6 @@ func run(ctx context.Context, done chan error) { log.Fatalf("listener for scenario progress failed: %v", err) } }() - } else if scenariopubsub != "" && githubtoken == "" { - log.Printf("WARNING: --scenario-pubsub set but github token is empty; set --secret-project-id or --github-token to enable GitHub status updates") } <-ctx.Done() diff --git a/scenario.go b/scenario.go index b509154..9a42682 100644 --- a/scenario.go +++ b/scenario.go @@ -399,11 +399,22 @@ func doScenario(in *doScenarioInput) error { attr["pubsub"] = pubsub } if in.Metadata != nil { - for _, key := range []string{"pr_number", "branch", "commit_sha", "actor", "trigger_type", "run_url", "repository", "workflow"} { + for _, key := range []string{ + "pr_number", "branch", "commit_sha", "actor", + "trigger_type", "run_url", "repository", "workflow", "total_scenarios", + } { if v, ok := in.Metadata[key].(string); ok && v != "" { attr[key] = v } } + if ta, ok := in.Metadata["test_analysis"].(map[string]interface{}); ok { + for _, key := range []string{"missing_tests_in_pr", "should_run_tests"} { + if v, ok := ta[key].(bool); ok { + attr[key] = fmt.Sprintf("%v", v) + } + } + } + if b, err := json.Marshal(in.Metadata); err == nil { attr["metadata"] = string(b) }