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

165 LOC · 8 covered · 157 uncovered · 4 ranges · 3 concepts · 3 introducers · 5 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.

1 package cinotify
2
3 import (
4 "context"
5 "fmt"
6 "slices"
7 "time"
8
9 "go.temporal.io/server/tools/common/github"
10 )
11
12 // filterCompleted removes workflow runs that are not completed
13 > func filterCompleted(runs []github.Run) []github.Run { digest.go ×2
14 > var completed []github.Run
15 > for _, run := range runs {
16 > // Only include runs with a conclusion (success or failure) digest.go ×1
17 > if run.Conclusion == github.ConclusionSuccess || run.Conclusion == github.ConclusionFailure {
18 > completed = append(completed, run) digest.go ×1
19 > }
20 }
21 > return completed digest.go ×2
22 }
23
24 // calculateAverage computes the mean duration
25 func calculateAverage(durations []time.Duration) time.Duration {
26 if len(durations) == 0 {
27 return 0
28 }
29
30 var total time.Duration
31 for _, d := range durations {
32 total += d
33 }
34 return total / time.Duration(len(durations))
35 }
36
37 // calculateMedian computes the median duration
38 func calculateMedian(durations []time.Duration) time.Duration {
39 if len(durations) == 0 {
40 return 0
41 }
42
43 // Make a copy to avoid modifying original
44 sorted := make([]time.Duration, len(durations))
45 copy(sorted, durations)
46 slices.Sort(sorted)
47
48 n := len(sorted)
49 if n%2 == 0 {
50 // Even number of elements: average of middle two
51 return (sorted[n/2-1] + sorted[n/2]) / 2
52 }
53 // Odd number of elements: middle element
54 return sorted[n/2]
55 }
56
57 // formatDuration formats a duration in human-readable form
58 func formatDuration(d time.Duration) string {
59 return d.Round(time.Second).String()
60 }
61
62 // calculatePercentUnder calculates the percentage of durations under a threshold
63 func calculatePercentUnder(durations []time.Duration, threshold time.Duration) float64 {
64 if len(durations) == 0 {
65 return 0.0
66 }
67
68 var count int
69 for _, d := range durations {
70 if d < threshold {
71 count++
72 }
73 }
74
75 return (float64(count) / float64(len(durations))) * 100
76 }
77
78 // getWorkflowRuns fetches workflow runs for a branch within a time range.
79 func getWorkflowRuns(branch, workflowName string, since time.Time) ([]github.Run, error) {
80 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
81 defer cancel()
82
83 // Format the date as YYYY-MM-DD for gh CLI
84 sinceDate := since.Format("2006-01-02")
85
86 runs, err := github.ListRuns(ctx, github.RunListOptions{
87 Branch: branch,
88 Workflow: workflowName,
89 Created: ">=" + sinceDate,
90 Limit: 1000,
91 })
92 if err != nil {
93 return nil, fmt.Errorf("failed to get workflow runs: %w", err)
94 }
95
96 // Calculate duration for each run (actual execution time, not including queue time)
97 for i := range runs {
98 if !runs[i].StartedAt.IsZero() && !runs[i].UpdatedAt.IsZero() {
99 runs[i].Duration = runs[i].UpdatedAt.Sub(runs[i].StartedAt)
100 }
101 }
102
103 return runs, nil
104 }
105
106 // BuildDigest builds a digest report for the specified time range
107 func BuildDigest(branch, workflowName string, days int) (*DigestReport, error) {
108 // Calculate the start date
109 endDate := time.Now()
110 startDate := endDate.AddDate(0, 0, -days)
111
112 // Fetch workflow runs
113 runs, err := getWorkflowRuns(branch, workflowName, startDate)
114 if err != nil {
115 return nil, err
116 }
117
118 // Filter to only completed runs
119 completedRuns := filterCompleted(runs)
120
121 // Count successes and failures
122 var successCount, failureCount int
123 var durations []time.Duration
124
125 for _, run := range completedRuns {
126 switch run.Conclusion {
127 case github.ConclusionSuccess:
128 successCount++
129 default:
130 failureCount++
131 }
132
133 if run.Duration > 0 {
134 durations = append(durations, run.Duration)
135 }
136 }
137
138 totalRuns := len(completedRuns)
139 successRate := 0.0
140 if totalRuns > 0 {
141 successRate = (float64(successCount) / float64(totalRuns)) * 100
142 }
143
144 // Calculate duration percentiles
145 under20 := calculatePercentUnder(durations, 20*time.Minute)
146 under25 := calculatePercentUnder(durations, 25*time.Minute)
147 under30 := calculatePercentUnder(durations, 30*time.Minute)
148
149 return &DigestReport{
150 Branch: branch,
151 WorkflowName: workflowName,
152 StartDate: startDate,
153 EndDate: endDate,
154 TotalRuns: totalRuns,
155 SuccessfulRuns: successCount,
156 FailedRuns: failureCount,
157 SuccessRate: successRate,
158 AverageDuration: calculateAverage(durations),
159 MedianDuration: calculateMedian(durations),
160 Under20MinutesPercent: under20,
161 Under25MinutesPercent: under25,
162 Under30MinutesPercent: under30,
163 Runs: completedRuns,
164 }, nil
165 }