@checkstack/healthcheck-jenkins-backend 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/package.json +23 -0
- package/src/collectors/build-history.test.ts +106 -0
- package/src/collectors/build-history.ts +280 -0
- package/src/collectors/index.ts +5 -0
- package/src/collectors/job-status.test.ts +146 -0
- package/src/collectors/job-status.ts +241 -0
- package/src/collectors/node-health.test.ts +149 -0
- package/src/collectors/node-health.ts +305 -0
- package/src/collectors/queue-info.test.ts +113 -0
- package/src/collectors/queue-info.ts +215 -0
- package/src/collectors/server-info.test.ts +90 -0
- package/src/collectors/server-info.ts +169 -0
- package/src/index.ts +43 -0
- package/src/plugin-metadata.ts +9 -0
- package/src/strategy.test.ts +198 -0
- package/src/strategy.ts +228 -0
- package/src/transport-client.ts +38 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @checkstack/healthcheck-jenkins-backend
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 97c5a6b: Add Jenkins health check strategy with 5 collectors
|
|
8
|
+
|
|
9
|
+
- **Jenkins Strategy**: Transport client for Jenkins REST API with Basic Auth (username + API token)
|
|
10
|
+
- **Server Info Collector**: Jenkins version, mode, executor count, job count
|
|
11
|
+
- **Job Status Collector**: Individual job monitoring, last build status, build duration
|
|
12
|
+
- **Build History Collector**: Analyze recent builds for trends (success rate, avg duration)
|
|
13
|
+
- **Queue Info Collector**: Monitor build queue length, wait times, stuck items
|
|
14
|
+
- **Node Health Collector**: Agent availability, executor utilization
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [97c5a6b]
|
|
19
|
+
- Updated dependencies [8e43507]
|
|
20
|
+
- Updated dependencies [97c5a6b]
|
|
21
|
+
- @checkstack/backend-api@0.2.0
|
|
22
|
+
- @checkstack/common@0.1.0
|
|
23
|
+
- @checkstack/healthcheck-common@0.2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/healthcheck-jenkins-backend",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "bun run lint:code",
|
|
9
|
+
"lint:code": "eslint . --max-warnings 0",
|
|
10
|
+
"test": "bun test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@checkstack/backend-api": "workspace:*",
|
|
14
|
+
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@checkstack/healthcheck-common": "workspace:*"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/bun": "^1.0.0",
|
|
19
|
+
"typescript": "^5.0.0",
|
|
20
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
21
|
+
"@checkstack/scripts": "workspace:*"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { BuildHistoryCollector } from "./build-history";
|
|
3
|
+
import type {
|
|
4
|
+
JenkinsTransportClient,
|
|
5
|
+
JenkinsResponse,
|
|
6
|
+
} from "../transport-client";
|
|
7
|
+
|
|
8
|
+
describe("BuildHistoryCollector", () => {
|
|
9
|
+
const collector = new BuildHistoryCollector();
|
|
10
|
+
|
|
11
|
+
const createMockClient = (
|
|
12
|
+
response: JenkinsResponse
|
|
13
|
+
): JenkinsTransportClient => ({
|
|
14
|
+
exec: async () => response,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should collect build history successfully", async () => {
|
|
18
|
+
const mockClient = createMockClient({
|
|
19
|
+
statusCode: 200,
|
|
20
|
+
data: {
|
|
21
|
+
builds: [
|
|
22
|
+
{ number: 10, result: "SUCCESS", duration: 60000 },
|
|
23
|
+
{ number: 9, result: "SUCCESS", duration: 55000 },
|
|
24
|
+
{ number: 8, result: "FAILURE", duration: 40000 },
|
|
25
|
+
{ number: 7, result: "UNSTABLE", duration: 70000 },
|
|
26
|
+
{ number: 6, result: "SUCCESS", duration: 65000 },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const result = await collector.execute({
|
|
32
|
+
config: { jobName: "my-job", buildCount: 10 },
|
|
33
|
+
client: mockClient,
|
|
34
|
+
pluginId: "healthcheck-jenkins",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result.error).toBeUndefined();
|
|
38
|
+
expect(result.result.totalBuilds).toBe(5);
|
|
39
|
+
expect(result.result.successCount).toBe(3);
|
|
40
|
+
expect(result.result.failureCount).toBe(1);
|
|
41
|
+
expect(result.result.unstableCount).toBe(1);
|
|
42
|
+
expect(result.result.successRate).toBe(60);
|
|
43
|
+
expect(result.result.avgDurationMs).toBe(58000);
|
|
44
|
+
expect(result.result.minDurationMs).toBe(40000);
|
|
45
|
+
expect(result.result.maxDurationMs).toBe(70000);
|
|
46
|
+
expect(result.result.lastSuccessBuildNumber).toBe(10);
|
|
47
|
+
expect(result.result.lastFailureBuildNumber).toBe(8);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle empty builds array", async () => {
|
|
51
|
+
const mockClient = createMockClient({
|
|
52
|
+
statusCode: 200,
|
|
53
|
+
data: { builds: [] },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await collector.execute({
|
|
57
|
+
config: { jobName: "new-job", buildCount: 10 },
|
|
58
|
+
client: mockClient,
|
|
59
|
+
pluginId: "healthcheck-jenkins",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.error).toBeUndefined();
|
|
63
|
+
expect(result.result.totalBuilds).toBe(0);
|
|
64
|
+
expect(result.result.successRate).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should aggregate correctly", () => {
|
|
68
|
+
const runs: Parameters<typeof collector.aggregateResult>[0] = [
|
|
69
|
+
{
|
|
70
|
+
status: "healthy" as const,
|
|
71
|
+
latencyMs: 100,
|
|
72
|
+
metadata: {
|
|
73
|
+
totalBuilds: 10,
|
|
74
|
+
successCount: 8,
|
|
75
|
+
failureCount: 1,
|
|
76
|
+
unstableCount: 1,
|
|
77
|
+
abortedCount: 0,
|
|
78
|
+
successRate: 80,
|
|
79
|
+
avgDurationMs: 60000,
|
|
80
|
+
minDurationMs: 40000,
|
|
81
|
+
maxDurationMs: 80000,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
status: "healthy" as const,
|
|
86
|
+
latencyMs: 100,
|
|
87
|
+
metadata: {
|
|
88
|
+
totalBuilds: 10,
|
|
89
|
+
successCount: 6,
|
|
90
|
+
failureCount: 2,
|
|
91
|
+
unstableCount: 1,
|
|
92
|
+
abortedCount: 1,
|
|
93
|
+
successRate: 60,
|
|
94
|
+
avgDurationMs: 80000,
|
|
95
|
+
minDurationMs: 50000,
|
|
96
|
+
maxDurationMs: 100000,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const aggregated = collector.aggregateResult(runs);
|
|
102
|
+
|
|
103
|
+
expect(aggregated.avgSuccessRate).toBe(70);
|
|
104
|
+
expect(aggregated.avgBuildDuration).toBe(70000);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import { healthResultNumber } from "@checkstack/healthcheck-common";
|
|
9
|
+
import { pluginMetadata } from "../plugin-metadata";
|
|
10
|
+
import type { JenkinsTransportClient } from "../transport-client";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// CONFIGURATION SCHEMA
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const buildHistoryConfigSchema = z.object({
|
|
17
|
+
jobName: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe("Full job path (e.g., 'folder/job-name' or 'my-job')"),
|
|
21
|
+
buildCount: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(100)
|
|
26
|
+
.default(10)
|
|
27
|
+
.describe("Number of recent builds to analyze"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type BuildHistoryConfig = z.infer<typeof buildHistoryConfigSchema>;
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// RESULT SCHEMAS
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
const buildHistoryResultSchema = z.object({
|
|
37
|
+
totalBuilds: healthResultNumber({
|
|
38
|
+
"x-chart-type": "counter",
|
|
39
|
+
"x-chart-label": "Total Builds",
|
|
40
|
+
}),
|
|
41
|
+
successCount: healthResultNumber({
|
|
42
|
+
"x-chart-type": "counter",
|
|
43
|
+
"x-chart-label": "Successful",
|
|
44
|
+
}),
|
|
45
|
+
failureCount: healthResultNumber({
|
|
46
|
+
"x-chart-type": "counter",
|
|
47
|
+
"x-chart-label": "Failed",
|
|
48
|
+
}),
|
|
49
|
+
unstableCount: healthResultNumber({
|
|
50
|
+
"x-chart-type": "counter",
|
|
51
|
+
"x-chart-label": "Unstable",
|
|
52
|
+
}),
|
|
53
|
+
abortedCount: healthResultNumber({
|
|
54
|
+
"x-chart-type": "counter",
|
|
55
|
+
"x-chart-label": "Aborted",
|
|
56
|
+
}),
|
|
57
|
+
successRate: healthResultNumber({
|
|
58
|
+
"x-chart-type": "gauge",
|
|
59
|
+
"x-chart-label": "Success Rate",
|
|
60
|
+
"x-chart-unit": "%",
|
|
61
|
+
}),
|
|
62
|
+
avgDurationMs: healthResultNumber({
|
|
63
|
+
"x-chart-type": "line",
|
|
64
|
+
"x-chart-label": "Avg Duration",
|
|
65
|
+
"x-chart-unit": "ms",
|
|
66
|
+
}),
|
|
67
|
+
minDurationMs: healthResultNumber({
|
|
68
|
+
"x-chart-type": "line",
|
|
69
|
+
"x-chart-label": "Min Duration",
|
|
70
|
+
"x-chart-unit": "ms",
|
|
71
|
+
}),
|
|
72
|
+
maxDurationMs: healthResultNumber({
|
|
73
|
+
"x-chart-type": "line",
|
|
74
|
+
"x-chart-label": "Max Duration",
|
|
75
|
+
"x-chart-unit": "ms",
|
|
76
|
+
}),
|
|
77
|
+
lastSuccessBuildNumber: healthResultNumber({
|
|
78
|
+
"x-chart-type": "counter",
|
|
79
|
+
"x-chart-label": "Last Success #",
|
|
80
|
+
}).optional(),
|
|
81
|
+
lastFailureBuildNumber: healthResultNumber({
|
|
82
|
+
"x-chart-type": "counter",
|
|
83
|
+
"x-chart-label": "Last Failure #",
|
|
84
|
+
}).optional(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export type BuildHistoryResult = z.infer<typeof buildHistoryResultSchema>;
|
|
88
|
+
|
|
89
|
+
const buildHistoryAggregatedSchema = z.object({
|
|
90
|
+
avgSuccessRate: healthResultNumber({
|
|
91
|
+
"x-chart-type": "gauge",
|
|
92
|
+
"x-chart-label": "Avg Success Rate",
|
|
93
|
+
"x-chart-unit": "%",
|
|
94
|
+
}),
|
|
95
|
+
avgBuildDuration: healthResultNumber({
|
|
96
|
+
"x-chart-type": "line",
|
|
97
|
+
"x-chart-label": "Avg Build Duration",
|
|
98
|
+
"x-chart-unit": "ms",
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export type BuildHistoryAggregatedResult = z.infer<
|
|
103
|
+
typeof buildHistoryAggregatedSchema
|
|
104
|
+
>;
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// BUILD HISTORY COLLECTOR
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Collector for Jenkins build history.
|
|
112
|
+
* Analyzes recent builds for trends and patterns.
|
|
113
|
+
*/
|
|
114
|
+
export class BuildHistoryCollector
|
|
115
|
+
implements
|
|
116
|
+
CollectorStrategy<
|
|
117
|
+
JenkinsTransportClient,
|
|
118
|
+
BuildHistoryConfig,
|
|
119
|
+
BuildHistoryResult,
|
|
120
|
+
BuildHistoryAggregatedResult
|
|
121
|
+
>
|
|
122
|
+
{
|
|
123
|
+
id = "build-history";
|
|
124
|
+
displayName = "Build History";
|
|
125
|
+
description = "Analyze recent build trends for a Jenkins job";
|
|
126
|
+
|
|
127
|
+
supportedPlugins = [pluginMetadata];
|
|
128
|
+
allowMultiple = true;
|
|
129
|
+
|
|
130
|
+
config = new Versioned({ version: 1, schema: buildHistoryConfigSchema });
|
|
131
|
+
result = new Versioned({ version: 1, schema: buildHistoryResultSchema });
|
|
132
|
+
aggregatedResult = new Versioned({
|
|
133
|
+
version: 1,
|
|
134
|
+
schema: buildHistoryAggregatedSchema,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
async execute({
|
|
138
|
+
config,
|
|
139
|
+
client,
|
|
140
|
+
}: {
|
|
141
|
+
config: BuildHistoryConfig;
|
|
142
|
+
client: JenkinsTransportClient;
|
|
143
|
+
pluginId: string;
|
|
144
|
+
}): Promise<CollectorResult<BuildHistoryResult>> {
|
|
145
|
+
// Encode job path for URL (handle folders)
|
|
146
|
+
const jobPath = config.jobName
|
|
147
|
+
.split("/")
|
|
148
|
+
.map((part) => `job/${encodeURIComponent(part)}`)
|
|
149
|
+
.join("/");
|
|
150
|
+
|
|
151
|
+
const response = await client.exec({
|
|
152
|
+
path: `/${jobPath}/api/json`,
|
|
153
|
+
query: {
|
|
154
|
+
tree: `builds[number,result,duration,timestamp]{0,${config.buildCount}}`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (response.error) {
|
|
159
|
+
return {
|
|
160
|
+
result: {
|
|
161
|
+
totalBuilds: 0,
|
|
162
|
+
successCount: 0,
|
|
163
|
+
failureCount: 0,
|
|
164
|
+
unstableCount: 0,
|
|
165
|
+
abortedCount: 0,
|
|
166
|
+
successRate: 0,
|
|
167
|
+
avgDurationMs: 0,
|
|
168
|
+
minDurationMs: 0,
|
|
169
|
+
maxDurationMs: 0,
|
|
170
|
+
},
|
|
171
|
+
error: response.error,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data = response.data as {
|
|
176
|
+
builds?: Array<{
|
|
177
|
+
number?: number;
|
|
178
|
+
result?: string;
|
|
179
|
+
duration?: number;
|
|
180
|
+
timestamp?: number;
|
|
181
|
+
}>;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const builds = data.builds || [];
|
|
185
|
+
|
|
186
|
+
// Count results
|
|
187
|
+
let successCount = 0;
|
|
188
|
+
let failureCount = 0;
|
|
189
|
+
let unstableCount = 0;
|
|
190
|
+
let abortedCount = 0;
|
|
191
|
+
let lastSuccessBuildNumber: number | undefined;
|
|
192
|
+
let lastFailureBuildNumber: number | undefined;
|
|
193
|
+
|
|
194
|
+
const durations: number[] = [];
|
|
195
|
+
|
|
196
|
+
for (const build of builds) {
|
|
197
|
+
if (build.duration !== undefined) {
|
|
198
|
+
durations.push(build.duration);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
switch (build.result) {
|
|
202
|
+
case "SUCCESS": {
|
|
203
|
+
successCount++;
|
|
204
|
+
if (lastSuccessBuildNumber === undefined) {
|
|
205
|
+
lastSuccessBuildNumber = build.number;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "FAILURE": {
|
|
210
|
+
failureCount++;
|
|
211
|
+
if (lastFailureBuildNumber === undefined) {
|
|
212
|
+
lastFailureBuildNumber = build.number;
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "UNSTABLE": {
|
|
217
|
+
unstableCount++;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "ABORTED": {
|
|
221
|
+
abortedCount++;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const totalBuilds = builds.length;
|
|
228
|
+
const successRate =
|
|
229
|
+
totalBuilds > 0 ? Math.round((successCount / totalBuilds) * 100) : 0;
|
|
230
|
+
|
|
231
|
+
const avgDurationMs =
|
|
232
|
+
durations.length > 0
|
|
233
|
+
? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
|
|
234
|
+
: 0;
|
|
235
|
+
|
|
236
|
+
const minDurationMs = durations.length > 0 ? Math.min(...durations) : 0;
|
|
237
|
+
const maxDurationMs = durations.length > 0 ? Math.max(...durations) : 0;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
result: {
|
|
241
|
+
totalBuilds,
|
|
242
|
+
successCount,
|
|
243
|
+
failureCount,
|
|
244
|
+
unstableCount,
|
|
245
|
+
abortedCount,
|
|
246
|
+
successRate,
|
|
247
|
+
avgDurationMs,
|
|
248
|
+
minDurationMs,
|
|
249
|
+
maxDurationMs,
|
|
250
|
+
lastSuccessBuildNumber,
|
|
251
|
+
lastFailureBuildNumber,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
aggregateResult(
|
|
257
|
+
runs: HealthCheckRunForAggregation<BuildHistoryResult>[]
|
|
258
|
+
): BuildHistoryAggregatedResult {
|
|
259
|
+
const successRates = runs
|
|
260
|
+
.map((r) => r.metadata?.successRate)
|
|
261
|
+
.filter((v): v is number => typeof v === "number");
|
|
262
|
+
|
|
263
|
+
const durations = runs
|
|
264
|
+
.map((r) => r.metadata?.avgDurationMs)
|
|
265
|
+
.filter((v): v is number => typeof v === "number");
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
avgSuccessRate:
|
|
269
|
+
successRates.length > 0
|
|
270
|
+
? Math.round(
|
|
271
|
+
successRates.reduce((a, b) => a + b, 0) / successRates.length
|
|
272
|
+
)
|
|
273
|
+
: 0,
|
|
274
|
+
avgBuildDuration:
|
|
275
|
+
durations.length > 0
|
|
276
|
+
? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
|
|
277
|
+
: 0,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ServerInfoCollector } from "./server-info";
|
|
2
|
+
export { JobStatusCollector } from "./job-status";
|
|
3
|
+
export { BuildHistoryCollector } from "./build-history";
|
|
4
|
+
export { QueueInfoCollector } from "./queue-info";
|
|
5
|
+
export { NodeHealthCollector } from "./node-health";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { JobStatusCollector } from "./job-status";
|
|
3
|
+
import type {
|
|
4
|
+
JenkinsTransportClient,
|
|
5
|
+
JenkinsResponse,
|
|
6
|
+
} from "../transport-client";
|
|
7
|
+
|
|
8
|
+
describe("JobStatusCollector", () => {
|
|
9
|
+
const collector = new JobStatusCollector();
|
|
10
|
+
|
|
11
|
+
const createMockClient = (
|
|
12
|
+
response: JenkinsResponse
|
|
13
|
+
): JenkinsTransportClient => ({
|
|
14
|
+
exec: async () => response,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should collect job status successfully", async () => {
|
|
18
|
+
const mockClient = createMockClient({
|
|
19
|
+
statusCode: 200,
|
|
20
|
+
data: {
|
|
21
|
+
name: "my-job",
|
|
22
|
+
buildable: true,
|
|
23
|
+
color: "blue",
|
|
24
|
+
inQueue: false,
|
|
25
|
+
lastBuild: {
|
|
26
|
+
number: 42,
|
|
27
|
+
result: "SUCCESS",
|
|
28
|
+
duration: 60000,
|
|
29
|
+
timestamp: Date.now() - 3600000, // 1 hour ago
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = await collector.execute({
|
|
35
|
+
config: { jobName: "my-job", checkLastBuild: true },
|
|
36
|
+
client: mockClient,
|
|
37
|
+
pluginId: "healthcheck-jenkins",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.error).toBeUndefined();
|
|
41
|
+
expect(result.result.jobName).toBe("my-job");
|
|
42
|
+
expect(result.result.buildable).toBe(true);
|
|
43
|
+
expect(result.result.color).toBe("blue");
|
|
44
|
+
expect(result.result.lastBuildNumber).toBe(42);
|
|
45
|
+
expect(result.result.lastBuildResult).toBe("SUCCESS");
|
|
46
|
+
expect(result.result.lastBuildDurationMs).toBe(60000);
|
|
47
|
+
expect(result.result.timeSinceLastBuildMs).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should report error for failed build", async () => {
|
|
51
|
+
const mockClient = createMockClient({
|
|
52
|
+
statusCode: 200,
|
|
53
|
+
data: {
|
|
54
|
+
name: "failing-job",
|
|
55
|
+
buildable: true,
|
|
56
|
+
color: "red",
|
|
57
|
+
inQueue: false,
|
|
58
|
+
lastBuild: {
|
|
59
|
+
number: 10,
|
|
60
|
+
result: "FAILURE",
|
|
61
|
+
duration: 30000,
|
|
62
|
+
timestamp: Date.now() - 1800000,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await collector.execute({
|
|
68
|
+
config: { jobName: "failing-job", checkLastBuild: true },
|
|
69
|
+
client: mockClient,
|
|
70
|
+
pluginId: "healthcheck-jenkins",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.error).toBe("Last build: FAILURE");
|
|
74
|
+
expect(result.result.lastBuildResult).toBe("FAILURE");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should handle folder paths correctly", async () => {
|
|
78
|
+
let capturedPath = "";
|
|
79
|
+
const mockClient: JenkinsTransportClient = {
|
|
80
|
+
exec: async (req) => {
|
|
81
|
+
capturedPath = req.path;
|
|
82
|
+
return {
|
|
83
|
+
statusCode: 200,
|
|
84
|
+
data: { name: "nested-job", buildable: true, color: "blue" },
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await collector.execute({
|
|
90
|
+
config: { jobName: "folder/subfolder/nested-job", checkLastBuild: false },
|
|
91
|
+
client: mockClient,
|
|
92
|
+
pluginId: "healthcheck-jenkins",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(capturedPath).toBe(
|
|
96
|
+
"/job/folder/job/subfolder/job/nested-job/api/json"
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should aggregate success rate correctly", () => {
|
|
101
|
+
const runs: Parameters<typeof collector.aggregateResult>[0] = [
|
|
102
|
+
{
|
|
103
|
+
status: "healthy" as const,
|
|
104
|
+
latencyMs: 100,
|
|
105
|
+
metadata: {
|
|
106
|
+
jobName: "my-job",
|
|
107
|
+
buildable: true,
|
|
108
|
+
inQueue: false,
|
|
109
|
+
color: "blue",
|
|
110
|
+
lastBuildResult: "SUCCESS",
|
|
111
|
+
lastBuildDurationMs: 60000,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
status: "healthy" as const,
|
|
116
|
+
latencyMs: 100,
|
|
117
|
+
metadata: {
|
|
118
|
+
jobName: "my-job",
|
|
119
|
+
buildable: true,
|
|
120
|
+
inQueue: false,
|
|
121
|
+
color: "blue",
|
|
122
|
+
lastBuildResult: "SUCCESS",
|
|
123
|
+
lastBuildDurationMs: 80000,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
status: "unhealthy" as const,
|
|
128
|
+
latencyMs: 100,
|
|
129
|
+
metadata: {
|
|
130
|
+
jobName: "my-job",
|
|
131
|
+
buildable: true,
|
|
132
|
+
inQueue: false,
|
|
133
|
+
color: "red",
|
|
134
|
+
lastBuildResult: "FAILURE",
|
|
135
|
+
lastBuildDurationMs: 40000,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const aggregated = collector.aggregateResult(runs);
|
|
141
|
+
|
|
142
|
+
expect(aggregated.successRate).toBe(67); // 2/3
|
|
143
|
+
expect(aggregated.avgBuildDurationMs).toBe(60000); // (60000+80000+40000)/3
|
|
144
|
+
expect(aggregated.buildableRate).toBe(100);
|
|
145
|
+
});
|
|
146
|
+
});
|