@cat-factory/orchestration 0.6.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/LICENSE +21 -0
- package/dist/container.d.ts +460 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +657 -0
- package/dist/container.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/board/BoardService.d.ts +125 -0
- package/dist/modules/board/BoardService.d.ts.map +1 -0
- package/dist/modules/board/BoardService.js +496 -0
- package/dist/modules/board/BoardService.js.map +1 -0
- package/dist/modules/board/board.logic.d.ts +17 -0
- package/dist/modules/board/board.logic.d.ts.map +1 -0
- package/dist/modules/board/board.logic.js +51 -0
- package/dist/modules/board/board.logic.js.map +1 -0
- package/dist/modules/boardScan/BoardScanService.d.ts +35 -0
- package/dist/modules/boardScan/BoardScanService.d.ts.map +1 -0
- package/dist/modules/boardScan/BoardScanService.js +91 -0
- package/dist/modules/boardScan/BoardScanService.js.map +1 -0
- package/dist/modules/boardScan/board-scan.logic.d.ts +10 -0
- package/dist/modules/boardScan/board-scan.logic.d.ts.map +1 -0
- package/dist/modules/boardScan/board-scan.logic.js +26 -0
- package/dist/modules/boardScan/board-scan.logic.js.map +1 -0
- package/dist/modules/bootstrap/BootstrapService.d.ts +114 -0
- package/dist/modules/bootstrap/BootstrapService.d.ts.map +1 -0
- package/dist/modules/bootstrap/BootstrapService.js +516 -0
- package/dist/modules/bootstrap/BootstrapService.js.map +1 -0
- package/dist/modules/clarity/ClarityReviewService.d.ts +48 -0
- package/dist/modules/clarity/ClarityReviewService.d.ts.map +1 -0
- package/dist/modules/clarity/ClarityReviewService.js +63 -0
- package/dist/modules/clarity/ClarityReviewService.js.map +1 -0
- package/dist/modules/clarity/clarity.logic.d.ts +36 -0
- package/dist/modules/clarity/clarity.logic.d.ts.map +1 -0
- package/dist/modules/clarity/clarity.logic.js +98 -0
- package/dist/modules/clarity/clarity.logic.js.map +1 -0
- package/dist/modules/estimation/estimate.logic.d.ts +11 -0
- package/dist/modules/estimation/estimate.logic.d.ts.map +1 -0
- package/dist/modules/estimation/estimate.logic.js +37 -0
- package/dist/modules/estimation/estimate.logic.js.map +1 -0
- package/dist/modules/execution/AgentContextBuilder.d.ts +114 -0
- package/dist/modules/execution/AgentContextBuilder.d.ts.map +1 -0
- package/dist/modules/execution/AgentContextBuilder.js +316 -0
- package/dist/modules/execution/AgentContextBuilder.js.map +1 -0
- package/dist/modules/execution/CompanionController.d.ts +60 -0
- package/dist/modules/execution/CompanionController.d.ts.map +1 -0
- package/dist/modules/execution/CompanionController.js +216 -0
- package/dist/modules/execution/CompanionController.js.map +1 -0
- package/dist/modules/execution/ExecutionService.d.ts +874 -0
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -0
- package/dist/modules/execution/ExecutionService.js +2921 -0
- package/dist/modules/execution/ExecutionService.js.map +1 -0
- package/dist/modules/execution/MergeResolver.d.ts +34 -0
- package/dist/modules/execution/MergeResolver.d.ts.map +1 -0
- package/dist/modules/execution/MergeResolver.js +81 -0
- package/dist/modules/execution/MergeResolver.js.map +1 -0
- package/dist/modules/execution/ReviewGateController.d.ts +163 -0
- package/dist/modules/execution/ReviewGateController.d.ts.map +1 -0
- package/dist/modules/execution/ReviewGateController.js +251 -0
- package/dist/modules/execution/ReviewGateController.js.map +1 -0
- package/dist/modules/execution/TesterController.d.ts +61 -0
- package/dist/modules/execution/TesterController.d.ts.map +1 -0
- package/dist/modules/execution/TesterController.js +215 -0
- package/dist/modules/execution/TesterController.js.map +1 -0
- package/dist/modules/execution/advance.d.ts +84 -0
- package/dist/modules/execution/advance.d.ts.map +1 -0
- package/dist/modules/execution/advance.js +2 -0
- package/dist/modules/execution/advance.js.map +1 -0
- package/dist/modules/execution/artifact-review.logic.d.ts +25 -0
- package/dist/modules/execution/artifact-review.logic.d.ts.map +1 -0
- package/dist/modules/execution/artifact-review.logic.js +39 -0
- package/dist/modules/execution/artifact-review.logic.js.map +1 -0
- package/dist/modules/execution/ci.logic.d.ts +101 -0
- package/dist/modules/execution/ci.logic.d.ts.map +1 -0
- package/dist/modules/execution/ci.logic.js +117 -0
- package/dist/modules/execution/ci.logic.js.map +1 -0
- package/dist/modules/execution/drive.d.ts +47 -0
- package/dist/modules/execution/drive.d.ts.map +1 -0
- package/dist/modules/execution/drive.js +112 -0
- package/dist/modules/execution/drive.js.map +1 -0
- package/dist/modules/execution/gates.d.ts +97 -0
- package/dist/modules/execution/gates.d.ts.map +1 -0
- package/dist/modules/execution/gates.js +2 -0
- package/dist/modules/execution/gates.js.map +1 -0
- package/dist/modules/execution/individualVendors.logic.d.ts +22 -0
- package/dist/modules/execution/individualVendors.logic.d.ts.map +1 -0
- package/dist/modules/execution/individualVendors.logic.js +33 -0
- package/dist/modules/execution/individualVendors.logic.js.map +1 -0
- package/dist/modules/execution/job.logic.d.ts +52 -0
- package/dist/modules/execution/job.logic.d.ts.map +1 -0
- package/dist/modules/execution/job.logic.js +56 -0
- package/dist/modules/execution/job.logic.js.map +1 -0
- package/dist/modules/execution/release.logic.d.ts +43 -0
- package/dist/modules/execution/release.logic.d.ts.map +1 -0
- package/dist/modules/execution/release.logic.js +49 -0
- package/dist/modules/execution/release.logic.js.map +1 -0
- package/dist/modules/execution/retry.logic.d.ts +40 -0
- package/dist/modules/execution/retry.logic.d.ts.map +1 -0
- package/dist/modules/execution/retry.logic.js +83 -0
- package/dist/modules/execution/retry.logic.js.map +1 -0
- package/dist/modules/execution/stepGating.logic.d.ts +15 -0
- package/dist/modules/execution/stepGating.logic.d.ts.map +1 -0
- package/dist/modules/execution/stepGating.logic.js +29 -0
- package/dist/modules/execution/stepGating.logic.js.map +1 -0
- package/dist/modules/execution/stepResolvers.d.ts +41 -0
- package/dist/modules/execution/stepResolvers.d.ts.map +1 -0
- package/dist/modules/execution/stepResolvers.js +2 -0
- package/dist/modules/execution/stepResolvers.js.map +1 -0
- package/dist/modules/execution/tester-infra.logic.d.ts +42 -0
- package/dist/modules/execution/tester-infra.logic.d.ts.map +1 -0
- package/dist/modules/execution/tester-infra.logic.js +46 -0
- package/dist/modules/execution/tester-infra.logic.js.map +1 -0
- package/dist/modules/merge/MergePresetService.d.ts +32 -0
- package/dist/modules/merge/MergePresetService.d.ts.map +1 -0
- package/dist/modules/merge/MergePresetService.js +109 -0
- package/dist/modules/merge/MergePresetService.js.map +1 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.d.ts +22 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.d.ts.map +1 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.js +28 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.js.map +1 -0
- package/dist/modules/notifications/NotificationService.d.ts +74 -0
- package/dist/modules/notifications/NotificationService.d.ts.map +1 -0
- package/dist/modules/notifications/NotificationService.js +131 -0
- package/dist/modules/notifications/NotificationService.js.map +1 -0
- package/dist/modules/observability/LlmObservabilityService.d.ts +121 -0
- package/dist/modules/observability/LlmObservabilityService.d.ts.map +1 -0
- package/dist/modules/observability/LlmObservabilityService.js +140 -0
- package/dist/modules/observability/LlmObservabilityService.js.map +1 -0
- package/dist/modules/observability/observability.logic.d.ts +57 -0
- package/dist/modules/observability/observability.logic.d.ts.map +1 -0
- package/dist/modules/observability/observability.logic.js +186 -0
- package/dist/modules/observability/observability.logic.js.map +1 -0
- package/dist/modules/pipelines/PipelineService.d.ts +54 -0
- package/dist/modules/pipelines/PipelineService.d.ts.map +1 -0
- package/dist/modules/pipelines/PipelineService.js +226 -0
- package/dist/modules/pipelines/PipelineService.js.map +1 -0
- package/dist/modules/pipelines/pipelineShape.d.ts +53 -0
- package/dist/modules/pipelines/pipelineShape.d.ts.map +1 -0
- package/dist/modules/pipelines/pipelineShape.js +74 -0
- package/dist/modules/pipelines/pipelineShape.js.map +1 -0
- package/dist/modules/recurring/RecurringPipelineService.d.ts +76 -0
- package/dist/modules/recurring/RecurringPipelineService.d.ts.map +1 -0
- package/dist/modules/recurring/RecurringPipelineService.js +295 -0
- package/dist/modules/recurring/RecurringPipelineService.js.map +1 -0
- package/dist/modules/recurring/TrackerSettingsService.d.ts +16 -0
- package/dist/modules/recurring/TrackerSettingsService.d.ts.map +1 -0
- package/dist/modules/recurring/TrackerSettingsService.js +30 -0
- package/dist/modules/recurring/TrackerSettingsService.js.map +1 -0
- package/dist/modules/recurring/schedule.logic.d.ts +14 -0
- package/dist/modules/recurring/schedule.logic.d.ts.map +1 -0
- package/dist/modules/recurring/schedule.logic.js +85 -0
- package/dist/modules/recurring/schedule.logic.js.map +1 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.d.ts +38 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.d.ts.map +1 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.js +96 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.js.map +1 -0
- package/dist/modules/requirements/RequirementReviewService.d.ts +48 -0
- package/dist/modules/requirements/RequirementReviewService.d.ts.map +1 -0
- package/dist/modules/requirements/RequirementReviewService.js +83 -0
- package/dist/modules/requirements/RequirementReviewService.js.map +1 -0
- package/dist/modules/requirements/requirements.logic.d.ts +93 -0
- package/dist/modules/requirements/requirements.logic.d.ts.map +1 -0
- package/dist/modules/requirements/requirements.logic.js +203 -0
- package/dist/modules/requirements/requirements.logic.js.map +1 -0
- package/dist/modules/review/IterativeReviewService.d.ts +175 -0
- package/dist/modules/review/IterativeReviewService.d.ts.map +1 -0
- package/dist/modules/review/IterativeReviewService.js +327 -0
- package/dist/modules/review/IterativeReviewService.js.map +1 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.d.ts +20 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.d.ts.map +1 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.js +26 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.js.map +1 -0
- package/dist/modules/services/ServiceMountService.d.ts +48 -0
- package/dist/modules/services/ServiceMountService.d.ts.map +1 -0
- package/dist/modules/services/ServiceMountService.js +90 -0
- package/dist/modules/services/ServiceMountService.js.map +1 -0
- package/dist/modules/settings/WorkspaceSettingsService.d.ts +22 -0
- package/dist/modules/settings/WorkspaceSettingsService.d.ts.map +1 -0
- package/dist/modules/settings/WorkspaceSettingsService.js +50 -0
- package/dist/modules/settings/WorkspaceSettingsService.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { assertFound, ConflictError, requireWorkspace } from '@cat-factory/kernel';
|
|
2
|
+
import { computeNextRun } from './schedule.logic.js';
|
|
3
|
+
/** Default seed descriptions for the canned recurring templates. */
|
|
4
|
+
const TEMPLATE_DESCRIPTIONS = {
|
|
5
|
+
'dep-update': 'Recurring dependency-update pass: bring this service’s dependencies up to the latest compatible versions, update lockfiles, and make sure the build and tests still pass.',
|
|
6
|
+
'tech-debt': 'Recurring tech-debt remediation pass: analyse this service for the highest-value technical debt, file a tracking ticket, then implement the fix with tests.',
|
|
7
|
+
custom: '',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Manages a workspace's recurring pipelines. Each schedule owns one reused
|
|
11
|
+
* on-board block (a task leaf inside the chosen service frame); the cron sweeper
|
|
12
|
+
* calls {@link runDue} to fire every due schedule by starting its pipeline against
|
|
13
|
+
* that block (skipping any whose block already has an active run), recording each
|
|
14
|
+
* fire in the run-history table the inspector reads.
|
|
15
|
+
*/
|
|
16
|
+
export class RecurringPipelineService {
|
|
17
|
+
schedules;
|
|
18
|
+
workspaceRepository;
|
|
19
|
+
pipelineRepository;
|
|
20
|
+
blockRepository;
|
|
21
|
+
executionRepository;
|
|
22
|
+
executionService;
|
|
23
|
+
idGenerator;
|
|
24
|
+
clock;
|
|
25
|
+
serviceRepository;
|
|
26
|
+
workspaceMountRepository;
|
|
27
|
+
constructor(deps) {
|
|
28
|
+
this.schedules = deps.pipelineScheduleRepository;
|
|
29
|
+
this.workspaceRepository = deps.workspaceRepository;
|
|
30
|
+
this.pipelineRepository = deps.pipelineRepository;
|
|
31
|
+
this.blockRepository = deps.blockRepository;
|
|
32
|
+
this.executionRepository = deps.executionRepository;
|
|
33
|
+
this.executionService = deps.executionService;
|
|
34
|
+
this.idGenerator = deps.idGenerator;
|
|
35
|
+
this.clock = deps.clock;
|
|
36
|
+
this.serviceRepository = deps.serviceRepository;
|
|
37
|
+
this.workspaceMountRepository = deps.workspaceMountRepository;
|
|
38
|
+
}
|
|
39
|
+
requireWorkspace(workspaceId) {
|
|
40
|
+
return requireWorkspace(this.workspaceRepository, workspaceId);
|
|
41
|
+
}
|
|
42
|
+
async list(workspaceId) {
|
|
43
|
+
await this.requireWorkspace(workspaceId);
|
|
44
|
+
// The workspace's own schedules (including legacy/seeded frames with no service) UNION
|
|
45
|
+
// the schedules of every service it mounts — so a shared service's schedules show on
|
|
46
|
+
// every board that mounts it. Dedup by id.
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const out = [];
|
|
49
|
+
const add = (schedule) => {
|
|
50
|
+
if (!seen.has(schedule.id)) {
|
|
51
|
+
seen.add(schedule.id);
|
|
52
|
+
out.push(schedule);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
for (const schedule of await this.schedules.list(workspaceId))
|
|
56
|
+
add(schedule);
|
|
57
|
+
if (this.workspaceMountRepository) {
|
|
58
|
+
const mounts = await this.workspaceMountRepository.listByWorkspace(workspaceId);
|
|
59
|
+
// One batched query for every mounted service's schedules (not one round-trip per mount).
|
|
60
|
+
for (const schedule of await this.schedules.listByServices(mounts.map((m) => m.serviceId))) {
|
|
61
|
+
add(schedule);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create a recurring pipeline on a service frame. Materialises the reused on-board
|
|
68
|
+
* block (a task leaf inside the frame), computes the first `nextRunAt`, and
|
|
69
|
+
* persists the schedule.
|
|
70
|
+
*/
|
|
71
|
+
async create(workspaceId, input) {
|
|
72
|
+
await this.requireWorkspace(workspaceId);
|
|
73
|
+
const frame = assertFound(await this.blockRepository.get(workspaceId, input.frameId), 'Block', input.frameId);
|
|
74
|
+
if (frame.level !== 'frame') {
|
|
75
|
+
throw new ConflictError('Recurring pipelines can only be attached to a service frame.');
|
|
76
|
+
}
|
|
77
|
+
assertFound(await this.pipelineRepository.get(workspaceId, input.pipelineId), 'Pipeline', input.pipelineId);
|
|
78
|
+
// The owning service (in-org sharing): the schedule + its reused block belong to the
|
|
79
|
+
// frame's service, so they render on — and are listed by — every workspace that mounts it.
|
|
80
|
+
const serviceId = this.serviceRepository
|
|
81
|
+
? ((await this.serviceRepository.getByFrameBlock(frame.id))?.id ?? null)
|
|
82
|
+
: null;
|
|
83
|
+
const now = this.clock.now();
|
|
84
|
+
const block = {
|
|
85
|
+
id: this.idGenerator.next('blk'),
|
|
86
|
+
title: input.name,
|
|
87
|
+
type: 'service',
|
|
88
|
+
// The user's own prompt when given, else the canned template seed.
|
|
89
|
+
description: input.description?.trim() || TEMPLATE_DESCRIPTIONS[input.template] || '',
|
|
90
|
+
position: { x: 24, y: 96 },
|
|
91
|
+
status: 'planned',
|
|
92
|
+
progress: 0,
|
|
93
|
+
dependsOn: [],
|
|
94
|
+
executionId: null,
|
|
95
|
+
level: 'task',
|
|
96
|
+
parentId: frame.id,
|
|
97
|
+
// A recurring schedule's reused on-board block is a recurring-type task.
|
|
98
|
+
taskType: 'recurring',
|
|
99
|
+
};
|
|
100
|
+
await this.blockRepository.insert(workspaceId, block, serviceId);
|
|
101
|
+
const schedule = {
|
|
102
|
+
id: this.idGenerator.next('sch'),
|
|
103
|
+
serviceId,
|
|
104
|
+
blockId: block.id,
|
|
105
|
+
frameId: frame.id,
|
|
106
|
+
pipelineId: input.pipelineId,
|
|
107
|
+
template: input.template,
|
|
108
|
+
name: input.name,
|
|
109
|
+
recurrence: input.recurrence,
|
|
110
|
+
enabled: input.enabled,
|
|
111
|
+
lastRunAt: null,
|
|
112
|
+
// First fire is one interval out, rolled into the allowed window.
|
|
113
|
+
nextRunAt: computeNextRun(now, input.recurrence),
|
|
114
|
+
createdAt: now,
|
|
115
|
+
};
|
|
116
|
+
await this.schedules.upsert(workspaceId, schedule);
|
|
117
|
+
return schedule;
|
|
118
|
+
}
|
|
119
|
+
async update(workspaceId, id, patch) {
|
|
120
|
+
await this.requireWorkspace(workspaceId);
|
|
121
|
+
const existing = assertFound(await this.schedules.get(workspaceId, id), 'Schedule', id);
|
|
122
|
+
if (patch.pipelineId !== undefined) {
|
|
123
|
+
assertFound(await this.pipelineRepository.get(workspaceId, patch.pipelineId), 'Pipeline', patch.pipelineId);
|
|
124
|
+
}
|
|
125
|
+
const recurrence = patch.recurrence ?? existing.recurrence;
|
|
126
|
+
const updated = {
|
|
127
|
+
...existing,
|
|
128
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
129
|
+
...(patch.pipelineId !== undefined ? { pipelineId: patch.pipelineId } : {}),
|
|
130
|
+
...(patch.recurrence !== undefined ? { recurrence } : {}),
|
|
131
|
+
...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
|
|
132
|
+
// Recomputing the next fire keeps a cadence change effective immediately.
|
|
133
|
+
...(patch.recurrence !== undefined
|
|
134
|
+
? { nextRunAt: computeNextRun(this.clock.now(), recurrence) }
|
|
135
|
+
: {}),
|
|
136
|
+
};
|
|
137
|
+
await this.schedules.upsert(workspaceId, updated);
|
|
138
|
+
if (patch.name !== undefined) {
|
|
139
|
+
await this.blockRepository.update(workspaceId, existing.blockId, { title: patch.name });
|
|
140
|
+
}
|
|
141
|
+
return updated;
|
|
142
|
+
}
|
|
143
|
+
/** Remove a schedule, its reused block, and its run history. */
|
|
144
|
+
async remove(workspaceId, id) {
|
|
145
|
+
await this.requireWorkspace(workspaceId);
|
|
146
|
+
const existing = assertFound(await this.schedules.get(workspaceId, id), 'Schedule', id);
|
|
147
|
+
await this.executionRepository.deleteByBlock(workspaceId, existing.blockId);
|
|
148
|
+
await this.blockRepository.deleteMany(workspaceId, [existing.blockId]);
|
|
149
|
+
await this.schedules.remove(workspaceId, id);
|
|
150
|
+
}
|
|
151
|
+
/** A schedule's run history (most recent first), with live status overlaid. */
|
|
152
|
+
async listRuns(workspaceId, id) {
|
|
153
|
+
await this.requireWorkspace(workspaceId);
|
|
154
|
+
const schedule = assertFound(await this.schedules.get(workspaceId, id), 'Schedule', id);
|
|
155
|
+
const runs = await this.schedules.listRuns(workspaceId, id);
|
|
156
|
+
// The most recent run's execution usually still exists (it is only replaced on
|
|
157
|
+
// the next fire); overlay its live status so the inspector reflects progress.
|
|
158
|
+
const live = await this.executionRepository.getByBlock(workspaceId, schedule.blockId);
|
|
159
|
+
if (!live)
|
|
160
|
+
return runs;
|
|
161
|
+
return runs.map((run) => run.executionId && run.executionId === live.id
|
|
162
|
+
? { ...run, ...this.deriveRunOutcome(live) }
|
|
163
|
+
: run);
|
|
164
|
+
}
|
|
165
|
+
/** Fire a schedule immediately (ignoring its cadence), if its block is free. */
|
|
166
|
+
async runNow(workspaceId, id) {
|
|
167
|
+
await this.requireWorkspace(workspaceId);
|
|
168
|
+
const schedule = assertFound(await this.schedules.get(workspaceId, id), 'Schedule', id);
|
|
169
|
+
await this.fire(workspaceId, schedule, { force: true });
|
|
170
|
+
return assertFound(await this.schedules.get(workspaceId, id), 'Schedule', id);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Fire every due schedule across all workspaces. The cron/interval sweepers call
|
|
174
|
+
* this; it skips any schedule whose block already has an active run. Returns the
|
|
175
|
+
* number of runs started (for logging).
|
|
176
|
+
*/
|
|
177
|
+
async runDue(now) {
|
|
178
|
+
const due = await this.schedules.listDue(now);
|
|
179
|
+
let fired = 0;
|
|
180
|
+
let skipped = 0;
|
|
181
|
+
for (const { workspaceId, schedule } of due) {
|
|
182
|
+
const started = await this.fire(workspaceId, schedule, { now });
|
|
183
|
+
if (started)
|
|
184
|
+
fired++;
|
|
185
|
+
else
|
|
186
|
+
skipped++;
|
|
187
|
+
}
|
|
188
|
+
return { fired, skipped };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Start a schedule's pipeline against its reused block. Finalises the prior run's
|
|
192
|
+
* history row (its execution is about to be replaced), records a new running row,
|
|
193
|
+
* and advances `lastRunAt`/`nextRunAt`. Returns false (without starting) when the
|
|
194
|
+
* block already has an active run.
|
|
195
|
+
*/
|
|
196
|
+
async fire(workspaceId, schedule, opts = {}) {
|
|
197
|
+
const now = opts.now ?? this.clock.now();
|
|
198
|
+
// Individual-usage subscriptions (Claude) require their owner to be present to unlock
|
|
199
|
+
// them per run, so they can never run on an unattended schedule. Refuse to fire and
|
|
200
|
+
// record a clear failure (the user must switch the block to an API-key or pooled
|
|
201
|
+
// coding-plan model) rather than starting a run that would fault at dispatch. Resolve
|
|
202
|
+
// the vendor set with the SAME precedence dispatch uses (block pin → workspace per-kind
|
|
203
|
+
// default), via the engine, so a block with no pin but an individual-usage workspace
|
|
204
|
+
// default is caught here too — not just an explicitly pinned one.
|
|
205
|
+
const scheduledBlock = await this.blockRepository.get(workspaceId, schedule.blockId);
|
|
206
|
+
const individualVendor = scheduledBlock
|
|
207
|
+
? ((await this.executionService.individualVendorsForBlock(workspaceId, schedule.blockId, schedule.pipelineId))[0] ?? null)
|
|
208
|
+
: null;
|
|
209
|
+
if (individualVendor) {
|
|
210
|
+
if (opts.force) {
|
|
211
|
+
throw new ConflictError(`This recurring pipeline targets an individual-usage ${individualVendor} model, which ` +
|
|
212
|
+
`cannot run on a schedule. Pick an API-key or coding-plan model.`);
|
|
213
|
+
}
|
|
214
|
+
await this.schedules.insertRun(workspaceId, {
|
|
215
|
+
id: this.idGenerator.next('schr'),
|
|
216
|
+
scheduleId: schedule.id,
|
|
217
|
+
executionId: null,
|
|
218
|
+
status: 'failed',
|
|
219
|
+
startedAt: now,
|
|
220
|
+
finishedAt: now,
|
|
221
|
+
outcome: `Individual-usage ${individualVendor} models cannot run on a recurring schedule.`,
|
|
222
|
+
});
|
|
223
|
+
await this.advanceCadence(workspaceId, schedule, now);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const prior = await this.executionRepository.getByBlock(workspaceId, schedule.blockId);
|
|
227
|
+
if (prior && (prior.status === 'running' || prior.status === 'paused')) {
|
|
228
|
+
if (opts.force) {
|
|
229
|
+
throw new ConflictError('This recurring pipeline already has a run in progress.');
|
|
230
|
+
}
|
|
231
|
+
// Don't overlap; leave nextRunAt so the sweeper retries next pass.
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
// Persist the prior (now terminal) run's outcome before start() deletes it.
|
|
235
|
+
if (prior) {
|
|
236
|
+
const runs = await this.schedules.listRuns(workspaceId, schedule.id);
|
|
237
|
+
const priorRun = runs.find((r) => r.executionId === prior.id);
|
|
238
|
+
if (priorRun) {
|
|
239
|
+
await this.schedules.updateRun(workspaceId, priorRun.id, this.deriveRunOutcome(prior));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let executionId = null;
|
|
243
|
+
try {
|
|
244
|
+
const instance = await this.executionService.start(workspaceId, schedule.blockId, schedule.pipelineId);
|
|
245
|
+
executionId = instance.id;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// Record the failed fire so the history shows it, then advance the cadence.
|
|
249
|
+
await this.schedules.insertRun(workspaceId, {
|
|
250
|
+
id: this.idGenerator.next('schr'),
|
|
251
|
+
scheduleId: schedule.id,
|
|
252
|
+
executionId: null,
|
|
253
|
+
status: 'failed',
|
|
254
|
+
startedAt: now,
|
|
255
|
+
finishedAt: now,
|
|
256
|
+
outcome: error instanceof Error ? error.message : 'Failed to start run.',
|
|
257
|
+
});
|
|
258
|
+
await this.advanceCadence(workspaceId, schedule, now);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
await this.schedules.insertRun(workspaceId, {
|
|
262
|
+
id: this.idGenerator.next('schr'),
|
|
263
|
+
scheduleId: schedule.id,
|
|
264
|
+
executionId,
|
|
265
|
+
status: 'running',
|
|
266
|
+
startedAt: now,
|
|
267
|
+
finishedAt: null,
|
|
268
|
+
outcome: null,
|
|
269
|
+
});
|
|
270
|
+
await this.advanceCadence(workspaceId, schedule, now);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
async advanceCadence(workspaceId, schedule, now) {
|
|
274
|
+
await this.schedules.upsert(workspaceId, {
|
|
275
|
+
...schedule,
|
|
276
|
+
lastRunAt: now,
|
|
277
|
+
nextRunAt: computeNextRun(now, schedule.recurrence),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/** Map an execution's state to a history-row status + short outcome. */
|
|
281
|
+
deriveRunOutcome(instance) {
|
|
282
|
+
if (instance.status === 'done') {
|
|
283
|
+
return { status: 'done', finishedAt: this.clock.now(), outcome: 'completed' };
|
|
284
|
+
}
|
|
285
|
+
if (instance.status === 'failed') {
|
|
286
|
+
return {
|
|
287
|
+
status: 'failed',
|
|
288
|
+
finishedAt: this.clock.now(),
|
|
289
|
+
outcome: instance.failure?.message ?? 'failed',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return { status: 'running', finishedAt: null, outcome: null };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=RecurringPipelineService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecurringPipelineService.js","sourceRoot":"","sources":["../../../src/modules/recurring/RecurringPipelineService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Clock, PutTrackerSettingsInput, TrackerSettings, TrackerSettingsRepository, WorkspaceRepository } from '@cat-factory/kernel';
|
|
2
|
+
export interface TrackerSettingsServiceDependencies {
|
|
3
|
+
trackerSettingsRepository: TrackerSettingsRepository;
|
|
4
|
+
workspaceRepository: WorkspaceRepository;
|
|
5
|
+
clock: Clock;
|
|
6
|
+
}
|
|
7
|
+
/** Read/write a workspace's issue-tracker selection (one row per workspace). */
|
|
8
|
+
export declare class TrackerSettingsService {
|
|
9
|
+
private readonly repo;
|
|
10
|
+
private readonly workspaceRepository;
|
|
11
|
+
private readonly clock;
|
|
12
|
+
constructor(deps: TrackerSettingsServiceDependencies);
|
|
13
|
+
get(workspaceId: string): Promise<TrackerSettings>;
|
|
14
|
+
put(workspaceId: string, input: PutTrackerSettingsInput): Promise<TrackerSettings>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=TrackerSettingsService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TrackerSettingsService.d.ts","sourceRoot":"","sources":["../../../src/modules/recurring/TrackerSettingsService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,uBAAuB,EACvB,eAAe,EACf,yBAAyB,EACzB,mBAAmB,EACpB,MAAM,qBAAqB,CAAA;AAG5B,MAAM,WAAW,kCAAkC;IACjD,yBAAyB,EAAE,yBAAyB,CAAA;IACpD,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,KAAK,EAAE,KAAK,CAAA;CACb;AAKD,gFAAgF;AAChF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAO;IAE7B,YAAY,IAAI,EAAE,kCAAkC,EAInD;IAEK,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAGvD;IAEK,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CAUvF;CACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { requireWorkspace } from '@cat-factory/kernel';
|
|
2
|
+
/** The empty/unconfigured tracker settings returned before anything is set. */
|
|
3
|
+
const EMPTY = { tracker: null, jiraProjectKey: null };
|
|
4
|
+
/** Read/write a workspace's issue-tracker selection (one row per workspace). */
|
|
5
|
+
export class TrackerSettingsService {
|
|
6
|
+
repo;
|
|
7
|
+
workspaceRepository;
|
|
8
|
+
clock;
|
|
9
|
+
constructor(deps) {
|
|
10
|
+
this.repo = deps.trackerSettingsRepository;
|
|
11
|
+
this.workspaceRepository = deps.workspaceRepository;
|
|
12
|
+
this.clock = deps.clock;
|
|
13
|
+
}
|
|
14
|
+
async get(workspaceId) {
|
|
15
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
16
|
+
return (await this.repo.get(workspaceId)) ?? { ...EMPTY, updatedAt: 0 };
|
|
17
|
+
}
|
|
18
|
+
async put(workspaceId, input) {
|
|
19
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
20
|
+
const settings = {
|
|
21
|
+
tracker: input.tracker,
|
|
22
|
+
// Only keep a Jira project key when Jira is the selected tracker.
|
|
23
|
+
jiraProjectKey: input.tracker === 'jira' ? input.jiraProjectKey?.trim() || null : null,
|
|
24
|
+
updatedAt: this.clock.now(),
|
|
25
|
+
};
|
|
26
|
+
await this.repo.put(workspaceId, settings);
|
|
27
|
+
return settings;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=TrackerSettingsService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TrackerSettingsService.js","sourceRoot":"","sources":["../../../src/modules/recurring/TrackerSettingsService.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAQtD,+EAA+E;AAC/E,MAAM,KAAK,GAAuC,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAA;AAEzF,gFAAgF;AAChF,MAAM,OAAO,sBAAsB;IAChB,IAAI,CAA2B;IAC/B,mBAAmB,CAAqB;IACxC,KAAK,CAAO;IAE7B,YAAY,IAAwC;QAClD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,yBAAyB,CAAA;QAC1C,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAA;QACnD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAAmB;QAC3B,MAAM,gBAAgB,CAAC,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;QAC7D,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,CAAA;IACzE,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAAmB,EAAE,KAA8B;QAC3D,MAAM,gBAAgB,CAAC,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;QAC7D,MAAM,QAAQ,GAAoB;YAChC,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,kEAAkE;YAClE,cAAc,EAAE,KAAK,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI;YACtF,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE;SAC5B,CAAA;QACD,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;QAC1C,OAAO,QAAQ,CAAA;IACjB,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Recurrence } from '@cat-factory/kernel';
|
|
2
|
+
/** The local weekday (0=Sun..6=Sat) and hour (0..23) of `ms` in `timezone`. */
|
|
3
|
+
export declare function localParts(ms: number, timezone: string): {
|
|
4
|
+
weekday: number;
|
|
5
|
+
hour: number;
|
|
6
|
+
};
|
|
7
|
+
/** Whether `ms` is an instant the schedule is allowed to fire at. */
|
|
8
|
+
export declare function isWithinWindow(ms: number, recurrence: Recurrence): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The next fire time after `fromMs`: advance by `intervalHours`, then roll forward
|
|
11
|
+
* to the next instant inside the allowed weekday/hour window.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeNextRun(fromMs: number, recurrence: Recurrence): number;
|
|
14
|
+
//# sourceMappingURL=schedule.logic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.logic.d.ts","sourceRoot":"","sources":["../../../src/modules/recurring/schedule.logic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAarD,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAsB1F;AAuBD,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAG1E;AAkBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAG7E"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Pure cadence math for recurring pipelines. A schedule fires every
|
|
2
|
+
// `intervalHours`, but only at instants inside its allowed window — a set of
|
|
3
|
+
// weekdays plus an hour-of-day range, evaluated in the schedule's IANA timezone.
|
|
4
|
+
// `computeNextRun` advances by the interval and then rolls forward to the next
|
|
5
|
+
// eligible instant; `isWithinWindow` answers whether a given instant is eligible.
|
|
6
|
+
//
|
|
7
|
+
// Timezone handling uses `Intl.DateTimeFormat` (available in both workerd and
|
|
8
|
+
// Node), so we never depend on the host's local zone.
|
|
9
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
10
|
+
/** The local weekday (0=Sun..6=Sat) and hour (0..23) of `ms` in `timezone`. */
|
|
11
|
+
export function localParts(ms, timezone) {
|
|
12
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
13
|
+
timeZone: timezone,
|
|
14
|
+
weekday: 'short',
|
|
15
|
+
hour: 'numeric',
|
|
16
|
+
hour12: false,
|
|
17
|
+
});
|
|
18
|
+
const parts = fmt.formatToParts(new Date(ms));
|
|
19
|
+
const weekdayName = parts.find((p) => p.type === 'weekday')?.value ?? 'Sun';
|
|
20
|
+
let hourStr = parts.find((p) => p.type === 'hour')?.value ?? '0';
|
|
21
|
+
// `hour12: false` can render midnight as "24"; normalize to 0.
|
|
22
|
+
if (hourStr === '24')
|
|
23
|
+
hourStr = '0';
|
|
24
|
+
const weekdays = {
|
|
25
|
+
Sun: 0,
|
|
26
|
+
Mon: 1,
|
|
27
|
+
Tue: 2,
|
|
28
|
+
Wed: 3,
|
|
29
|
+
Thu: 4,
|
|
30
|
+
Fri: 5,
|
|
31
|
+
Sat: 6,
|
|
32
|
+
};
|
|
33
|
+
return { weekday: weekdays[weekdayName] ?? 0, hour: Number(hourStr) };
|
|
34
|
+
}
|
|
35
|
+
/** Whether the allowed-day set permits `weekday` (empty set = every day). */
|
|
36
|
+
function weekdayAllowed(recurrence, weekday) {
|
|
37
|
+
return recurrence.weekdays.length === 0 || recurrence.weekdays.includes(weekday);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether `hour` falls in the allowed window. Both bounds null = any hour. The
|
|
41
|
+
* window is [start, end): a start without an end runs from `start` to midnight; an
|
|
42
|
+
* end without a start runs from midnight to `end`. A `start >= end` window wraps
|
|
43
|
+
* past midnight (e.g. 22→6 means 22,23,0..5).
|
|
44
|
+
*/
|
|
45
|
+
function hourAllowed(recurrence, hour) {
|
|
46
|
+
const { windowStartHour: start, windowEndHour: end } = recurrence;
|
|
47
|
+
if (start === null && end === null)
|
|
48
|
+
return true;
|
|
49
|
+
const lo = start ?? 0;
|
|
50
|
+
const hi = end ?? 24;
|
|
51
|
+
if (lo < hi)
|
|
52
|
+
return hour >= lo && hour < hi;
|
|
53
|
+
// Wrapping window (lo >= hi): allowed outside the [hi, lo) gap.
|
|
54
|
+
return hour >= lo || hour < hi;
|
|
55
|
+
}
|
|
56
|
+
/** Whether `ms` is an instant the schedule is allowed to fire at. */
|
|
57
|
+
export function isWithinWindow(ms, recurrence) {
|
|
58
|
+
const { weekday, hour } = localParts(ms, recurrence.timezone);
|
|
59
|
+
return weekdayAllowed(recurrence, weekday) && hourAllowed(recurrence, hour);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The next instant at or after `fromMs` that satisfies the window, snapping to the
|
|
63
|
+
* top of the hour. Steps hour-by-hour (bounded) so DST transitions and wrapping
|
|
64
|
+
* windows are handled by the same `isWithinWindow` check rather than date math.
|
|
65
|
+
*/
|
|
66
|
+
function nextEligible(fromMs, recurrence) {
|
|
67
|
+
// Snap up to the next whole hour boundary so fires land at :00.
|
|
68
|
+
let candidate = Math.ceil(fromMs / HOUR_MS) * HOUR_MS;
|
|
69
|
+
// At most a year of hourly steps — guards against an impossible window.
|
|
70
|
+
for (let i = 0; i < 24 * 366; i++) {
|
|
71
|
+
if (isWithinWindow(candidate, recurrence))
|
|
72
|
+
return candidate;
|
|
73
|
+
candidate += HOUR_MS;
|
|
74
|
+
}
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The next fire time after `fromMs`: advance by `intervalHours`, then roll forward
|
|
79
|
+
* to the next instant inside the allowed weekday/hour window.
|
|
80
|
+
*/
|
|
81
|
+
export function computeNextRun(fromMs, recurrence) {
|
|
82
|
+
const base = fromMs + recurrence.intervalHours * HOUR_MS;
|
|
83
|
+
return nextEligible(base, recurrence);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=schedule.logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.logic.js","sourceRoot":"","sources":["../../../src/modules/recurring/schedule.logic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BlockRepository, Clock, DatadogConnectionRepository, ReleaseHealthConfigRepository, SecretCipher, WorkspaceRepository } from '@cat-factory/kernel';
|
|
2
|
+
import type { DatadogConnectionView, ReleaseHealthConfigWire, UpsertDatadogConnectionInput, UpsertReleaseHealthConfigInput } from '@cat-factory/contracts';
|
|
3
|
+
export interface ReleaseHealthServiceDependencies {
|
|
4
|
+
datadogConnectionRepository: DatadogConnectionRepository;
|
|
5
|
+
releaseHealthConfigRepository: ReleaseHealthConfigRepository;
|
|
6
|
+
/** Seals the Datadog API/app keys at rest (domain tag 'cat-factory:datadog'). */
|
|
7
|
+
datadogSecretCipher: SecretCipher;
|
|
8
|
+
workspaceRepository: WorkspaceRepository;
|
|
9
|
+
/** Validates a per-block config targets a block that exists in the workspace. */
|
|
10
|
+
blockRepository: BlockRepository;
|
|
11
|
+
clock: Clock;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Manages the post-release-health integration's settings for a workspace: the (single)
|
|
15
|
+
* Datadog connection — credentials sealed at rest, never read back — and the per-block
|
|
16
|
+
* monitor/SLO mappings the gate reads. Read paths return redacted views; the secrets
|
|
17
|
+
* only leave the cipher inside the `DatadogReleaseHealthProvider` at probe time.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ReleaseHealthService {
|
|
20
|
+
private readonly connections;
|
|
21
|
+
private readonly configs;
|
|
22
|
+
private readonly cipher;
|
|
23
|
+
private readonly workspaceRepository;
|
|
24
|
+
private readonly blocks;
|
|
25
|
+
private readonly clock;
|
|
26
|
+
constructor(deps: ReleaseHealthServiceDependencies);
|
|
27
|
+
/** The workspace's Datadog connection, redacted (never returns the secret keys). */
|
|
28
|
+
getConnection(workspaceId: string): Promise<DatadogConnectionView>;
|
|
29
|
+
/** Set/replace the workspace's Datadog connection, sealing the keys at rest. */
|
|
30
|
+
setConnection(workspaceId: string, input: UpsertDatadogConnectionInput): Promise<DatadogConnectionView>;
|
|
31
|
+
deleteConnection(workspaceId: string): Promise<void>;
|
|
32
|
+
/** The workspace's per-block monitor/SLO mappings. */
|
|
33
|
+
listConfigs(workspaceId: string): Promise<ReleaseHealthConfigWire[]>;
|
|
34
|
+
/** Create/replace a block's release-health config. */
|
|
35
|
+
upsertConfig(workspaceId: string, blockId: string, input: UpsertReleaseHealthConfigInput): Promise<ReleaseHealthConfigWire>;
|
|
36
|
+
deleteConfig(workspaceId: string, blockId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=ReleaseHealthService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReleaseHealthService.d.ts","sourceRoot":"","sources":["../../../src/modules/releaseHealth/ReleaseHealthService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,KAAK,EACL,2BAA2B,EAC3B,6BAA6B,EAC7B,YAAY,EACZ,mBAAmB,EACpB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EACV,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,8BAA8B,EAC/B,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,gCAAgC;IAC/C,2BAA2B,EAAE,2BAA2B,CAAA;IACxD,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,iFAAiF;IACjF,mBAAmB,EAAE,YAAY,CAAA;IACjC,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,iFAAiF;IACjF,eAAe,EAAE,eAAe,CAAA;IAChC,KAAK,EAAE,KAAK,CAAA;CACb;AAED;;;;;GAKG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAO;IAE7B,YAAY,IAAI,EAAE,gCAAgC,EAOjD;IAED,oFAAoF;IAC9E,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAMvE;IAED,gFAAgF;IAC1E,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,qBAAqB,CAAC,CAiBhC;IAEK,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzD;IAED,sDAAsD;IAChD,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CASzE;IAED,sDAAsD;IAChD,YAAY,CAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,uBAAuB,CAAC,CAuBlC;IAEK,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;CACF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { assertFound, requireWorkspace } from '@cat-factory/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* Manages the post-release-health integration's settings for a workspace: the (single)
|
|
4
|
+
* Datadog connection — credentials sealed at rest, never read back — and the per-block
|
|
5
|
+
* monitor/SLO mappings the gate reads. Read paths return redacted views; the secrets
|
|
6
|
+
* only leave the cipher inside the `DatadogReleaseHealthProvider` at probe time.
|
|
7
|
+
*/
|
|
8
|
+
export class ReleaseHealthService {
|
|
9
|
+
connections;
|
|
10
|
+
configs;
|
|
11
|
+
cipher;
|
|
12
|
+
workspaceRepository;
|
|
13
|
+
blocks;
|
|
14
|
+
clock;
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.connections = deps.datadogConnectionRepository;
|
|
17
|
+
this.configs = deps.releaseHealthConfigRepository;
|
|
18
|
+
this.cipher = deps.datadogSecretCipher;
|
|
19
|
+
this.workspaceRepository = deps.workspaceRepository;
|
|
20
|
+
this.blocks = deps.blockRepository;
|
|
21
|
+
this.clock = deps.clock;
|
|
22
|
+
}
|
|
23
|
+
/** The workspace's Datadog connection, redacted (never returns the secret keys). */
|
|
24
|
+
async getConnection(workspaceId) {
|
|
25
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
26
|
+
const connection = await this.connections.get(workspaceId);
|
|
27
|
+
return connection
|
|
28
|
+
? { connected: true, site: connection.site }
|
|
29
|
+
: { connected: false, site: null };
|
|
30
|
+
}
|
|
31
|
+
/** Set/replace the workspace's Datadog connection, sealing the keys at rest. */
|
|
32
|
+
async setConnection(workspaceId, input) {
|
|
33
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
34
|
+
const now = this.clock.now();
|
|
35
|
+
const [apiKey, appKey] = await Promise.all([
|
|
36
|
+
this.cipher.encrypt(input.apiKey),
|
|
37
|
+
this.cipher.encrypt(input.appKey),
|
|
38
|
+
]);
|
|
39
|
+
const existing = await this.connections.get(workspaceId);
|
|
40
|
+
await this.connections.upsert({
|
|
41
|
+
workspaceId,
|
|
42
|
+
site: input.site,
|
|
43
|
+
apiKey,
|
|
44
|
+
appKey,
|
|
45
|
+
createdAt: existing?.createdAt ?? now,
|
|
46
|
+
updatedAt: now,
|
|
47
|
+
});
|
|
48
|
+
return { connected: true, site: input.site };
|
|
49
|
+
}
|
|
50
|
+
async deleteConnection(workspaceId) {
|
|
51
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
52
|
+
await this.connections.delete(workspaceId);
|
|
53
|
+
}
|
|
54
|
+
/** The workspace's per-block monitor/SLO mappings. */
|
|
55
|
+
async listConfigs(workspaceId) {
|
|
56
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
57
|
+
const rows = await this.configs.listByWorkspace(workspaceId);
|
|
58
|
+
return rows.map((r) => ({
|
|
59
|
+
blockId: r.blockId,
|
|
60
|
+
monitorIds: r.monitorIds,
|
|
61
|
+
sloIds: r.sloIds,
|
|
62
|
+
envTag: r.envTag,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
/** Create/replace a block's release-health config. */
|
|
66
|
+
async upsertConfig(workspaceId, blockId, input) {
|
|
67
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
68
|
+
// The config is keyed by (workspace, block); reject a block that isn't in this
|
|
69
|
+
// workspace so a config can't be planted against a foreign/non-existent block id.
|
|
70
|
+
assertFound(await this.blocks.get(workspaceId, blockId), 'Block', blockId);
|
|
71
|
+
const now = this.clock.now();
|
|
72
|
+
const existing = await this.configs.getByBlock(workspaceId, blockId);
|
|
73
|
+
const record = {
|
|
74
|
+
workspaceId,
|
|
75
|
+
blockId,
|
|
76
|
+
monitorIds: input.monitorIds ?? [],
|
|
77
|
+
sloIds: input.sloIds ?? [],
|
|
78
|
+
envTag: input.envTag ?? null,
|
|
79
|
+
createdAt: existing?.createdAt ?? now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
};
|
|
82
|
+
await this.configs.upsert(record);
|
|
83
|
+
return {
|
|
84
|
+
blockId: record.blockId,
|
|
85
|
+
monitorIds: record.monitorIds,
|
|
86
|
+
sloIds: record.sloIds,
|
|
87
|
+
envTag: record.envTag,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async deleteConfig(workspaceId, blockId) {
|
|
91
|
+
await requireWorkspace(this.workspaceRepository, workspaceId);
|
|
92
|
+
assertFound(await this.blocks.get(workspaceId, blockId), 'Block', blockId);
|
|
93
|
+
await this.configs.delete(workspaceId, blockId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=ReleaseHealthService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReleaseHealthService.js","sourceRoot":"","sources":["../../../src/modules/releaseHealth/ReleaseHealthService.ts"],"names":[],"mappings":"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"}
|