@checkstack/healthcheck-ssh-backend 0.1.14 → 0.2.1

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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # @checkstack/healthcheck-ssh-backend
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 869b4ab: ## Health Check Execution Improvements
8
+
9
+ ### Breaking Changes (backend-api)
10
+
11
+ - `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
12
+
13
+ ### Features
14
+
15
+ - **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
16
+ - **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
17
+ - **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
18
+
19
+ ### Fixes
20
+
21
+ - Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
22
+ - Simplified registry type signatures by using default type parameters.
23
+
24
+ - Updated dependencies [869b4ab]
25
+ - @checkstack/backend-api@0.8.0
26
+
27
+ ## 0.2.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
32
+
33
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
34
+
35
+ **Key changes:**
36
+
37
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
38
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
39
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
40
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
41
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
42
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
43
+
44
+ **Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [3dd1914]
49
+ - @checkstack/backend-api@0.7.0
50
+
3
51
  ## 0.1.14
4
52
 
5
53
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-ssh-backend",
3
- "version": "0.1.14",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,10 +9,10 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.2",
13
- "@checkstack/common": "0.6.1",
14
- "@checkstack/healthcheck-common": "0.8.1",
15
- "@checkstack/healthcheck-ssh-common": "0.1.7",
12
+ "@checkstack/backend-api": "0.7.0",
13
+ "@checkstack/common": "0.6.2",
14
+ "@checkstack/healthcheck-common": "0.8.2",
15
+ "@checkstack/healthcheck-ssh-common": "0.1.8",
16
16
  "ssh2": "^1.15.0"
17
17
  },
18
18
  "devDependencies": {
@@ -104,8 +104,8 @@ describe("CommandCollector", () => {
104
104
  let aggregated = collector.mergeResult(undefined, runs[0]);
105
105
  aggregated = collector.mergeResult(aggregated, runs[1]);
106
106
 
107
- expect(aggregated.avgExecutionTimeMs).toBe(75);
108
- expect(aggregated.successRate).toBe(100);
107
+ expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
108
+ expect(aggregated.successRate.rate).toBe(100);
109
109
  });
110
110
 
111
111
  it("should calculate success rate based on exit codes", () => {
@@ -142,7 +142,7 @@ describe("CommandCollector", () => {
142
142
  let aggregated = collector.mergeResult(undefined, runs[0]);
143
143
  aggregated = collector.mergeResult(aggregated, runs[1]);
144
144
 
145
- expect(aggregated.successRate).toBe(50);
145
+ expect(aggregated.successRate.rate).toBe(50);
146
146
  });
147
147
  });
148
148
 
@@ -5,11 +5,11 @@ import {
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
7
  mergeAverage,
8
- averageStateSchema,
9
8
  mergeRate,
10
- rateStateSchema,
11
- type AverageState,
12
- type RateState,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
13
13
  } from "@checkstack/backend-api";
14
14
  import {
15
15
  healthResultNumber,
@@ -55,31 +55,24 @@ const commandResultSchema = healthResultSchema({
55
55
 
56
56
  export type CommandResult = z.infer<typeof commandResultSchema>;
57
57
 
58
- const commandAggregatedDisplaySchema = healthResultSchema({
59
- avgExecutionTimeMs: healthResultNumber({
58
+ // Aggregated result fields definition
59
+ const commandAggregatedFields = {
60
+ avgExecutionTimeMs: aggregatedAverage({
60
61
  "x-chart-type": "line",
61
62
  "x-chart-label": "Avg Execution Time",
62
63
  "x-chart-unit": "ms",
63
64
  }),
64
- successRate: healthResultNumber({
65
+ successRate: aggregatedRate({
65
66
  "x-chart-type": "gauge",
66
67
  "x-chart-label": "Success Rate",
67
68
  "x-chart-unit": "%",
68
69
  }),
69
- });
70
-
71
- const commandAggregatedInternalSchema = z.object({
72
- _executionTime: averageStateSchema
73
- .optional(),
74
- _success: rateStateSchema
75
- .optional(),
76
- });
70
+ };
77
71
 
78
- const commandAggregatedSchema = commandAggregatedDisplaySchema.merge(
79
- commandAggregatedInternalSchema,
80
- );
81
-
82
- export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
72
+ // Type inferred from field definitions
73
+ export type CommandAggregatedResult = InferAggregatedResult<
74
+ typeof commandAggregatedFields
75
+ >;
83
76
 
84
77
  // ============================================================================
85
78
  // COMMAND COLLECTOR (PSEUDO-COLLECTOR)
@@ -112,9 +105,9 @@ export class CommandCollector implements CollectorStrategy<
112
105
 
113
106
  config = new Versioned({ version: 1, schema: commandConfigSchema });
114
107
  result = new Versioned({ version: 1, schema: commandResultSchema });
115
- aggregatedResult = new Versioned({
108
+ aggregatedResult = new VersionedAggregated({
116
109
  version: 1,
117
- schema: commandAggregatedSchema,
110
+ fields: commandAggregatedFields,
118
111
  });
119
112
 
120
113
  async execute({
@@ -145,22 +138,13 @@ export class CommandCollector implements CollectorStrategy<
145
138
  ): CommandAggregatedResult {
146
139
  const metadata = run.metadata;
147
140
 
148
- const executionTimeState = mergeAverage(
149
- existing?._executionTime as AverageState | undefined,
150
- metadata?.executionTimeMs,
151
- );
152
-
153
141
  // Success is exit code 0
154
- const successState = mergeRate(
155
- existing?._success as RateState | undefined,
156
- metadata?.exitCode === 0,
157
- );
158
-
159
142
  return {
160
- avgExecutionTimeMs: executionTimeState.avg,
161
- successRate: successState.rate,
162
- _executionTime: executionTimeState,
163
- _success: successState,
143
+ avgExecutionTimeMs: mergeAverage(
144
+ existing?.avgExecutionTimeMs,
145
+ metadata?.executionTimeMs,
146
+ ),
147
+ successRate: mergeRate(existing?.successRate, metadata?.exitCode === 0),
164
148
  };
165
149
  }
166
150
  }
@@ -142,9 +142,9 @@ describe("SshHealthCheckStrategy", () => {
142
142
  let aggregated = strategy.mergeResult(undefined, runs[0]);
143
143
  aggregated = strategy.mergeResult(aggregated, runs[1]);
144
144
 
145
- expect(aggregated.avgConnectionTime).toBe(75);
146
- expect(aggregated.successRate).toBe(100);
147
- expect(aggregated.errorCount).toBe(0);
145
+ expect(aggregated.avgConnectionTime.avg).toBe(75);
146
+ expect(aggregated.successRate.rate).toBe(100);
147
+ expect(aggregated.errorCount.count).toBe(0);
148
148
  });
149
149
 
150
150
  it("should count errors", () => {
@@ -164,8 +164,8 @@ describe("SshHealthCheckStrategy", () => {
164
164
 
165
165
  const aggregated = strategy.mergeResult(undefined, run);
166
166
 
167
- expect(aggregated.errorCount).toBe(1);
168
- expect(aggregated.successRate).toBe(0);
167
+ expect(aggregated.errorCount.count).toBe(1);
168
+ expect(aggregated.successRate.rate).toBe(0);
169
169
  });
170
170
  });
171
171
  });
package/src/strategy.ts CHANGED
@@ -3,22 +3,20 @@ import {
3
3
  HealthCheckStrategy,
4
4
  HealthCheckRunForAggregation,
5
5
  Versioned,
6
- z,
7
- configString,
8
- configNumber,
9
- type ConnectedClient,
6
+ VersionedAggregated,
7
+ aggregatedAverage,
8
+ aggregatedMinMax,
9
+ aggregatedRate,
10
+ aggregatedCounter,
10
11
  mergeAverage,
11
- averageStateSchema,
12
12
  mergeRate,
13
- rateStateSchema,
14
13
  mergeCounter,
15
- counterStateSchema,
16
14
  mergeMinMax,
17
- minMaxStateSchema,
18
- type AverageState,
19
- type RateState,
20
- type CounterState,
21
- type MinMaxState,
15
+ z,
16
+ configString,
17
+ type ConnectedClient,
18
+ type InferAggregatedResult,
19
+ baseStrategyConfigSchema,
22
20
  } from "@checkstack/backend-api";
23
21
  import {
24
22
  healthResultBoolean,
@@ -35,7 +33,7 @@ import type { SshTransportClient, SshCommandResult } from "./transport-client";
35
33
  /**
36
34
  * Configuration schema for SSH health checks.
37
35
  */
38
- export const sshConfigSchema = z.object({
36
+ export const sshConfigSchema = baseStrategyConfigSchema.extend({
39
37
  host: z.string().describe("SSH server hostname"),
40
38
  port: z.number().int().min(1).max(65_535).default(22).describe("SSH port"),
41
39
  username: z.string().describe("SSH username"),
@@ -48,10 +46,6 @@ export const sshConfigSchema = z.object({
48
46
  passphrase: configString({ "x-secret": true })
49
47
  .describe("Passphrase for private key")
50
48
  .optional(),
51
- timeout: configNumber({})
52
- .min(100)
53
- .default(10_000)
54
- .describe("Connection timeout in milliseconds"),
55
49
  });
56
50
 
57
51
  export type SshConfig = z.infer<typeof sshConfigSchema>;
@@ -78,45 +72,30 @@ const sshResultSchema = healthResultSchema({
78
72
 
79
73
  type SshResult = z.infer<typeof sshResultSchema>;
80
74
 
81
- /**
82
- * Aggregated metadata for buckets.
83
- */
84
- const sshAggregatedDisplaySchema = healthResultSchema({
85
- avgConnectionTime: healthResultNumber({
75
+ /** Aggregated field definitions for bucket merging */
76
+ const sshAggregatedFields = {
77
+ avgConnectionTime: aggregatedAverage({
86
78
  "x-chart-type": "line",
87
79
  "x-chart-label": "Avg Connection Time",
88
80
  "x-chart-unit": "ms",
89
81
  }),
90
- maxConnectionTime: healthResultNumber({
82
+ maxConnectionTime: aggregatedMinMax({
91
83
  "x-chart-type": "line",
92
84
  "x-chart-label": "Max Connection Time",
93
85
  "x-chart-unit": "ms",
94
86
  }),
95
- successRate: healthResultNumber({
87
+ successRate: aggregatedRate({
96
88
  "x-chart-type": "gauge",
97
89
  "x-chart-label": "Success Rate",
98
90
  "x-chart-unit": "%",
99
91
  }),
100
- errorCount: healthResultNumber({
92
+ errorCount: aggregatedCounter({
101
93
  "x-chart-type": "counter",
102
94
  "x-chart-label": "Errors",
103
95
  }),
104
- });
105
-
106
- const sshAggregatedInternalSchema = z.object({
107
- _connectionTime: averageStateSchema
108
- .optional(),
109
- _maxConnectionTime: minMaxStateSchema.optional(),
110
- _success: rateStateSchema
111
- .optional(),
112
- _errors: counterStateSchema.optional(),
113
- });
114
-
115
- const sshAggregatedSchema = sshAggregatedDisplaySchema.merge(
116
- sshAggregatedInternalSchema,
117
- );
96
+ };
118
97
 
119
- type SshAggregatedResult = z.infer<typeof sshAggregatedSchema>;
98
+ type SshAggregatedResult = InferAggregatedResult<typeof sshAggregatedFields>;
120
99
 
121
100
  // ============================================================================
122
101
  // SSH CLIENT INTERFACE (for testability)
@@ -207,7 +186,7 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
207
186
  SshConfig,
208
187
  SshTransportClient,
209
188
  SshResult,
210
- SshAggregatedResult
189
+ typeof sshAggregatedFields
211
190
  > {
212
191
  id = "ssh";
213
192
  displayName = "SSH Health Check";
@@ -229,9 +208,9 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
229
208
  schema: sshResultSchema,
230
209
  });
231
210
 
232
- aggregatedResult: Versioned<SshAggregatedResult> = new Versioned({
211
+ aggregatedResult = new VersionedAggregated({
233
212
  version: 1,
234
- schema: sshAggregatedSchema,
213
+ fields: sshAggregatedFields,
235
214
  });
236
215
 
237
216
  mergeResult(
@@ -240,36 +219,23 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
240
219
  ): SshAggregatedResult {
241
220
  const metadata = run.metadata;
242
221
 
243
- const connectionTimeState = mergeAverage(
244
- existing?._connectionTime as AverageState | undefined,
222
+ const avgConnectionTime = mergeAverage(
223
+ existing?.avgConnectionTime,
245
224
  metadata?.connectionTimeMs,
246
225
  );
247
226
 
248
- const maxConnectionTimeState = mergeMinMax(
249
- existing?._maxConnectionTime as MinMaxState | undefined,
227
+ const maxConnectionTime = mergeMinMax(
228
+ existing?.maxConnectionTime,
250
229
  metadata?.connectionTimeMs,
251
230
  );
252
231
 
253
- const successState = mergeRate(
254
- existing?._success as RateState | undefined,
255
- metadata?.connected,
256
- );
232
+ const isSuccess = metadata?.connected ?? false;
233
+ const successRate = mergeRate(existing?.successRate, isSuccess);
257
234
 
258
- const errorState = mergeCounter(
259
- existing?._errors as CounterState | undefined,
260
- metadata?.error !== undefined,
261
- );
235
+ const hasError = metadata?.error !== undefined;
236
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
262
237
 
263
- return {
264
- avgConnectionTime: connectionTimeState.avg,
265
- maxConnectionTime: maxConnectionTimeState.max,
266
- successRate: successState.rate,
267
- errorCount: errorState.count,
268
- _connectionTime: connectionTimeState,
269
- _maxConnectionTime: maxConnectionTimeState,
270
- _success: successState,
271
- _errors: errorState,
272
- };
238
+ return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
273
239
  }
274
240
 
275
241
  /**