go.temporal.io/server/tools/ci-notify/slack.go

284 LOC · 91 covered · 193 uncovered · 3 ranges · 2 concepts · 2 introducers · 3 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 package cinotify
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "strings"
9 "time"
10
11 "go.temporal.io/server/tools/common/github"
12 )
13
14 // SlackMessage represents a Slack Block Kit message
15 type SlackMessage struct {
16 Text string `json:"text"`
17 Blocks []SlackBlock `json:"blocks"`
18 }
19
20 // SlackBlock represents a block in the Slack message
21 type SlackBlock struct {
22 Type string `json:"type"`
23 Text *SlackText `json:"text,omitempty"`
24 Fields []SlackText `json:"fields,omitempty"`
25 }
26
27 // SlackText represents text content in a Slack block
28 type SlackText struct {
29 Type string `json:"type"`
30 Text string `json:"text"`
31 }
32
33 // BuildFailureMessage creates a Slack message for CI failure
34 > func BuildFailureMessage(report *FailureReport) *SlackMessage { slack.go ×2
35 > // Header with alert emoji
36 > headerBlock := SlackBlock{
37 > Type: "section",
38 > Text: &SlackText{
39 > Type: "mrkdwn",
40 > Text: ":rotating_light: *CI Failed on Main Branch* :rotating_light:",
41 > },
42 > }
43 >
44 > // Workflow and commit info
45 > commitURL := github.CommitURL("temporalio/temporal", report.Commit.SHA)
46 > infoBlock := SlackBlock{
47 > Type: "section",
48 > Fields: []SlackText{
49 > {
50 > Type: "mrkdwn",
51 > Text: fmt.Sprintf("*Workflow:*\n%s", report.Workflow.Name),
52 > },
53 > {
54 > Type: "mrkdwn",
55 > Text: fmt.Sprintf("*Branch:*\n`%s`", report.Workflow.HeadBranch),
56 > },
57 > {
58 > Type: "mrkdwn",
59 > Text: fmt.Sprintf("*Commit:*\n<%s|%s>", commitURL, report.Commit.ShortSHA),
60 > },
61 > {
62 > Type: "mrkdwn",
63 > Text: fmt.Sprintf("*Author:*\n%s", report.Commit.Author),
64 > },
65 > },
66 > }
67 >
68 > // Failure summary
69 > summaryBlock := SlackBlock{
70 > Type: "section",
71 > Text: &SlackText{
72 > Type: "mrkdwn",
73 > Text: fmt.Sprintf("*Failed Jobs:* %d of %d total jobs",
74 > len(report.FailedJobs), report.TotalJobs),
75 > },
76 > }
77 >
78 > // List of failed jobs
79 > var failedJobNames []string
80 > for _, job := range report.FailedJobs {
81 > failedJobNames = append(failedJobNames,
82 > fmt.Sprintf("• <%s|%s>", job.URL, job.Name))
83 > }
84
85 > jobsBlock := SlackBlock{ slack.go ×2
86 > Type: "section",
87 > Text: &SlackText{
88 > Type: "mrkdwn",
89 > Text: fmt.Sprintf("*Failed Jobs:*\n%s",
90 > strings.Join(failedJobNames, "\n")),
91 > },
92 > }
93 >
94 > // Link to workflow run
95 > linkBlock := SlackBlock{
96 > Type: "section",
97 > Text: &SlackText{
98 > Type: "mrkdwn",
99 > Text: fmt.Sprintf("<%s|View Full Workflow Run>", report.Workflow.URL),
100 > },
101 > }
102 >
103 > return &SlackMessage{
104 > Text: fmt.Sprintf("CI Failed on Main: %s", report.Workflow.Name),
105 > Blocks: []SlackBlock{
106 > headerBlock,
107 > infoBlock,
108 > summaryBlock,
109 > jobsBlock,
110 > linkBlock,
111 > },
112 > }
113 }
114
115 // FormatMessageForDebug formats the message for console output
116 > func FormatMessageForDebug(report *FailureReport) string { slack.go ×1
117 > var sb strings.Builder
118 > fmt.Fprint(&sb, "🚨 CI Failed on Main Branch 🚨\n\n")
119 > fmt.Fprintf(&sb, "Workflow: %s\n", report.Workflow.Name)
120 > fmt.Fprintf(&sb, "Branch: %s\n", report.Workflow.HeadBranch)
121 > fmt.Fprintf(&sb, "Commit: %s (%s)\n", report.Commit.ShortSHA, report.Commit.Author)
122 > fmt.Fprintf(&sb, "Failed Jobs: %d of %d total jobs\n\n", len(report.FailedJobs), report.TotalJobs)
123 > fmt.Fprintln(&sb, "Failed Jobs:")
124 > for _, job := range report.FailedJobs {
125 > fmt.Fprintf(&sb, " • %s\n %s\n", job.Name, job.URL)
126 > }
127 > fmt.Fprintf(&sb, "\nView Full Workflow Run: %s\n", report.Workflow.URL)
128 > return sb.String()
129 }
130
131 // SendSlackMessage sends the message to Slack webhook
132 func SendSlackMessage(webhookURL string, message *SlackMessage) error {
133 payload, err := json.Marshal(message)
134 if err != nil {
135 return fmt.Errorf("failed to marshal message: %w", err)
136 }
137
138 client := &http.Client{
139 Timeout: 30 * time.Second,
140 }
141
142 resp, err := client.Post(webhookURL, "application/json", bytes.NewBuffer(payload))
143 if err != nil {
144 return fmt.Errorf("failed to send message: %w", err)
145 }
146 defer func() { _ = resp.Body.Close() }()
147
148 if resp.StatusCode != http.StatusOK {
149 return fmt.Errorf("slack returned status %d", resp.StatusCode)
150 }
151
152 return nil
153 }
154
155 // BuildSuccessReportMessage creates a Slack message for success report
156 func BuildSuccessReportMessage(report *DigestReport) *SlackMessage {
157 // Header
158 headerBlock := SlackBlock{
159 Type: "section",
160 Text: &SlackText{
161 Type: "mrkdwn",
162 Text: fmt.Sprintf(":chart_with_upwards_trend: *Weekly CI Report - %s Branch*", report.Branch),
163 },
164 }
165
166 // Period
167 periodBlock := SlackBlock{
168 Type: "section",
169 Text: &SlackText{
170 Type: "mrkdwn",
171 Text: fmt.Sprintf("*Report Period:* %s to %s",
172 report.StartDate.Format("Jan 2, 2006"),
173 report.EndDate.Format("Jan 2, 2006")),
174 },
175 }
176
177 // Metrics grid
178 metricsBlock := SlackBlock{
179 Type: "section",
180 Fields: []SlackText{
181 {
182 Type: "mrkdwn",
183 Text: fmt.Sprintf("*Success Rate:*\n%.1f%%", report.SuccessRate),
184 },
185 {
186 Type: "mrkdwn",
187 Text: fmt.Sprintf("*Total Runs:*\n%d", report.TotalRuns),
188 },
189 {
190 Type: "mrkdwn",
191 Text: fmt.Sprintf("*Failed Runs:*\n%d", report.FailedRuns),
192 },
193 {
194 Type: "mrkdwn",
195 Text: fmt.Sprintf("*Successful Runs:*\n%d", report.SuccessfulRuns),
196 },
197 {
198 Type: "mrkdwn",
199 Text: fmt.Sprintf("*Average Duration:*\n%s", formatDuration(report.AverageDuration)),
200 },
201 {
202 Type: "mrkdwn",
203 Text: fmt.Sprintf("*Median Duration:*\n%s", formatDuration(report.MedianDuration)),
204 },
205 },
206 }
207
208 // Timing percentiles section
209 timingBlock := SlackBlock{
210 Type: "section",
211 Text: &SlackText{
212 Type: "mrkdwn",
213 Text: fmt.Sprintf("*Run Duration Distribution:*\n"+
214 "• Under 20 minutes: %.1f%%\n"+
215 "• Under 25 minutes: %.1f%%\n"+
216 "• Under 30 minutes: %.1f%%",
217 report.Under20MinutesPercent,
218 report.Under25MinutesPercent,
219 report.Under30MinutesPercent),
220 },
221 }
222
223 blocks := []SlackBlock{headerBlock, periodBlock, metricsBlock, timingBlock}
224 slowestRuns := report.slowestRuns(3)
225 if len(slowestRuns) > 0 {
226 var slowest []string
227 for _, run := range slowestRuns {
228 slowest = append(slowest, fmt.Sprintf("• <%s|%s> — %s (%s)",
229 run.URL,
230 run.ShortSHA(),
231 formatDuration(run.Duration),
232 run.Conclusion,
233 ))
234 }
235 blocks = append(blocks, SlackBlock{
236 Type: "section",
237 Text: &SlackText{
238 Type: "mrkdwn",
239 Text: fmt.Sprintf("*Slowest Runs:*\n%s", strings.Join(slowest, "\n")),
240 },
241 })
242 }
243
244 return &SlackMessage{
245 Text: fmt.Sprintf("Weekly CI Report - %s Branch", report.Branch),
246 Blocks: blocks,
247 }
248 }
249
250 // FormatReportForDebug formats the success report for console output
251 func FormatReportForDebug(report *DigestReport) string {
252 var sb strings.Builder
253 fmt.Fprintf(&sb, "📊 Weekly CI Report - %s Branch\n\n", report.Branch)
254 fmt.Fprintf(&sb, "Report Period: %s to %s\n\n",
255 report.StartDate.Format("Jan 2, 2006"),
256 report.EndDate.Format("Jan 2, 2006"))
257 fmt.Fprintln(&sb, "Metrics:")
258 fmt.Fprintf(&sb, " Success Rate: %.1f%%\n", report.SuccessRate)
259 fmt.Fprintf(&sb, " Total Runs: %d\n", report.TotalRuns)
260 fmt.Fprintf(&sb, " Successful Runs: %d\n", report.SuccessfulRuns)
261 fmt.Fprintf(&sb, " Failed Runs: %d\n", report.FailedRuns)
262 fmt.Fprintf(&sb, " Average Duration: %s\n", formatDuration(report.AverageDuration))
263 fmt.Fprintf(&sb, " Median Duration: %s\n", formatDuration(report.MedianDuration))
264
265 fmt.Fprintln(&sb, "\nRun Duration Distribution:")
266 fmt.Fprintf(&sb, " Under 20 minutes: %.1f%%\n", report.Under20MinutesPercent)
267 fmt.Fprintf(&sb, " Under 25 minutes: %.1f%%\n", report.Under25MinutesPercent)
268 fmt.Fprintf(&sb, " Under 30 minutes: %.1f%%\n", report.Under30MinutesPercent)
269
270 slowestRuns := report.slowestRuns(3)
271 if len(slowestRuns) > 0 {
272 fmt.Fprintln(&sb, "\nSlowest Runs:")
273 for _, run := range slowestRuns {
274 fmt.Fprintf(&sb, " %s (%s): %s\n %s\n",
275 formatDuration(run.Duration),
276 run.Conclusion,
277 run.ShortSHA(),
278 run.URL,
279 )
280 }
281 }
282
283 return sb.String()
284 }