@checkstack/maintenance-backend 1.1.5 → 1.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 CHANGED
@@ -1,5 +1,153 @@
1
1
  # @checkstack/maintenance-backend
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 41c77f4: feat(automation): type enum-able trigger/artifact fields as enums for editor value autocompletion
8
+
9
+ The automation editor's staged completion offers concrete values after a
10
+ comparator (`{{ trigger.payload.severity == "high" }}`) only when the
11
+ field's JSON Schema carries an `enum`. Several trigger payload + artifact
12
+ schemas declared closed-set fields as loose `z.string()`, so no values
13
+ were suggested. Tightened them to the canonical enums that already
14
+ existed in each plugin's `-common` package (and matched the hook payload
15
+ types in lockstep so the trigger's `payloadSchema` and `hook` keep the
16
+ same `TPayload`):
17
+
18
+ - **incident** — trigger payloads: `severity` → `IncidentSeverityEnum`,
19
+ `status` / `statusChange` → `IncidentStatusEnum`.
20
+ - **healthcheck** — trigger payloads: `previousStatus` / `newStatus` /
21
+ `status` → `HealthCheckStatusSchema` (across systemDegraded,
22
+ systemHealthy, systemHealthChanged, checkFailed; plus checkCompleted's
23
+ hook type).
24
+ - **dependency** — trigger + artifact: `impactType` → `ImpactTypeSchema`;
25
+ impactPropagated `previousState` / `newState` → `DerivedStateSchema`.
26
+ Also deduped the inline `impactTypeSchema` action-config enum to reuse
27
+ the canonical `ImpactTypeSchema`.
28
+ - **maintenance** — trigger + artifact: `status` →
29
+ `MaintenanceStatusEnum`; deduped the inline `maintenanceStatusEnum`
30
+ (used by `add_update.statusChange`) to the canonical one.
31
+ - **slo** — `achievement.unlocked` trigger + hook: `achievement` →
32
+ `AchievementTypeSchema`.
33
+
34
+ Runtime behaviour is unchanged — these fields always carried valid enum
35
+ values (the underlying records are enum-constrained); only the schema
36
+ types were loose. The hook payload generics are now precise too, which
37
+ caught one stale test fixture asserting an invalid `impactType: "soft"`.
38
+
39
+ Fields that look enum-ish but are genuinely free-form were intentionally
40
+ left as `z.string()`: satellite `region` (user-entered), Jira issue
41
+ `status` (per-instance workflow name), notification `strategyQualifiedId`
42
+ / `errorMessage`, healthcheck collector `result`, and script
43
+ `stdout` / `stderr`.
44
+
45
+ - 41c77f4: feat(maintenance): Phase 9 — actions + system-shaped helpers
46
+
47
+ - Triggers `maintenance.created`, `maintenance.updated` are unchanged;
48
+ they're now lifted out of the inline `register()` block into
49
+ `automations.ts` alongside the new actions.
50
+ - Actions `maintenance.create`, `maintenance.update`,
51
+ `maintenance.add_update` wrapping `MaintenanceService`. Each emits
52
+ the appropriate `maintenanceHooks.*` so downstream automations and
53
+ caches react identically to RPC-driven changes; `add_update`
54
+ re-fetches the window before emitting so the hook payload reflects
55
+ the new status.
56
+ - The two deferred catalog actions land here as
57
+ `maintenance.set_system` (schedule a `now → now+durationMinutes`
58
+ window covering a single system — the "park this system" operation)
59
+ and `maintenance.clear_system` (close every active or scheduled
60
+ window covering a given system — the "let it back into rotation"
61
+ operation).
62
+ - Artifact type `maintenance.window` for downstream steps to consume.
63
+
64
+ ### Patch Changes
65
+
66
+ - 41c77f4: feat(automation): one-time migration of webhook subscriptions + remove legacy integration backend
67
+
68
+ **BREAKING CHANGES** (platform is in BETA — no major bump):
69
+
70
+ - `IntegrationProvider` no longer carries `config` (subscription
71
+ config) or `deliver`. The interface now models a connection provider
72
+ only: connection schema + `getConnectionOptions` + `testConnection`.
73
+ - The legacy subscription / delivery-log / event endpoints
74
+ (`listSubscriptions`, `createSubscription`, `getDeliveryLogs`,
75
+ `listEventTypes`, …) are removed from `integrationContract`.
76
+ - `delivery-coordinator`, `hook-subscriber`, `event-registry`, and the
77
+ `integrationEventExtensionPoint` are deleted. Plugins that
78
+ previously called `integrationEvents.registerEvent(...)` now
79
+ register their hooks as automation triggers via
80
+ `automationTriggerExtensionPoint.registerTrigger(...)`.
81
+ - Frontend pages `IntegrationsPage` and `DeliveryLogsPage` are gone;
82
+ the integration plugin's only remaining UI is connection
83
+ management. Subscription management lives under `/automation/...`.
84
+ - `webhook_subscriptions` and `delivery_logs` tables stay in the
85
+ database for one release as a safety net (no code reads or writes
86
+ them), and will be dropped in a follow-up migration.
87
+
88
+ **New**:
89
+
90
+ - `jira.create_issue`, `teams.post_message`, `webex.post_message`,
91
+ `webhook.send`, `integration-script.run_shell`, and
92
+ `integration-script.run_script` actions registered against the
93
+ Automation Platform with matching `*.message`, `*.delivery`,
94
+ `shell.result`, and `script.result` artifact types. The script
95
+ plugin exposes **two** actions — `run_shell` runs bash via the
96
+ shared `ShellScriptRunner` (Monaco `shell` editor), `run_script`
97
+ runs an ESM module in a Bun subprocess via `EsmScriptRunner`
98
+ (Monaco `typescript` editor + `defineIntegration` helper) — to
99
+ preserve the legacy provider split. `jira.create_issue` keeps the
100
+ dynamic field-mapping dropdown (driven by
101
+ `JIRA_RESOLVERS.FIELD_OPTIONS`).
102
+ - One-time data migration runs on boot in
103
+ `automation-backend.afterPluginsReady`. It reads
104
+ `webhook_subscriptions` via a new service RPC
105
+ `IntegrationApi.listLegacySubscriptions`, translates each row into
106
+ a single-trigger / single-action automation (marked with
107
+ `managed_by = "migrated-subscription:<id>"`), and is idempotent
108
+ across restarts.
109
+ - Failed translations are recorded in a new
110
+ `automation_migration_failures` table and surfaced via
111
+ `AutomationApi.listMigrationFailures` /
112
+ `acknowledgeMigrationFailure` so admins can review and re-create
113
+ failed entries by hand.
114
+
115
+ - Updated dependencies [e2d6f25]
116
+ - Updated dependencies [41c77f4]
117
+ - Updated dependencies [e1a2077]
118
+ - Updated dependencies [41c77f4]
119
+ - Updated dependencies [41c77f4]
120
+ - Updated dependencies [41c77f4]
121
+ - Updated dependencies [41c77f4]
122
+ - Updated dependencies [41c77f4]
123
+ - Updated dependencies [41c77f4]
124
+ - Updated dependencies [6d52276]
125
+ - Updated dependencies [6d52276]
126
+ - Updated dependencies [35bc682]
127
+ - @checkstack/automation-backend@0.2.0
128
+ - @checkstack/catalog-backend@1.2.0
129
+ - @checkstack/common@0.12.0
130
+ - @checkstack/backend-api@0.18.0
131
+ - @checkstack/catalog-common@2.2.3
132
+ - @checkstack/maintenance-common@1.2.3
133
+ - @checkstack/auth-common@0.7.2
134
+ - @checkstack/command-backend@0.1.31
135
+ - @checkstack/notification-common@1.2.1
136
+ - @checkstack/signal-common@0.2.5
137
+ - @checkstack/cache-api@0.3.6
138
+ - @checkstack/cache-utils@0.2.11
139
+
140
+ ## 1.1.6
141
+
142
+ ### Patch Changes
143
+
144
+ - @checkstack/backend-api@0.17.1
145
+ - @checkstack/cache-api@0.3.5
146
+ - @checkstack/catalog-backend@1.1.6
147
+ - @checkstack/command-backend@0.1.30
148
+ - @checkstack/integration-backend@0.1.30
149
+ - @checkstack/cache-utils@0.2.10
150
+
3
151
  ## 1.1.5
4
152
 
5
153
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/maintenance-backend",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -11,30 +11,30 @@
11
11
  "typecheck": "tsgo -b",
12
12
  "generate": "drizzle-kit generate",
13
13
  "lint": "bun run lint:code",
14
- "lint:code": "eslint . --max-warnings 0"
14
+ "lint:code": "eslint . --max-warnings 0",
15
+ "test": "bun test"
15
16
  },
16
17
  "dependencies": {
17
- "@checkstack/backend-api": "0.16.0",
18
- "@checkstack/cache-api": "0.3.3",
19
- "@checkstack/cache-utils": "0.2.8",
20
- "@checkstack/maintenance-common": "1.2.1",
21
- "@checkstack/notification-common": "1.1.1",
22
- "@checkstack/catalog-common": "2.2.1",
23
- "@checkstack/catalog-backend": "1.1.4",
24
- "@checkstack/auth-common": "0.7.0",
25
- "@checkstack/command-backend": "0.1.28",
26
- "@checkstack/signal-common": "0.2.3",
27
- "@checkstack/integration-backend": "0.1.28",
28
- "@checkstack/integration-common": "0.4.0",
18
+ "@checkstack/backend-api": "0.17.1",
19
+ "@checkstack/cache-api": "0.3.5",
20
+ "@checkstack/cache-utils": "0.2.10",
21
+ "@checkstack/maintenance-common": "1.2.2",
22
+ "@checkstack/notification-common": "1.2.0",
23
+ "@checkstack/catalog-common": "2.2.2",
24
+ "@checkstack/catalog-backend": "1.1.6",
25
+ "@checkstack/auth-common": "0.7.1",
26
+ "@checkstack/command-backend": "0.1.30",
27
+ "@checkstack/signal-common": "0.2.4",
28
+ "@checkstack/automation-backend": "0.1.0",
29
29
  "drizzle-orm": "^0.45.0",
30
30
  "zod": "^4.2.1",
31
- "@checkstack/common": "0.10.0",
31
+ "@checkstack/common": "0.11.0",
32
32
  "@orpc/server": "^1.13.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@checkstack/drizzle-helper": "0.0.5",
36
- "@checkstack/scripts": "0.3.2",
37
- "@checkstack/test-utils-backend": "0.1.28",
36
+ "@checkstack/scripts": "0.3.3",
37
+ "@checkstack/test-utils-backend": "0.1.30",
38
38
  "@checkstack/tsconfig": "0.0.7",
39
39
  "@types/bun": "^1.0.0",
40
40
  "drizzle-kit": "^0.31.10",
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Behaviour tests for the maintenance automation triggers + actions.
3
+ */
4
+ import { describe, expect, it, mock } from "bun:test";
5
+ import type { Logger } from "@checkstack/backend-api";
6
+ import { createMockLogger } from "@checkstack/test-utils-backend";
7
+
8
+ import {
9
+ createMaintenanceActions,
10
+ maintenanceArtifactType,
11
+ maintenanceCreatedTrigger,
12
+ maintenanceTriggers,
13
+ maintenanceUpdatedTrigger,
14
+ } from "./automations";
15
+ import { maintenanceHooks } from "./hooks";
16
+ import type { MaintenanceService } from "./service";
17
+
18
+ const logger = createMockLogger() as Logger;
19
+
20
+ const ctxBase = {
21
+ runId: "run-1",
22
+ automationId: "auto-1",
23
+ contextKey: null,
24
+ logger,
25
+ getService: async <T,>(): Promise<T> => {
26
+ throw new Error("not used");
27
+ },
28
+ };
29
+
30
+ // ─── Triggers ──────────────────────────────────────────────────────────
31
+
32
+ describe("maintenance triggers", () => {
33
+ it("exposes two triggers in a stable order", () => {
34
+ expect(maintenanceTriggers).toHaveLength(2);
35
+ expect(maintenanceTriggers[0]).toBe(
36
+ maintenanceCreatedTrigger as (typeof maintenanceTriggers)[number],
37
+ );
38
+ expect(maintenanceTriggers[1]).toBe(
39
+ maintenanceUpdatedTrigger as (typeof maintenanceTriggers)[number],
40
+ );
41
+ });
42
+
43
+ it("extracts maintenanceId as the contextKey on both triggers", () => {
44
+ const payload = {
45
+ maintenanceId: "m-1",
46
+ systemIds: ["sys-1"],
47
+ title: "Deploy",
48
+ status: "scheduled" as const,
49
+ startAt: "2026-05-29T11:00:00Z",
50
+ endAt: "2026-05-29T12:00:00Z",
51
+ };
52
+ expect(maintenanceCreatedTrigger.contextKey?.(payload)).toBe("m-1");
53
+ expect(
54
+ maintenanceUpdatedTrigger.contextKey?.({ ...payload, action: "updated" }),
55
+ ).toBe("m-1");
56
+ });
57
+
58
+ it("requires action enum on updated payload", () => {
59
+ const ok = maintenanceUpdatedTrigger.payloadSchema.safeParse({
60
+ maintenanceId: "m-1",
61
+ systemIds: [],
62
+ title: "Deploy",
63
+ status: "completed",
64
+ startAt: "2026-05-29T11:00:00Z",
65
+ endAt: "2026-05-29T12:00:00Z",
66
+ action: "closed",
67
+ });
68
+ const bad = maintenanceUpdatedTrigger.payloadSchema.safeParse({
69
+ maintenanceId: "m-1",
70
+ systemIds: [],
71
+ title: "Deploy",
72
+ status: "completed",
73
+ startAt: "2026-05-29T11:00:00Z",
74
+ endAt: "2026-05-29T12:00:00Z",
75
+ });
76
+ expect(ok.success).toBe(true);
77
+ expect(bad.success).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe("maintenanceArtifactType", () => {
82
+ it("validates the canonical artifact shape", () => {
83
+ const ok = maintenanceArtifactType.schema.safeParse({
84
+ maintenanceId: "m-1",
85
+ status: "scheduled",
86
+ systemIds: ["sys-1"],
87
+ startAt: "2026-05-29T11:00:00Z",
88
+ endAt: "2026-05-29T12:00:00Z",
89
+ });
90
+ expect(ok.success).toBe(true);
91
+ });
92
+ });
93
+
94
+ // ─── Actions ───────────────────────────────────────────────────────────
95
+
96
+ interface FakeMaintenance {
97
+ id: string;
98
+ title: string;
99
+ description?: string;
100
+ status: string;
101
+ systemIds: string[];
102
+ startAt: Date;
103
+ endAt: Date;
104
+ }
105
+
106
+ function makeService(args: {
107
+ rowToReturn?: FakeMaintenance;
108
+ updateReturn?: FakeMaintenance | undefined;
109
+ getMaintenanceReturn?: FakeMaintenance | undefined;
110
+ activeForSystem?: FakeMaintenance[];
111
+ closeReturn?: FakeMaintenance | undefined;
112
+ }): MaintenanceService & {
113
+ createMock: ReturnType<typeof mock>;
114
+ updateMock: ReturnType<typeof mock>;
115
+ addUpdateMock: ReturnType<typeof mock>;
116
+ getMock: ReturnType<typeof mock>;
117
+ activeMock: ReturnType<typeof mock>;
118
+ closeMock: ReturnType<typeof mock>;
119
+ } {
120
+ const createMock = mock(async (_input: unknown) => args.rowToReturn);
121
+ const updateMock = mock(async (_input: unknown) => args.updateReturn);
122
+ const addUpdateMock = mock(async (_input: unknown) => ({
123
+ id: "upd-1",
124
+ maintenanceId: "m-1",
125
+ message: "x",
126
+ statusChange: undefined,
127
+ createdBy: undefined,
128
+ createdAt: new Date(),
129
+ }));
130
+ const getMock = mock(async (_id: string) => args.getMaintenanceReturn);
131
+ const activeMock = mock(async (_id: string) => args.activeForSystem ?? []);
132
+ const closeMock = mock(async (_id: string, _msg?: string) => args.closeReturn);
133
+ return {
134
+ createMaintenance: createMock,
135
+ updateMaintenance: updateMock,
136
+ addUpdate: addUpdateMock,
137
+ getMaintenance: getMock,
138
+ getMaintenancesForSystem: activeMock,
139
+ closeMaintenance: closeMock,
140
+ createMock,
141
+ updateMock,
142
+ addUpdateMock,
143
+ getMock,
144
+ activeMock,
145
+ closeMock,
146
+ } as unknown as MaintenanceService & {
147
+ createMock: ReturnType<typeof mock>;
148
+ updateMock: ReturnType<typeof mock>;
149
+ addUpdateMock: ReturnType<typeof mock>;
150
+ getMock: ReturnType<typeof mock>;
151
+ activeMock: ReturnType<typeof mock>;
152
+ closeMock: ReturnType<typeof mock>;
153
+ };
154
+ }
155
+
156
+ const sampleRow: FakeMaintenance = {
157
+ id: "m-1",
158
+ title: "Deploy",
159
+ status: "scheduled",
160
+ systemIds: ["sys-1"],
161
+ startAt: new Date("2026-05-29T11:00:00Z"),
162
+ endAt: new Date("2026-05-29T12:00:00Z"),
163
+ };
164
+
165
+ describe("maintenance.create", () => {
166
+ it("creates a maintenance, fires maintenanceCreated, and emits a maintenance.window artifact", async () => {
167
+ const service = makeService({ rowToReturn: sampleRow });
168
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
169
+ const [create] = createMaintenanceActions({
170
+ service,
171
+ emitHook: emitHook as never,
172
+ });
173
+
174
+ const result = await create!.execute({
175
+ ...ctxBase,
176
+ consumedArtifacts: {},
177
+ config: {
178
+ title: "Deploy",
179
+ systemIds: ["sys-1"],
180
+ startAt: "2026-05-29T11:00:00Z",
181
+ endAt: "2026-05-29T12:00:00Z",
182
+ } as never,
183
+ });
184
+
185
+ expect(result.success).toBe(true);
186
+ if (!result.success) return;
187
+ expect(result.externalId).toBe("m-1");
188
+ expect(emitHook).toHaveBeenCalledTimes(1);
189
+ expect(emitHook.mock.calls[0]![0]).toBe(maintenanceHooks.maintenanceCreated);
190
+ });
191
+ });
192
+
193
+ describe("maintenance.update", () => {
194
+ it("returns failure when the maintenance doesn't exist", async () => {
195
+ const service = makeService({ updateReturn: undefined });
196
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
197
+ const [, update] = createMaintenanceActions({
198
+ service,
199
+ emitHook: emitHook as never,
200
+ });
201
+
202
+ const result = await update!.execute({
203
+ ...ctxBase,
204
+ consumedArtifacts: {},
205
+ config: { maintenanceId: "missing", title: "x" } as never,
206
+ });
207
+
208
+ expect(result.success).toBe(false);
209
+ if (result.success) return;
210
+ expect(result.error).toMatch(/not found/i);
211
+ expect(emitHook).not.toHaveBeenCalled();
212
+ });
213
+
214
+ it("emits maintenanceUpdated with action='updated' on success", async () => {
215
+ const service = makeService({ updateReturn: sampleRow });
216
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
217
+ const [, update] = createMaintenanceActions({
218
+ service,
219
+ emitHook: emitHook as never,
220
+ });
221
+
222
+ const result = await update!.execute({
223
+ ...ctxBase,
224
+ consumedArtifacts: {},
225
+ config: { maintenanceId: "m-1", title: "Deploy v2" } as never,
226
+ });
227
+
228
+ expect(result.success).toBe(true);
229
+ const emitCall = emitHook.mock.calls[0]!;
230
+ expect(emitCall[0]).toBe(maintenanceHooks.maintenanceUpdated);
231
+ expect((emitCall[1] as { action: string }).action).toBe("updated");
232
+ });
233
+ });
234
+
235
+ describe("maintenance.add_update", () => {
236
+ it("uses action='closed' when statusChange is 'completed'", async () => {
237
+ const service = makeService({
238
+ getMaintenanceReturn: { ...sampleRow, status: "completed" },
239
+ });
240
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
241
+ const [, , addUpdate] = createMaintenanceActions({
242
+ service,
243
+ emitHook: emitHook as never,
244
+ });
245
+
246
+ const result = await addUpdate!.execute({
247
+ ...ctxBase,
248
+ consumedArtifacts: {},
249
+ config: {
250
+ maintenanceId: "m-1",
251
+ message: "done",
252
+ statusChange: "completed",
253
+ } as never,
254
+ });
255
+
256
+ expect(result.success).toBe(true);
257
+ expect((emitHook.mock.calls[0]![1] as { action: string }).action).toBe(
258
+ "closed",
259
+ );
260
+ });
261
+
262
+ it("returns failure when the maintenance vanishes between addUpdate and getMaintenance", async () => {
263
+ const service = makeService({ getMaintenanceReturn: undefined });
264
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
265
+ const [, , addUpdate] = createMaintenanceActions({
266
+ service,
267
+ emitHook: emitHook as never,
268
+ });
269
+
270
+ const result = await addUpdate!.execute({
271
+ ...ctxBase,
272
+ consumedArtifacts: {},
273
+ config: { maintenanceId: "m-1", message: "x" } as never,
274
+ });
275
+
276
+ expect(result.success).toBe(false);
277
+ if (result.success) return;
278
+ expect(result.error).toMatch(/not found/i);
279
+ expect(emitHook).not.toHaveBeenCalled();
280
+ });
281
+ });
282
+
283
+ describe("maintenance.set_system", () => {
284
+ it("schedules a now+durationMinutes window covering one system", async () => {
285
+ const service = makeService({ rowToReturn: sampleRow });
286
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
287
+ const fixedNow = new Date("2026-05-29T11:00:00Z");
288
+ const [, , , setSystem] = createMaintenanceActions({
289
+ service,
290
+ emitHook: emitHook as never,
291
+ now: () => fixedNow,
292
+ });
293
+
294
+ await setSystem!.execute({
295
+ ...ctxBase,
296
+ consumedArtifacts: {},
297
+ config: {
298
+ systemId: "sys-1",
299
+ durationMinutes: 60,
300
+ } as never,
301
+ });
302
+
303
+ expect(service.createMock).toHaveBeenCalledTimes(1);
304
+ const call = service.createMock.mock.calls[0]![0] as {
305
+ systemIds: string[];
306
+ startAt: Date;
307
+ endAt: Date;
308
+ };
309
+ expect(call.systemIds).toEqual(["sys-1"]);
310
+ expect(call.startAt.toISOString()).toBe("2026-05-29T11:00:00.000Z");
311
+ expect(call.endAt.toISOString()).toBe("2026-05-29T12:00:00.000Z");
312
+ });
313
+ });
314
+
315
+ describe("maintenance.clear_system", () => {
316
+ it("closes every active window for the system + emits one updated hook per close", async () => {
317
+ const window1 = { ...sampleRow, id: "m-1" };
318
+ const window2 = { ...sampleRow, id: "m-2" };
319
+ const service = makeService({
320
+ activeForSystem: [window1, window2],
321
+ closeReturn: window1,
322
+ });
323
+ // closeMaintenance returns the same row both times in the fixture
324
+ // — that's fine for this test; we only assert the count + ids.
325
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
326
+ const actions = createMaintenanceActions({
327
+ service,
328
+ emitHook: emitHook as never,
329
+ });
330
+ const clearSystem = actions[4]!;
331
+
332
+ const result = await clearSystem.execute({
333
+ ...ctxBase,
334
+ consumedArtifacts: {},
335
+ config: { systemId: "sys-1" } as never,
336
+ });
337
+
338
+ expect(result.success).toBe(true);
339
+ expect(service.closeMock).toHaveBeenCalledTimes(2);
340
+ expect(emitHook).toHaveBeenCalledTimes(2);
341
+ for (const call of emitHook.mock.calls) {
342
+ expect(call[0]).toBe(maintenanceHooks.maintenanceUpdated);
343
+ expect((call[1] as { action: string }).action).toBe("closed");
344
+ }
345
+ });
346
+
347
+ it("succeeds and emits an empty artifact when no windows are active for the system", async () => {
348
+ const service = makeService({ activeForSystem: [] });
349
+ const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
350
+ const actions = createMaintenanceActions({
351
+ service,
352
+ emitHook: emitHook as never,
353
+ });
354
+ const clearSystem = actions[4]!;
355
+
356
+ const result = await clearSystem.execute({
357
+ ...ctxBase,
358
+ consumedArtifacts: {},
359
+ config: { systemId: "sys-1" } as never,
360
+ });
361
+
362
+ expect(result.success).toBe(true);
363
+ if (!result.success) return;
364
+ const artifact = result.artifact as { closedMaintenanceIds: string[] };
365
+ expect(artifact.closedMaintenanceIds).toEqual([]);
366
+ expect(emitHook).not.toHaveBeenCalled();
367
+ });
368
+ });
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Maintenance triggers + actions registered with the Automation Platform.
3
+ *
4
+ * Triggers re-expose `maintenanceHooks` as automation entry points
5
+ * (`maintenance.created`, `maintenance.updated`). The existing index
6
+ * registered these inline; this module owns them now alongside the
7
+ * actions so the plugin's automation surface lives in one place.
8
+ *
9
+ * Actions wrap `MaintenanceService` for `create`, `update`, and
10
+ * `add_update`. The catalog Phase-9 chunk deferred two additional
11
+ * "system-shaped" actions to this chunk:
12
+ *
13
+ * - `set_system`: schedule a maintenance window that starts now and
14
+ * covers a single system, for a given duration. The convenient
15
+ * "park this system for an hour" operation.
16
+ * - `clear_system`: close every active/scheduled maintenance that
17
+ * covers a given system. The convenient "let it back into rotation
18
+ * even if maintenance was over-scheduled" operation.
19
+ *
20
+ * Mutation actions emit the matching hooks themselves (via the
21
+ * `emitHook` factory dep) so downstream automations + caches react
22
+ * the same way they do when the mutation comes in via RPC.
23
+ */
24
+ import { z } from "zod";
25
+ import { Versioned, type Hook } from "@checkstack/backend-api";
26
+ import type {
27
+ ActionDefinition,
28
+ TriggerDefinition,
29
+ } from "@checkstack/automation-backend";
30
+ import {
31
+ MaintenanceStatusEnum,
32
+ type MaintenanceStatus,
33
+ } from "@checkstack/maintenance-common";
34
+
35
+ import { maintenanceHooks } from "./hooks";
36
+ import type { MaintenanceService } from "./service";
37
+
38
+ // ─── Payload schemas ───────────────────────────────────────────────────
39
+
40
+ const maintenanceCreatedPayloadSchema = z.object({
41
+ maintenanceId: z.string(),
42
+ systemIds: z.array(z.string()),
43
+ title: z.string(),
44
+ description: z.string().optional(),
45
+ status: MaintenanceStatusEnum,
46
+ startAt: z.string(),
47
+ endAt: z.string(),
48
+ });
49
+
50
+ const maintenanceUpdatedPayloadSchema = z.object({
51
+ maintenanceId: z.string(),
52
+ systemIds: z.array(z.string()),
53
+ title: z.string(),
54
+ description: z.string().optional(),
55
+ status: MaintenanceStatusEnum,
56
+ startAt: z.string(),
57
+ endAt: z.string(),
58
+ action: z.enum(["updated", "closed"]),
59
+ });
60
+
61
+ // ─── Triggers ──────────────────────────────────────────────────────────
62
+
63
+ export const maintenanceCreatedTrigger: TriggerDefinition<
64
+ z.infer<typeof maintenanceCreatedPayloadSchema>
65
+ > = {
66
+ id: "created",
67
+ displayName: "Maintenance Created",
68
+ description: "Fired when a new maintenance is scheduled",
69
+ category: "Maintenance",
70
+ icon: "Wrench",
71
+ payloadSchema: maintenanceCreatedPayloadSchema,
72
+ hook: maintenanceHooks.maintenanceCreated,
73
+ contextKey: (p) => p.maintenanceId,
74
+ };
75
+
76
+ export const maintenanceUpdatedTrigger: TriggerDefinition<
77
+ z.infer<typeof maintenanceUpdatedPayloadSchema>
78
+ > = {
79
+ id: "updated",
80
+ displayName: "Maintenance Updated",
81
+ description: "Fired when a maintenance is updated or closed",
82
+ category: "Maintenance",
83
+ icon: "Wrench",
84
+ payloadSchema: maintenanceUpdatedPayloadSchema,
85
+ hook: maintenanceHooks.maintenanceUpdated,
86
+ contextKey: (p) => p.maintenanceId,
87
+ };
88
+
89
+ export const maintenanceTriggers: TriggerDefinition<unknown>[] = [
90
+ maintenanceCreatedTrigger as TriggerDefinition<unknown>,
91
+ maintenanceUpdatedTrigger as TriggerDefinition<unknown>,
92
+ ];
93
+
94
+ // ─── Action configs ────────────────────────────────────────────────────
95
+
96
+ const createConfigSchema = z.object({
97
+ title: z.string().min(1),
98
+ description: z.string().optional(),
99
+ systemIds: z.array(z.string()).min(1),
100
+ startAt: z
101
+ .string()
102
+ .describe("ISO timestamp when the window starts"),
103
+ endAt: z.string().describe("ISO timestamp when the window ends"),
104
+ suppressNotifications: z.boolean().optional().default(false),
105
+ });
106
+
107
+ const updateConfigSchema = z.object({
108
+ maintenanceId: z.string().min(1),
109
+ title: z.string().optional(),
110
+ description: z.string().optional(),
111
+ systemIds: z.array(z.string()).optional(),
112
+ startAt: z.string().optional(),
113
+ endAt: z.string().optional(),
114
+ suppressNotifications: z.boolean().optional(),
115
+ });
116
+
117
+ const addUpdateConfigSchema = z.object({
118
+ maintenanceId: z.string().min(1),
119
+ message: z.string().min(1),
120
+ statusChange: MaintenanceStatusEnum.optional(),
121
+ });
122
+
123
+ const setSystemConfigSchema = z.object({
124
+ systemId: z.string().min(1),
125
+ title: z.string().optional(),
126
+ description: z.string().optional(),
127
+ durationMinutes: z
128
+ .number()
129
+ .int()
130
+ .min(1)
131
+ .max(30 * 24 * 60)
132
+ .describe("Window length in minutes (1 min – 30 days)"),
133
+ suppressNotifications: z.boolean().optional().default(false),
134
+ });
135
+
136
+ const clearSystemConfigSchema = z.object({
137
+ systemId: z.string().min(1),
138
+ message: z
139
+ .string()
140
+ .optional()
141
+ .describe(
142
+ "Note appended to the maintenance update log; defaults to a generic 'cleared by automation' message",
143
+ ),
144
+ });
145
+
146
+ // ─── Artifact ──────────────────────────────────────────────────────────
147
+
148
+ const maintenanceArtifactSchema = z.object({
149
+ maintenanceId: z.string(),
150
+ status: MaintenanceStatusEnum,
151
+ systemIds: z.array(z.string()),
152
+ startAt: z.string(),
153
+ endAt: z.string(),
154
+ });
155
+
156
+ export type MaintenanceArtifact = z.infer<typeof maintenanceArtifactSchema>;
157
+
158
+ export const maintenanceArtifactType = {
159
+ id: "window",
160
+ displayName: "Maintenance Window",
161
+ description: "Maintenance row touched (created/updated/closed) by an automation",
162
+ schema: maintenanceArtifactSchema,
163
+ } as const;
164
+
165
+ // ─── Action factory ────────────────────────────────────────────────────
166
+
167
+ export interface MaintenanceActionDeps {
168
+ service: MaintenanceService;
169
+ emitHook: <T>(hook: Hook<T>, payload: T) => Promise<void>;
170
+ /**
171
+ * Override for `Date.now()`. Only used by `set_system` to compute
172
+ * `endAt = now + durationMinutes`. Tests inject a fixed clock; the
173
+ * default uses the real wall clock.
174
+ */
175
+ now?: () => Date;
176
+ }
177
+
178
+ function toArtifact(maint: {
179
+ id: string;
180
+ status: MaintenanceStatus;
181
+ systemIds: string[];
182
+ startAt: Date | string;
183
+ endAt: Date | string;
184
+ }): MaintenanceArtifact {
185
+ return {
186
+ maintenanceId: maint.id,
187
+ status: maint.status,
188
+ systemIds: maint.systemIds,
189
+ startAt:
190
+ maint.startAt instanceof Date ? maint.startAt.toISOString() : maint.startAt,
191
+ endAt: maint.endAt instanceof Date ? maint.endAt.toISOString() : maint.endAt,
192
+ };
193
+ }
194
+
195
+ export function createMaintenanceActions(
196
+ deps: MaintenanceActionDeps,
197
+ ): ActionDefinition<unknown, unknown>[] {
198
+ const now = deps.now ?? (() => new Date());
199
+
200
+ const createAction: ActionDefinition<
201
+ z.infer<typeof createConfigSchema>,
202
+ MaintenanceArtifact
203
+ > = {
204
+ id: "create",
205
+ displayName: "Schedule Maintenance",
206
+ description: "Schedule a new maintenance window",
207
+ category: "Maintenance",
208
+ icon: "Wrench",
209
+ config: new Versioned({ version: 1, schema: createConfigSchema }),
210
+ produces: "maintenance.window",
211
+ execute: async ({ config, logger }) => {
212
+ const created = await deps.service.createMaintenance({
213
+ title: config.title,
214
+ description: config.description,
215
+ systemIds: config.systemIds,
216
+ startAt: new Date(config.startAt),
217
+ endAt: new Date(config.endAt),
218
+ suppressNotifications: config.suppressNotifications,
219
+ });
220
+ const artifact = toArtifact(created);
221
+ await deps.emitHook(maintenanceHooks.maintenanceCreated, {
222
+ maintenanceId: created.id,
223
+ systemIds: created.systemIds,
224
+ title: created.title,
225
+ description: created.description,
226
+ status: created.status,
227
+ startAt: artifact.startAt,
228
+ endAt: artifact.endAt,
229
+ });
230
+ logger.info(`Automation scheduled maintenance ${created.id}`);
231
+ return {
232
+ success: true,
233
+ externalId: created.id,
234
+ artifact,
235
+ };
236
+ },
237
+ };
238
+
239
+ const updateAction: ActionDefinition<
240
+ z.infer<typeof updateConfigSchema>,
241
+ MaintenanceArtifact
242
+ > = {
243
+ id: "update",
244
+ displayName: "Update Maintenance",
245
+ description: "Update an existing maintenance window's metadata or schedule",
246
+ category: "Maintenance",
247
+ icon: "Wrench",
248
+ config: new Versioned({ version: 1, schema: updateConfigSchema }),
249
+ produces: "maintenance.window",
250
+ execute: async ({ config, logger }) => {
251
+ const updated = await deps.service.updateMaintenance({
252
+ id: config.maintenanceId,
253
+ title: config.title,
254
+ description: config.description,
255
+ systemIds: config.systemIds,
256
+ startAt: config.startAt ? new Date(config.startAt) : undefined,
257
+ endAt: config.endAt ? new Date(config.endAt) : undefined,
258
+ suppressNotifications: config.suppressNotifications,
259
+ });
260
+ if (!updated) {
261
+ return {
262
+ success: false,
263
+ error: `Maintenance not found: ${config.maintenanceId}`,
264
+ };
265
+ }
266
+ const artifact = toArtifact(updated);
267
+ await deps.emitHook(maintenanceHooks.maintenanceUpdated, {
268
+ maintenanceId: updated.id,
269
+ systemIds: updated.systemIds,
270
+ title: updated.title,
271
+ description: updated.description,
272
+ status: updated.status,
273
+ startAt: artifact.startAt,
274
+ endAt: artifact.endAt,
275
+ action: "updated",
276
+ });
277
+ logger.info(`Automation updated maintenance ${updated.id}`);
278
+ return { success: true, externalId: updated.id, artifact };
279
+ },
280
+ };
281
+
282
+ const addUpdateAction: ActionDefinition<
283
+ z.infer<typeof addUpdateConfigSchema>,
284
+ MaintenanceArtifact
285
+ > = {
286
+ id: "add_update",
287
+ displayName: "Add Maintenance Update",
288
+ description: "Append a status-update note to a maintenance window",
289
+ category: "Maintenance",
290
+ icon: "MessageSquarePlus",
291
+ config: new Versioned({ version: 1, schema: addUpdateConfigSchema }),
292
+ produces: "maintenance.window",
293
+ execute: async ({ config, logger }) => {
294
+ await deps.service.addUpdate({
295
+ maintenanceId: config.maintenanceId,
296
+ message: config.message,
297
+ statusChange: config.statusChange,
298
+ });
299
+ // Re-fetch so we surface the latest window state to the next
300
+ // step + so the emitted hook payload matches the (now-updated)
301
+ // row.
302
+ const refreshed = await deps.service.getMaintenance(config.maintenanceId);
303
+ if (!refreshed) {
304
+ return {
305
+ success: false,
306
+ error: `Maintenance ${config.maintenanceId} not found after update`,
307
+ };
308
+ }
309
+ const artifact = toArtifact(refreshed);
310
+ await deps.emitHook(maintenanceHooks.maintenanceUpdated, {
311
+ maintenanceId: refreshed.id,
312
+ systemIds: refreshed.systemIds,
313
+ title: refreshed.title,
314
+ description: refreshed.description,
315
+ status: refreshed.status,
316
+ startAt: artifact.startAt,
317
+ endAt: artifact.endAt,
318
+ action: config.statusChange === "completed" ? "closed" : "updated",
319
+ });
320
+ logger.info(`Automation added update to maintenance ${refreshed.id}`);
321
+ return { success: true, externalId: refreshed.id, artifact };
322
+ },
323
+ };
324
+
325
+ const setSystemAction: ActionDefinition<
326
+ z.infer<typeof setSystemConfigSchema>,
327
+ MaintenanceArtifact
328
+ > = {
329
+ id: "set_system",
330
+ displayName: "Set System Maintenance",
331
+ description:
332
+ "Schedule a maintenance window covering a single system, starting now and lasting `durationMinutes` minutes.",
333
+ category: "Maintenance",
334
+ icon: "Wrench",
335
+ config: new Versioned({ version: 1, schema: setSystemConfigSchema }),
336
+ produces: "maintenance.window",
337
+ execute: async ({ config, logger }) => {
338
+ const startAt = now();
339
+ const endAt = new Date(
340
+ startAt.getTime() + config.durationMinutes * 60_000,
341
+ );
342
+ const created = await deps.service.createMaintenance({
343
+ title: config.title ?? `Automation maintenance (${config.systemId})`,
344
+ description: config.description,
345
+ systemIds: [config.systemId],
346
+ startAt,
347
+ endAt,
348
+ suppressNotifications: config.suppressNotifications,
349
+ });
350
+ const artifact = toArtifact(created);
351
+ await deps.emitHook(maintenanceHooks.maintenanceCreated, {
352
+ maintenanceId: created.id,
353
+ systemIds: created.systemIds,
354
+ title: created.title,
355
+ description: created.description,
356
+ status: created.status,
357
+ startAt: artifact.startAt,
358
+ endAt: artifact.endAt,
359
+ });
360
+ logger.info(
361
+ `Automation parked system ${config.systemId} via maintenance ${created.id}`,
362
+ );
363
+ return { success: true, externalId: created.id, artifact };
364
+ },
365
+ };
366
+
367
+ interface ClearSystemArtifact {
368
+ systemId: string;
369
+ closedMaintenanceIds: string[];
370
+ }
371
+
372
+ const clearSystemAction: ActionDefinition<
373
+ z.infer<typeof clearSystemConfigSchema>,
374
+ ClearSystemArtifact
375
+ > = {
376
+ id: "clear_system",
377
+ displayName: "Clear System Maintenance",
378
+ description:
379
+ "Close every active or scheduled maintenance window that covers this system.",
380
+ category: "Maintenance",
381
+ icon: "Wrench",
382
+ config: new Versioned({ version: 1, schema: clearSystemConfigSchema }),
383
+ produces: "maintenance.window",
384
+ execute: async ({ config, logger }) => {
385
+ const active = await deps.service.getMaintenancesForSystem(config.systemId);
386
+ const closedIds: string[] = [];
387
+ const message = config.message ?? "Cleared by automation";
388
+ for (const window of active) {
389
+ const closed = await deps.service.closeMaintenance(window.id, message);
390
+ if (!closed) continue;
391
+ closedIds.push(closed.id);
392
+ const artifact = toArtifact(closed);
393
+ await deps.emitHook(maintenanceHooks.maintenanceUpdated, {
394
+ maintenanceId: closed.id,
395
+ systemIds: closed.systemIds,
396
+ title: closed.title,
397
+ description: closed.description,
398
+ status: closed.status,
399
+ startAt: artifact.startAt,
400
+ endAt: artifact.endAt,
401
+ action: "closed",
402
+ });
403
+ }
404
+ logger.info(
405
+ `Automation cleared maintenance for system ${config.systemId} (${closedIds.length} window(s))`,
406
+ );
407
+ return {
408
+ success: true,
409
+ externalId: config.systemId,
410
+ artifact: { systemId: config.systemId, closedMaintenanceIds: closedIds },
411
+ };
412
+ },
413
+ };
414
+
415
+ return [
416
+ createAction as ActionDefinition<unknown, unknown>,
417
+ updateAction as ActionDefinition<unknown, unknown>,
418
+ addUpdateAction as ActionDefinition<unknown, unknown>,
419
+ setSystemAction as ActionDefinition<unknown, unknown>,
420
+ clearSystemAction as ActionDefinition<unknown, unknown>,
421
+ ];
422
+ }
package/src/hooks.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { createHook } from "@checkstack/backend-api";
2
+ import type { MaintenanceStatus } from "@checkstack/maintenance-common";
2
3
 
3
4
  /**
4
5
  * Maintenance hooks for cross-plugin communication.
5
6
  * Other plugins can subscribe to these hooks to react to maintenance lifecycle events.
6
7
  * These hooks are registered as integration events for webhook subscriptions.
8
+ *
9
+ * `status` carries the canonical `MaintenanceStatus` enum value, so
10
+ * automation triggers built on these hooks can offer the known values
11
+ * for `==` comparisons in the editor.
7
12
  */
8
13
  export const maintenanceHooks = {
9
14
  /**
@@ -15,7 +20,7 @@ export const maintenanceHooks = {
15
20
  systemIds: string[];
16
21
  title: string;
17
22
  description?: string;
18
- status: string;
23
+ status: MaintenanceStatus;
19
24
  startAt: string;
20
25
  endAt: string;
21
26
  }>("maintenance.created"),
@@ -29,7 +34,7 @@ export const maintenanceHooks = {
29
34
  systemIds: string[];
30
35
  title: string;
31
36
  description?: string;
32
- status: string;
37
+ status: MaintenanceStatus;
33
38
  startAt: string;
34
39
  endAt: string;
35
40
  action: "updated" | "closed";
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as schema from "./schema";
2
2
  import type { SafeDatabase } from "@checkstack/backend-api";
3
- import { z } from "zod";
4
3
  import {
5
4
  maintenanceAccessRules,
6
5
  maintenanceAccess,
@@ -13,7 +12,11 @@ import {
13
12
  } from "@checkstack/maintenance-common";
14
13
 
15
14
  import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
16
- import { integrationEventExtensionPoint } from "@checkstack/integration-backend";
15
+ import {
16
+ automationActionExtensionPoint,
17
+ automationArtifactTypeExtensionPoint,
18
+ automationTriggerExtensionPoint,
19
+ } from "@checkstack/automation-backend";
17
20
  import {
18
21
  NotificationApi,
19
22
  specToRegistration,
@@ -24,33 +27,12 @@ import { CatalogApi } from "@checkstack/catalog-common";
24
27
  import { AuthApi } from "@checkstack/auth-common";
25
28
  import { registerSearchProvider } from "@checkstack/command-backend";
26
29
  import { resolveRoute, type InferClient } from "@checkstack/common";
27
- import { maintenanceHooks } from "./hooks";
28
30
  import { createMaintenanceCache } from "./cache";
29
-
30
- // =============================================================================
31
- // Integration Event Payload Schemas
32
- // =============================================================================
33
-
34
- const maintenanceCreatedPayloadSchema = z.object({
35
- maintenanceId: z.string(),
36
- systemIds: z.array(z.string()),
37
- title: z.string(),
38
- description: z.string().optional(),
39
- status: z.string(),
40
- startAt: z.string(),
41
- endAt: z.string(),
42
- });
43
-
44
- const maintenanceUpdatedPayloadSchema = z.object({
45
- maintenanceId: z.string(),
46
- systemIds: z.array(z.string()),
47
- title: z.string(),
48
- description: z.string().optional(),
49
- status: z.string(),
50
- startAt: z.string(),
51
- endAt: z.string(),
52
- action: z.enum(["updated", "closed"]),
53
- });
31
+ import {
32
+ createMaintenanceActions,
33
+ maintenanceArtifactType,
34
+ maintenanceTriggers,
35
+ } from "./automations";
54
36
 
55
37
  // Queue and job constants
56
38
  const STATUS_TRANSITION_QUEUE = "maintenance-status-transitions";
@@ -70,32 +52,19 @@ export default createBackendPlugin({
70
52
  maintenanceGroupSubscription,
71
53
  ]);
72
54
 
73
- // Register hooks as integration events
74
- const integrationEvents = env.getExtensionPoint(
75
- integrationEventExtensionPoint,
76
- );
77
-
78
- integrationEvents.registerEvent(
79
- {
80
- hook: maintenanceHooks.maintenanceCreated,
81
- displayName: "Maintenance Created",
82
- description: "Fired when a new maintenance is scheduled",
83
- category: "Maintenance",
84
- payloadSchema: maintenanceCreatedPayloadSchema,
85
- },
86
- pluginMetadata,
87
- );
88
-
89
- integrationEvents.registerEvent(
90
- {
91
- hook: maintenanceHooks.maintenanceUpdated,
92
- displayName: "Maintenance Updated",
93
- description: "Fired when a maintenance is updated or closed",
94
- category: "Maintenance",
95
- payloadSchema: maintenanceUpdatedPayloadSchema,
96
- },
97
- pluginMetadata,
55
+ // ─── Automation Platform: triggers + artifact type ─────────────────
56
+ // Buffered behind the extension point until automation-backend's
57
+ // register() runs. Actions are wired in afterPluginsReady so
58
+ // `emitHook` is available — see below.
59
+ const automationTriggers = env.getExtensionPoint(
60
+ automationTriggerExtensionPoint,
98
61
  );
62
+ for (const trigger of maintenanceTriggers) {
63
+ automationTriggers.registerTrigger(trigger, pluginMetadata);
64
+ }
65
+ env
66
+ .getExtensionPoint(automationArtifactTypeExtensionPoint)
67
+ .registerArtifactType(maintenanceArtifactType, pluginMetadata);
99
68
 
100
69
  // Store service reference for afterPluginsReady
101
70
  let maintenanceService: MaintenanceService;
@@ -172,7 +141,18 @@ export default createBackendPlugin({
172
141
 
173
142
  logger.debug("✅ Maintenance Backend initialized.");
174
143
  },
175
- afterPluginsReady: async ({ queueManager, logger }) => {
144
+ afterPluginsReady: async ({ queueManager, logger, emitHook }) => {
145
+ // Register automation actions now that `emitHook` is available.
146
+ const automationActions = env.getExtensionPoint(
147
+ automationActionExtensionPoint,
148
+ );
149
+ for (const action of createMaintenanceActions({
150
+ service: maintenanceService,
151
+ emitHook,
152
+ })) {
153
+ automationActions.registerAction(action, pluginMetadata);
154
+ }
155
+
176
156
  // Notification subscription specs. Per-resource group lifecycle
177
157
  // is platform-managed by notification-backend — maintenance just
178
158
  // declares the specs.
package/tsconfig.json CHANGED
@@ -7,6 +7,9 @@
7
7
  {
8
8
  "path": "../auth-common"
9
9
  },
10
+ {
11
+ "path": "../automation-backend"
12
+ },
10
13
  {
11
14
  "path": "../backend-api"
12
15
  },
@@ -31,12 +34,6 @@
31
34
  {
32
35
  "path": "../drizzle-helper"
33
36
  },
34
- {
35
- "path": "../integration-backend"
36
- },
37
- {
38
- "path": "../integration-common"
39
- },
40
37
  {
41
38
  "path": "../maintenance-common"
42
39
  },