diff --git a/go.mod b/go.mod index c415940..35d743c 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/drevell/hackathon go 1.20 -require github.com/abcxyz/pkg v0.3.0 +require ( + github.com/abcxyz/pkg v0.3.0 + github.com/google/go-cmp v0.5.9 +) require ( github.com/kr/text v0.1.0 // indirect diff --git a/go.sum b/go.sum index e4b622f..6e39d3c 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/goccy/go-json v0.10.1 h1:lEs5Ob+oOG/Ze199njvzHbhn6p9T+h64F5hRj69iTTo= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= diff --git a/src/main.go b/src/main.go index 6dd1fc2..1ee2237 100644 --- a/src/main.go +++ b/src/main.go @@ -21,6 +21,8 @@ import ( "fmt" "net/http" "os" + "os/signal" + "syscall" "time" "github.com/abcxyz/pkg/cli" @@ -48,14 +50,32 @@ Usage: {{ COMMAND }} [options] ` } +var rootCmd = func() cli.Command { + return &cli.RootCommand{ + Name: "send-google-chat-webhook", + Commands: map[string]cli.CommandFactory{ + "chat": func() cli.Command { + return &cli.RootCommand{ + Name: "workflownotification", + Description: "notification for workflow", + Commands: map[string]cli.CommandFactory{ + "workflownotification": func() cli.Command { + return &WorkflowNotificationCommand{} + }, + }, + } + }, + }, + } +} + func (c *WorkflowNotificationCommand) Flags() *cli.FlagSet { set := cli.NewFlagSet() f := set.NewSection("Chat space options") f.StringVar(&cli.StringVar{ - Name: "webhook_url", - Aliases: []string{"w"}, + Name: "webhook-url", Example: "https://chat.goog...", Default: "", Target: &c.flagWebhookUrl, @@ -94,70 +114,50 @@ func (c *WorkflowNotificationCommand) Run(ctx context.Context, args []string) er return fmt.Errorf("failed unmarshaling %s: %w", jobContextEnv, err) } - b, err := messageBody(ghJson, jobJson) + b, err := generateMessageBody(ghJson, jobJson, time.Now()) if err != nil { return fmt.Errorf("failed to generate message body: %w", err) } url := c.flagWebhookUrl - fmt.Println("url: ", url) - request, err := http.NewRequest("POST", url, bytes.NewBuffer(b)) + request, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(b)) if err != nil { return fmt.Errorf("creating http request failed: %w", err) } - resp, err := http.DefaultClient.Do(request) + + client := http.Client{} + resp, err := client.Do(request) if err != nil { return fmt.Errorf("sending http request failed: %w", err) } - fmt.Println("resp: ", resp) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected HTTP status code %d (%s)", resp.StatusCode, http.StatusText(resp.StatusCode)) + } + return nil } func main() { - if err := realMain(); err != nil { + ctx, done := signal.NotifyContext(context.Background(), + syscall.SIGINT, syscall.SIGTERM) + defer done() + + if err := realMain(ctx); err != nil { + done() fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } -func realMain() error { - rootCmd := func() cli.Command { - return &cli.RootCommand{ - Name: "send-google-chat-webhook", - Commands: map[string]cli.CommandFactory{ - "chat": func() cli.Command { - return &cli.RootCommand{ - Name: "workflownotification", - Description: "notification for workflow", - Commands: map[string]cli.CommandFactory{ - "workflownotification": func() cli.Command { - return &WorkflowNotificationCommand{} - }, - }, - } - }, - }, - } - } - - cmd := rootCmd() - // Help output is written to stderr by default. Redirect to stdout so the - // "Output" assertion works. - cmd.SetStderr(os.Stdout) - - ctx := context.Background() - err := cmd.Run(ctx, os.Args[1:]) - if err != nil { - return fmt.Errorf("failed to run command: %w", err) - } - - return nil +func realMain(ctx context.Context) error { + return rootCmd().Run(ctx, os.Args[1:]) //nolint:wrapcheck // Want passthrough } -func messageBody(ghJson, jobJson map[string]any) ([]byte, error) { +func generateMessageBody(ghJson, jobJson map[string]any, timestamp time.Time) ([]byte, error) { timezoneLoc, _ := time.LoadLocation("America/Los_Angeles") var iconUrl string @@ -204,7 +204,7 @@ func messageBody(ghJson, jobJson map[string]any) ([]byte, error) { "startIcon": map[string]any{ "knownIcon": "CLOCK", }, - "text": fmt.Sprintf("Pacific: %s", time.Now().In(timezoneLoc).Format(time.DateTime)), + "text": fmt.Sprintf("Pacific: %s", timestamp.In(timezoneLoc).Format(time.DateTime)), }, }, { @@ -212,7 +212,7 @@ func messageBody(ghJson, jobJson map[string]any) ([]byte, error) { "startIcon": map[string]any{ "knownIcon": "CLOCK", }, - "text": fmt.Sprintf("UTC: %s", time.Now().UTC().Format(time.DateTime)), + "text": fmt.Sprintf("UTC: %s", timestamp.UTC().Format(time.DateTime)), }, }, { diff --git a/src/main_test.go b/src/main_test.go new file mode 100644 index 0000000..fc7c8fc --- /dev/null +++ b/src/main_test.go @@ -0,0 +1,207 @@ +package main + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestGenerateMessageBody(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + ghJson map[string]any + jobJson map[string]any + timestamp time.Time + location time.Location + wantMessageBody map[string]any + }{ + { + name: "test_success_workflow", + ghJson: map[string]any{ + "workflow": "test-workflow", + "ref": "test-ref", + "triggering_actor": "test-triggered_actor", + "repository": "test-repository", + "run_id": "test-run-id", + }, + jobJson: map[string]any{ + "status": "success", + }, + timestamp: time.Date(2023, time.April, 25, 17, 44, 57, 0, time.UTC), + wantMessageBody: map[string]any{ + "cardsV2": map[string]any{ + "cardId": "createCardMessage", + "card": map[string]any{ + "header": map[string]any{ + "title": fmt.Sprintf("GitHub workflow %s", "success"), + "subtitle": fmt.Sprintf("Workflow: %s", "test-workflow"), + "imageUrl": "https://github.githubassets.com/favicons/favicon.png", + }, + "sections": []any{ + map[string]any{ + "collapsible": true, + "uncollapsibleWidgetsCount": float64(1), + "widgets": []map[string]any{ + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "iconUrl": "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/quick_reference/default/48px.svg", + }, + "text": fmt.Sprintf("Ref: %s", "test-ref"), + }, + }, + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "knownIcon": "PERSON", + }, + "text": fmt.Sprintf("Run by: %s", "test-triggered_actor"), + }, + }, + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "knownIcon": "CLOCK", + }, + "text": fmt.Sprintf("Pacific: %s", time.Date(2023, time.April, 25, 17, 44, 57, 0, time.UTC).In(time.FixedZone("UTC-8", -7*60*60)).Format(time.DateTime)), + }, + }, + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "knownIcon": "CLOCK", + }, + "text": fmt.Sprintf("UTC: %s", time.Date(2023, time.April, 25, 17, 44, 57, 0, time.UTC).UTC().Format(time.DateTime)), + }, + }, + { + "buttonList": map[string]any{ + "buttons": []any{ + map[string]any{ + "text": "Open", + "onClick": map[string]any{ + "openLink": map[string]any{ + "url": fmt.Sprintf("https://github.com/%s/actions/runs/%s", + "test-repository", "test-run-id"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "test_failed_workflow", + ghJson: map[string]any{ + "workflow": "test-workflow", + "ref": "test-ref", + "triggering_actor": "test-triggered_actor", + "repository": "test-repository", + "run_id": "test-run-id", + }, + jobJson: map[string]any{ + "status": "xxx", + }, + timestamp: time.Date(2023, time.April, 25, 17, 44, 57, 0, time.UTC), + wantMessageBody: map[string]any{ + "cardsV2": map[string]any{ + "cardId": "createCardMessage", + "card": map[string]any{ + "header": map[string]any{ + "title": fmt.Sprintf("GitHub workflow %s", "xxx"), + "subtitle": fmt.Sprintf("Workflow: %s", "test-workflow"), + "imageUrl": "https://github.githubassets.com/favicons/favicon-failure.png", + }, + "sections": []any{ + map[string]any{ + "collapsible": true, + "uncollapsibleWidgetsCount": float64(1), + "widgets": []map[string]any{ + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "iconUrl": "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/quick_reference/default/48px.svg", + }, + "text": fmt.Sprintf("Ref: %s", "test-ref"), + }, + }, + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "knownIcon": "PERSON", + }, + "text": fmt.Sprintf("Run by: %s", "test-triggered_actor"), + }, + }, + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "knownIcon": "CLOCK", + }, + "text": fmt.Sprintf("Pacific: %s", time.Date(2023, time.April, 25, 17, 44, 57, 0, time.UTC).In(time.FixedZone("UTC-8", -7*60*60)).Format(time.DateTime)), + }, + }, + { + "decoratedText": map[string]any{ + "startIcon": map[string]any{ + "knownIcon": "CLOCK", + }, + "text": fmt.Sprintf("UTC: %s", time.Date(2023, time.April, 25, 17, 44, 57, 0, time.UTC).UTC().Format(time.DateTime)), + }, + }, + { + "buttonList": map[string]any{ + "buttons": []any{ + map[string]any{ + "text": "Open", + "onClick": map[string]any{ + "openLink": map[string]any{ + "url": fmt.Sprintf("https://github.com/%s/actions/runs/%s", + "test-repository", "test-run-id"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + gotMessageBody, err := generateMessageBody(tc.ghJson, tc.jobJson, tc.timestamp) + if err != nil { + t.Fatalf("failed to generate messag body %v", err) + } + + wantMessageBodyByte, err := json.Marshal(tc.wantMessageBody) + if err != nil { + t.Fatalf("failed to marshal tc.wantMessageBody: %v", err) + } + + if diff := cmp.Diff(wantMessageBodyByte, gotMessageBody); diff != "" { + t.Errorf("messageBody got unexpected diff (-want, +got):\n%s", diff) + } + + }) + } +}