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)
+ }
+
+ })
+ }
+}