-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathtracker.go
More file actions
215 lines (193 loc) · 5.1 KB
/
tracker.go
File metadata and controls
215 lines (193 loc) · 5.1 KB
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package watch
import (
"fmt"
"sort"
"strings"
buildkite "github.com/buildkite/go-buildkite/v4"
)
// trackedJob holds a job and its lifecycle state across polls.
type trackedJob struct {
Job buildkite.Job
PrevState string // state from previous poll, "" if first seen
Reported bool // true once surfaced to caller as failed
RetryReported bool // true once surfaced to caller as retry-passed
}
// JobSummary aggregates job counts by high-level state.
type JobSummary struct {
Passed int `json:"passed"`
Failed int `json:"failed"`
SoftFailed int `json:"soft_failed"`
Running int `json:"running"`
Scheduled int `json:"scheduled"`
Blocked int `json:"blocked"`
Skipped int `json:"skipped"`
Waiting int `json:"waiting"`
}
// String returns a human-readable summary of non-zero job counts.
func (s JobSummary) String() string {
type entry struct {
count int
label string
}
entries := []entry{
{s.Passed, "passed"},
{s.Failed, "failed"},
{s.SoftFailed, "soft failed"},
{s.Running, "running"},
{s.Scheduled, "scheduled"},
{s.Blocked, "blocked"},
{s.Skipped, "skipped"},
{s.Waiting, "waiting"},
}
var parts []string
for _, e := range entries {
if e.count > 0 {
parts = append(parts, fmt.Sprintf("%d %s", e.count, e.label))
}
}
return strings.Join(parts, ", ")
}
// BuildStatus is the output of JobTracker.Update().
type BuildStatus struct {
NewlyFailed []buildkite.Job
NewlyRetryPassed []buildkite.Job
Running []buildkite.Job
TotalRunning int
Summary JobSummary
Build buildkite.Build
}
// JobTracker tracks job state changes across polls.
type JobTracker struct {
jobs map[string]*trackedJob
}
// NewJobTracker creates a new JobTracker.
func NewJobTracker() *JobTracker {
return &JobTracker{
jobs: make(map[string]*trackedJob),
}
}
// Update processes a build and returns the current status with any state changes.
func (t *JobTracker) Update(b buildkite.Build) BuildStatus {
var status BuildStatus
status.Build = b
var running []buildkite.Job
for _, j := range b.Jobs {
if j.Type != "script" || j.State == "broken" {
continue
}
job := NewFormattedJob(j)
tj, exists := t.jobs[j.ID]
if !exists {
tj = &trackedJob{}
t.jobs[j.ID] = tj
} else {
tj.PrevState = tj.Job.State
}
tj.Job = j
prevJob := NewFormattedJob(buildkite.Job{State: tj.PrevState})
if job.IsFailed() && !prevJob.IsTerminalFailureState() && !tj.Reported {
status.NewlyFailed = append(status.NewlyFailed, j)
tj.Reported = true
}
if isActiveState(j.State) {
running = append(running, j)
}
}
// Second pass: detect retry jobs that just reached passed.
for _, j := range b.Jobs {
if j.Type != "script" || j.State != "passed" || j.RetriesCount == 0 {
continue
}
tj := t.jobs[j.ID]
if tj == nil || tj.RetryReported {
continue
}
for _, orig := range t.jobs {
if orig.Job.RetriedInJobID == j.ID && orig.Reported {
status.NewlyRetryPassed = append(status.NewlyRetryPassed, j)
tj.RetryReported = true
break
}
}
}
status.Summary = t.summarize(b)
status.TotalRunning = len(running)
status.Running = running
return status
}
// PassedJobs returns all non-superseded jobs that passed, sorted by start time.
func (t *JobTracker) PassedJobs() []buildkite.Job {
var result []buildkite.Job
for _, tj := range t.jobs {
if tj.Job.State == "passed" && !tj.Job.Retried {
result = append(result, tj.Job)
}
}
sortJobsByStartTime(result)
return result
}
// FailedJobs returns all hard-failed, non-superseded jobs (excludes soft failures),
// sorted by start time.
func (t *JobTracker) FailedJobs() []buildkite.Job {
var result []buildkite.Job
for _, tj := range t.jobs {
job := NewFormattedJob(tj.Job)
if job.IsFailed() && !job.IsSoftFailed() && !tj.Job.Retried {
result = append(result, tj.Job)
}
}
sortJobsByStartTime(result)
return result
}
func sortJobsByStartTime(jobs []buildkite.Job) {
sort.Slice(jobs, func(i, j int) bool {
si, sj := jobs[i].StartedAt, jobs[j].StartedAt
switch {
case si == nil && sj == nil:
return jobs[i].ID < jobs[j].ID
case si == nil:
return false
case sj == nil:
return true
case si.Time.Equal(sj.Time):
return jobs[i].ID < jobs[j].ID
default:
return si.Before(sj.Time)
}
})
}
func (t *JobTracker) summarize(b buildkite.Build) JobSummary {
var s JobSummary
for _, j := range b.Jobs {
if j.Type != "script" || j.Retried {
continue
}
job := NewFormattedJob(j)
if job.IsSoftFailed() {
s.SoftFailed++
continue
}
switch j.State {
case "running", "canceling", "timing_out":
s.Running++
case "passed":
s.Passed++
case "failed", "timed_out", "canceled", "expired":
s.Failed++
case "skipped", "broken":
s.Skipped++
case "blocked", "blocked_failed":
s.Blocked++
case "scheduled", "assigned", "accepted", "reserved":
s.Scheduled++
case "waiting", "waiting_failed",
"pending", "limited", "limiting",
"platform_limited", "platform_limiting":
s.Waiting++
}
}
return s
}
func isActiveState(state string) bool {
return state == "running" || state == "canceling" || state == "timing_out"
}