@@ -19,6 +19,7 @@ import (
1919 "context"
2020 "encoding/json"
2121 "fmt"
22+ "io"
2223 "net/http"
2324 "os"
2425 "os/signal"
@@ -29,8 +30,22 @@ import (
2930)
3031
3132const (
32- githubContextEnv = "GITHUB_CONTEXT"
33- jobContextEnv = "JOB_CONTEXT"
33+ githubContextEnvKey = "GITHUB_CONTEXT"
34+ jobContextEnvKey = "JOB_CONTEXT"
35+ githubContextRefKey = "ref"
36+ githubContextRepositoryKey = "repository"
37+ githubContextTriggeringActorKey = "triggering_actor"
38+ githubContextEventObjectActionKey = "action"
39+ githubContextEventNameKey = "event_name"
40+ githubContextEventKey = "event"
41+ githubContextEventURLKey = "html_url"
42+ githubEventContenntCreatedAtKey = "created_at"
43+ )
44+
45+ const (
46+ successHeaderIconURL = "https://github.githubassets.com/favicons/favicon.png"
47+ failureHeaderIconURL = "https://github.githubassets.com/favicons/favicon-failure.png"
48+ widgetRefIconURL = "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/quick_reference/default/48px.svg"
3449)
3550
3651var rootCmd = func () cli.Command {
@@ -54,7 +69,7 @@ var rootCmd = func() cli.Command {
5469
5570type WorkflowNotificationCommand struct {
5671 cli.BaseCommand
57- flagWebhookUrl string
72+ flagWebhookURL string
5873}
5974
6075func (c * WorkflowNotificationCommand ) Desc () string {
@@ -77,7 +92,7 @@ func (c *WorkflowNotificationCommand) Flags() *cli.FlagSet {
7792 f .StringVar (& cli.StringVar {
7893 Name : "webhook-url" ,
7994 Example : "https://chat.googleapis.com/v1/spaces/<SPACE_ID>/messages?key=<KEY>&token=<TOKEN>" ,
80- Target : & c .flagWebhookUrl ,
95+ Target : & c .flagWebhookURL ,
8196 Usage : `Webhook URL from google chat` ,
8297 })
8398
@@ -95,30 +110,30 @@ func (c *WorkflowNotificationCommand) Run(ctx context.Context, args []string) er
95110 return fmt .Errorf ("expected 0 arguments, got %q" , args )
96111 }
97112
98- ghJsonStr := c .GetEnv (githubContextEnv )
99- if ghJsonStr == "" {
100- return fmt .Errorf ("environment var %s not set" , githubContextEnv )
113+ ghJSONStr := c .GetEnv (githubContextEnvKey )
114+ if ghJSONStr == "" {
115+ return fmt .Errorf ("environment var %s not set" , githubContextEnvKey )
101116 }
102- jobJsonStr := c .GetEnv (jobContextEnv )
103- if jobJsonStr == "" {
104- return fmt .Errorf ("environment var %s not set" , jobContextEnv )
117+ jobJSONStr := c .GetEnv (jobContextEnvKey )
118+ if jobJSONStr == "" {
119+ return fmt .Errorf ("environment var %s not set" , jobContextEnvKey )
105120 }
106121
107- ghJson := map [string ]any {}
108- jobJson := map [string ]any {}
109- if err := json .Unmarshal ([]byte (ghJsonStr ), & ghJson ); err != nil {
110- return fmt .Errorf ("failed unmarshaling %s: %w" , githubContextEnv , err )
122+ ghJSON := map [string ]any {}
123+ jobJSON := map [string ]any {}
124+ if err := json .Unmarshal ([]byte (ghJSONStr ), & ghJSON ); err != nil {
125+ return fmt .Errorf ("failed unmarshaling %s: %w" , githubContextEnvKey , err )
111126 }
112- if err := json .Unmarshal ([]byte (jobJsonStr ), & jobJson ); err != nil {
113- return fmt .Errorf ("failed unmarshaling %s: %w" , jobContextEnv , err )
127+ if err := json .Unmarshal ([]byte (jobJSONStr ), & jobJSON ); err != nil {
128+ return fmt .Errorf ("failed unmarshaling %s: %w" , jobContextEnvKey , err )
114129 }
115130
116- b , err := generateMessageBody ( ghJson , jobJson , time .Now ())
131+ b , err := generateRequestBody ( generateMessageBodyContent ( ghJSON , jobJSON , time .Now () ))
117132 if err != nil {
118133 return fmt .Errorf ("failed to generate message body: %w" , err )
119134 }
120135
121- url := c .flagWebhookUrl
136+ url := c .flagWebhookURL
122137
123138 request , err := http .NewRequestWithContext (ctx , "POST" , url , bytes .NewBuffer (b ))
124139 if err != nil {
@@ -133,7 +148,12 @@ func (c *WorkflowNotificationCommand) Run(ctx context.Context, args []string) er
133148 defer resp .Body .Close ()
134149
135150 if got , want := resp .StatusCode , http .StatusOK ; got != want {
136- return fmt .Errorf ("unexpected HTTP status code %d (%s)" , got , http .StatusText (got ))
151+ bodyBytes , err := io .ReadAll (resp .Body )
152+ if err != nil {
153+ return fmt .Errorf ("failed to read" )
154+ }
155+ bodyString := string (bodyBytes )
156+ return fmt .Errorf ("unexpected HTTP status code %d (%s)\n got body: %s" , got , http .StatusText (got ), bodyString )
137157 }
138158
139159 return nil
@@ -155,73 +175,139 @@ func realMain(ctx context.Context) error {
155175 return rootCmd ().Run (ctx , os .Args [1 :]) //nolint:wrapcheck // Want passthrough
156176}
157177
158- func generateMessageBody (ghJson , jobJson map [string ]any , timestamp time.Time ) ([]byte , error ) {
159- timezoneLoc , _ := time .LoadLocation ("America/Los_Angeles" )
178+ // messageBodyContent defines the necessary fields for generating the request body.
179+ type messageBodyContent struct {
180+ title string
181+ subtitle string
182+ ref string
183+ triggeringActor string
184+ timestamp string
185+ clickURL string
186+ headerIconURL string
187+ eventName string
188+ repo string
189+ }
160190
161- var iconUrl string
162- switch jobJson ["status" ] {
163- case "success" :
164- iconUrl = "https://github.githubassets.com/favicons/favicon.png"
191+ // generateMessageBodyContent returns messageBodyContent for generating the request body.
192+ // using currentTimestamp as a input is for easier testing on default case.
193+ func generateMessageBodyContent (ghJSON , jobJSON map [string ]any , currentTimeStamp time.Time ) * messageBodyContent {
194+ event , ok := ghJSON [githubContextEventKey ].(map [string ]any )
195+ if ! ok {
196+ event = map [string ]any {}
197+ }
198+ eventName := getMapFieldStringValue (ghJSON , githubContextEventNameKey )
199+ switch eventName {
200+ case "issues" :
201+ issueContent , ok := event ["issue" ].(map [string ]any )
202+ if ! ok {
203+ issueContent = map [string ]any {}
204+ }
205+ return & messageBodyContent {
206+ title : fmt .Sprintf ("A issue is %s" , getMapFieldStringValue (event , githubContextEventObjectActionKey )),
207+ subtitle : fmt .Sprintf ("Issue title: <b>%s</b>" , getMapFieldStringValue (issueContent , "title" )),
208+ ref : getMapFieldStringValue (ghJSON , githubContextRefKey ),
209+ triggeringActor : getMapFieldStringValue (ghJSON , githubContextTriggeringActorKey ),
210+ timestamp : getMapFieldStringValue (issueContent , githubEventContenntCreatedAtKey ),
211+ clickURL : getMapFieldStringValue (issueContent , githubContextEventURLKey ),
212+ eventName : "issue" ,
213+ repo : getMapFieldStringValue (ghJSON , githubContextRepositoryKey ),
214+ headerIconURL : successHeaderIconURL ,
215+ }
216+ case "release" :
217+ releaseContent , ok := event ["release" ].(map [string ]any )
218+ if ! ok {
219+ releaseContent = map [string ]any {}
220+ }
221+ return & messageBodyContent {
222+ title : fmt .Sprintf ("A release is %s" , getMapFieldStringValue (event , githubContextEventObjectActionKey )),
223+ subtitle : fmt .Sprintf ("Release name: <b>%s</b>" , getMapFieldStringValue (releaseContent , "name" )),
224+ ref : getMapFieldStringValue (ghJSON , githubContextRefKey ),
225+ triggeringActor : getMapFieldStringValue (ghJSON , githubContextTriggeringActorKey ),
226+ timestamp : getMapFieldStringValue (releaseContent , githubEventContenntCreatedAtKey ),
227+ clickURL : getMapFieldStringValue (releaseContent , githubContextEventURLKey ),
228+ eventName : "release" ,
229+ repo : getMapFieldStringValue (ghJSON , githubContextRepositoryKey ),
230+ headerIconURL : successHeaderIconURL ,
231+ }
165232 default :
166- iconUrl = "https://github.githubassets.com/favicons/favicon-failure.png"
233+ res := & messageBodyContent {
234+ title : fmt .Sprintf ("GitHub workflow %s" , getMapFieldStringValue (jobJSON , "status" )),
235+ subtitle : fmt .Sprintf ("Workflow: <b>%s</b>" , getMapFieldStringValue (ghJSON , "workflow" )),
236+ ref : getMapFieldStringValue (ghJSON , githubContextRefKey ),
237+ triggeringActor : getMapFieldStringValue (ghJSON , githubContextTriggeringActorKey ),
238+ // The key for getting timestamp is different in differnet triggering event
239+ // a simple work around is using the new timestamp.
240+ timestamp : currentTimeStamp .UTC ().Format (time .RFC3339 ),
241+ clickURL : fmt .Sprintf ("https://github.com/%s/actions/runs/%s" , getMapFieldStringValue (ghJSON , githubContextRepositoryKey ), getMapFieldStringValue (ghJSON , "run_id" )),
242+ eventName : "workflow" ,
243+ repo : getMapFieldStringValue (ghJSON , githubContextRepositoryKey ),
244+ }
245+ v , ok := jobJSON ["status" ]
246+ if ! ok || v == "failure" || v == "canceled" {
247+ res .headerIconURL = failureHeaderIconURL
248+ } else {
249+ res .headerIconURL = successHeaderIconURL
250+ }
251+ return res
167252 }
253+ }
168254
255+ // generateRequestBody returns the body of the request.
256+ func generateRequestBody (m * messageBodyContent ) ([]byte , error ) {
169257 jsonData := map [string ]any {
170258 "cardsV2" : map [string ]any {
171259 "cardId" : "createCardMessage" ,
172260 "card" : map [string ]any {
173261 "header" : map [string ]any {
174- "title" : fmt . Sprintf ( "GitHub workflow %s" , jobJson [ "status" ]) ,
175- "subtitle" : fmt . Sprintf ( "Workflow: <b>%s</b>" , ghJson [ "workflow" ]) ,
176- "imageUrl" : iconUrl ,
262+ "title" : m . title ,
263+ "subtitle" : m . subtitle ,
264+ "imageUrl" : m . headerIconURL ,
177265 },
178266 "sections" : []any {
179267 map [string ]any {
180- // "header": "This is the section header",
181268 "collapsible" : true ,
182269 "uncollapsibleWidgetsCount" : 1 ,
183270 "widgets" : []map [string ]any {
184271 {
185272 "decoratedText" : map [string ]any {
186273 "startIcon" : map [string ]any {
187- "iconUrl" : "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/quick_reference/default/48px.svg" ,
274+ "iconUrl" : widgetRefIconURL ,
188275 },
189- "text" : fmt .Sprintf ("<b>Ref: </b> %s" , ghJson [ "ref" ] ),
276+ "text" : fmt .Sprintf ("<b>Repo: </b> %s" , m . repo ),
190277 },
191278 },
192279 {
193280 "decoratedText" : map [string ]any {
194281 "startIcon" : map [string ]any {
195- "knownIcon " : "PERSON" ,
282+ "iconUrl " : widgetRefIconURL ,
196283 },
197- "text" : fmt .Sprintf ("<b>Run by: </b> %s" , ghJson [ "triggering_actor" ] ),
284+ "text" : fmt .Sprintf ("<b>Ref: </b> %s" , m . ref ),
198285 },
199286 },
200287 {
201288 "decoratedText" : map [string ]any {
202289 "startIcon" : map [string ]any {
203- "knownIcon" : "CLOCK " ,
290+ "knownIcon" : "PERSON " ,
204291 },
205- "text" : fmt .Sprintf ("<b>Pacific: </b> %s" , timestamp . In ( timezoneLoc ). Format ( time . DateTime ) ),
292+ "text" : fmt .Sprintf ("<b>Actor: </b> %s" , m . triggeringActor ),
206293 },
207294 },
208295 {
209296 "decoratedText" : map [string ]any {
210297 "startIcon" : map [string ]any {
211298 "knownIcon" : "CLOCK" ,
212299 },
213- "text" : fmt .Sprintf ("<b>UTC:</b> %s" , timestamp . UTC (). Format ( time . DateTime ) ),
300+ "text" : fmt .Sprintf ("<b>UTC: </b> %s" , m . timestamp ),
214301 },
215302 },
216303 {
217304 "buttonList" : map [string ]any {
218305 "buttons" : []any {
219306 map [string ]any {
220- "text" : "Open" ,
307+ "text" : fmt . Sprintf ( "Open %s" , m . eventName ) ,
221308 "onClick" : map [string ]any {
222309 "openLink" : map [string ]any {
223- "url" : fmt .Sprintf ("https://github.com/%s/actions/runs/%s" ,
224- ghJson ["repository" ], ghJson ["run_id" ]),
310+ "url" : m .clickURL ,
225311 },
226312 },
227313 },
@@ -235,5 +321,23 @@ func generateMessageBody(ghJson, jobJson map[string]any, timestamp time.Time) ([
235321 },
236322 }
237323
238- return json .Marshal (jsonData )
324+ fmt .Println (jsonData )
325+
326+ res , err := json .Marshal (jsonData )
327+ if err != nil {
328+ return nil , fmt .Errorf ("error marshal jsonData: %w" , err )
329+ }
330+ return res , nil
331+ }
332+
333+ // getMapFieldStringValue get value from a map[sting]any map.
334+ // And convert it into string type. Return empty if the conversion failed.
335+ // The keys should all exist as they are popluated by github, to simple the
336+ // code on unnecessary error handling, a empty string is returned.
337+ func getMapFieldStringValue (m map [string ]any , key string ) string {
338+ v , ok := m [key ].(string )
339+ if ! ok {
340+ v = ""
341+ }
342+ return v
239343}
0 commit comments