-
Notifications
You must be signed in to change notification settings - Fork 14
/
gchatsink.go
152 lines (132 loc) · 4.22 KB
/
gchatsink.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package cogito
import (
"context"
"fmt"
"io/fs"
"log/slog"
"strings"
"time"
"github.com/Pix4D/cogito/googlechat"
)
// GoogleChatSink is an implementation of [Sinker] for the Cogito resource.
type GoogleChatSink struct {
Log *slog.Logger
InputDir fs.FS
GitRef string
Request PutRequest
}
// Send sends a message to Google Chat if the configuration matches.
func (sink GoogleChatSink) Send() error {
sink.Log.Debug("send: started")
defer sink.Log.Debug("send: finished")
// If present, params.gchat_webhook overrides source.gchat_webhook.
webHook := sink.Request.Source.GChatWebHook
if sink.Request.Params.GChatWebHook != "" {
webHook = sink.Request.Params.GChatWebHook
sink.Log.Debug("params.gchat_webhook is overriding source.gchat_webhook")
}
if webHook == "" {
sink.Log.Info("not sending to chat", "reason", "feature not enabled")
return nil
}
state := sink.Request.Params.State
if !shouldSendToChat(sink.Request) {
sink.Log.Debug("not sending to chat",
"reason", "state not in configured states", "state", state)
return nil
}
text, err := prepareChatMessage(sink.InputDir, sink.Request, sink.GitRef)
if err != nil {
return fmt.Errorf("GoogleChatSink: %s", err)
}
threadKey := fmt.Sprintf("%s %s", sink.Request.Env.BuildPipelineName, sink.GitRef)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
reply, err := googlechat.TextMessage(ctx, webHook, threadKey, text)
if err != nil {
return fmt.Errorf("GoogleChatSink: %s", err)
}
sink.Log.Info("state posted successfully to chat",
"state", state, "space", reply.Space.DisplayName,
"sender", reply.Sender.DisplayName, "text", text)
return nil
}
// shouldSendToChat returns true if the state is configured to do so.
func shouldSendToChat(request PutRequest) bool {
if request.Params.ChatMessage != "" || request.Params.ChatMessageFile != "" {
return true
}
for _, x := range request.Source.ChatNotifyOnStates {
if request.Params.State == x {
return true
}
}
return false
}
// prepareChatMessage returns a message ready to be sent to the chat sink.
func prepareChatMessage(inputDir fs.FS, request PutRequest, gitRef string,
) (string, error) {
params := request.Params
var parts []string
if params.ChatMessage != "" {
parts = append(parts, params.ChatMessage)
}
if params.ChatMessageFile != "" {
contents, err := fs.ReadFile(inputDir, params.ChatMessageFile)
if err != nil {
return "", fmt.Errorf("reading chat_message_file: %s", err)
}
parts = append(parts, string(contents))
}
if len(parts) == 0 || (len(parts) > 0 && params.ChatAppendSummary) {
parts = append(
parts,
gChatBuildSummaryText(gitRef, params.State, request.Source, request.Env))
}
return strings.Join(parts, "\n\n"), nil
}
// gChatBuildSummaryText returns a plain text message to be sent to Google Chat.
func gChatBuildSummaryText(gitRef string, state BuildState, src Source, env Environment,
) string {
now := time.Now().Format("2006-01-02 15:04:05 MST")
// Google Chat format for links with alternate name:
// <https://example.com/foo|my link text>
// GitHub link to commit:
// https://github.com/Pix4D/cogito/commit/e8c6e2ac0318b5f0baa3f55
job := fmt.Sprintf("<%s|%s/%s>",
concourseBuildURL(env), env.BuildJobName, env.BuildName)
// Unfortunately the font is proportional and doesn't support tabs,
// so we cannot align in columns.
var bld strings.Builder
fmt.Fprintf(&bld, "%s\n", now)
fmt.Fprintf(&bld, "*pipeline* %s\n", env.BuildPipelineName)
fmt.Fprintf(&bld, "*job* %s\n", job)
fmt.Fprintf(&bld, "*state* %s\n", decorateState(state))
// An empty gitRef means that cogito has been configured as chat only.
if gitRef != "" {
commitUrl := fmt.Sprintf("https://%s/%s/%s/commit/%s",
src.GhHostname, src.Owner, src.Repo, gitRef)
commit := fmt.Sprintf("<%s|%.10s> (repo: %s/%s)",
commitUrl, gitRef, src.Owner, src.Repo)
fmt.Fprintf(&bld, "*commit* %s\n", commit)
}
return bld.String()
}
func decorateState(state BuildState) string {
var icon string
switch state {
case StateAbort:
icon = "🟤"
case StateError:
icon = "🟠"
case StateFailure:
icon = "🔴"
case StatePending:
icon = "🟡"
case StateSuccess:
icon = "🟢"
default:
icon = "❓"
}
return fmt.Sprintf("%s %s", icon, state)
}