@vibescope/mcp-server 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1169 -0
- package/dist/api-client.js +713 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +108 -477
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +113 -828
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +26 -627
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +56 -142
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +65 -68
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +119 -590
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +275 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +245 -894
- package/dist/handlers/tool-docs.d.ts +9 -0
- package/dist/handlers/tool-docs.js +904 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +38 -153
- package/dist/index.js +493 -162
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +34 -4
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1822 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +215 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +210 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +143 -896
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +29 -714
- package/src/handlers/fallback.test.ts +206 -361
- package/src/handlers/fallback.ts +81 -156
- package/src/handlers/findings.test.ts +229 -320
- package/src/handlers/findings.ts +76 -64
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +276 -206
- package/src/handlers/session.ts +136 -662
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +510 -0
- package/src/handlers/tasks.test.ts +669 -353
- package/src/handlers/tasks.ts +263 -1015
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +237 -568
- package/src/handlers/validation.ts +43 -167
- package/src/index.ts +493 -186
- package/src/tools.ts +2532 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/knowledge.ts +0 -132
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* - get_deployment_requirements
|
|
15
15
|
*/
|
|
16
16
|
import { ValidationError, validateRequired, validateUUID, validateEnvironment, } from '../validators.js';
|
|
17
|
+
import { getApiClient } from '../api-client.js';
|
|
17
18
|
export const requestDeployment = async (args, ctx) => {
|
|
18
19
|
const { project_id, environment = 'production', version_bump = 'patch', notes, git_ref } = args;
|
|
19
|
-
const {
|
|
20
|
-
const currentSessionId = session.currentSessionId;
|
|
20
|
+
const { session } = ctx;
|
|
21
21
|
validateRequired(project_id, 'project_id');
|
|
22
22
|
validateUUID(project_id, 'project_id');
|
|
23
23
|
validateEnvironment(environment);
|
|
@@ -28,204 +28,33 @@ export const requestDeployment = async (args, ctx) => {
|
|
|
28
28
|
hint: 'Must be one of: patch, minor, major',
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
.from('deployments')
|
|
34
|
-
.select('id, status')
|
|
35
|
-
.eq('project_id', project_id)
|
|
36
|
-
.not('status', 'in', '("deployed","failed")')
|
|
37
|
-
.single();
|
|
38
|
-
if (existingDeployment) {
|
|
39
|
-
return {
|
|
40
|
-
result: {
|
|
41
|
-
success: false,
|
|
42
|
-
error: 'A deployment is already in progress',
|
|
43
|
-
existing_deployment_id: existingDeployment.id,
|
|
44
|
-
existing_status: existingDeployment.status,
|
|
45
|
-
hint: 'Wait for the current deployment to complete or cancel it first',
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
// Check for unvalidated completed tasks
|
|
50
|
-
const { data: unvalidatedTasks } = await supabase
|
|
51
|
-
.from('tasks')
|
|
52
|
-
.select('id, title, completed_at, completed_by_session_id')
|
|
53
|
-
.eq('project_id', project_id)
|
|
54
|
-
.eq('status', 'completed')
|
|
55
|
-
.is('validated_at', null)
|
|
56
|
-
.order('completed_at', { ascending: true });
|
|
57
|
-
if (unvalidatedTasks && unvalidatedTasks.length > 0) {
|
|
58
|
-
return {
|
|
59
|
-
result: {
|
|
60
|
-
success: false,
|
|
61
|
-
error: 'Cannot deploy: There are unvalidated completed tasks',
|
|
62
|
-
unvalidated_tasks: unvalidatedTasks.map(t => ({
|
|
63
|
-
id: t.id,
|
|
64
|
-
title: t.title,
|
|
65
|
-
completed_at: t.completed_at,
|
|
66
|
-
})),
|
|
67
|
-
unvalidated_count: unvalidatedTasks.length,
|
|
68
|
-
hint: 'All completed tasks must be validated before deployment. Use validate_task to review each task.',
|
|
69
|
-
action: `Call validate_task(task_id: "${unvalidatedTasks[0].id}", approved: true/false, validation_notes: "...")`,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// Get current version from project
|
|
74
|
-
const { data: project } = await supabase
|
|
75
|
-
.from('projects')
|
|
76
|
-
.select('current_version')
|
|
77
|
-
.eq('id', project_id)
|
|
78
|
-
.single();
|
|
79
|
-
const currentVersion = project?.current_version || '0.0.0';
|
|
80
|
-
// Create new deployment
|
|
81
|
-
const { data: deployment, error } = await supabase
|
|
82
|
-
.from('deployments')
|
|
83
|
-
.insert({
|
|
84
|
-
project_id,
|
|
31
|
+
const apiClient = getApiClient();
|
|
32
|
+
const response = await apiClient.requestDeployment(project_id, {
|
|
85
33
|
environment,
|
|
86
34
|
version_bump,
|
|
87
35
|
notes,
|
|
88
|
-
git_ref
|
|
89
|
-
requested_by: 'agent',
|
|
90
|
-
requesting_agent_session_id: currentSessionId,
|
|
91
|
-
})
|
|
92
|
-
.select()
|
|
93
|
-
.single();
|
|
94
|
-
if (error)
|
|
95
|
-
throw error;
|
|
96
|
-
// Auto-convert pending deployment requirements to tasks
|
|
97
|
-
const { data: pendingRequirements } = await supabase
|
|
98
|
-
.from('deployment_requirements')
|
|
99
|
-
.select('id, type, title, description, stage, blocking')
|
|
100
|
-
.eq('project_id', project_id)
|
|
101
|
-
.eq('status', 'pending')
|
|
102
|
-
.is('converted_task_id', null);
|
|
103
|
-
const convertedTasks = [];
|
|
104
|
-
if (pendingRequirements && pendingRequirements.length > 0) {
|
|
105
|
-
for (const req of pendingRequirements) {
|
|
106
|
-
const isDeployStage = req.stage === 'deployment';
|
|
107
|
-
const isBlocking = req.blocking ?? isDeployStage;
|
|
108
|
-
const titlePrefix = isBlocking
|
|
109
|
-
? 'DEPLOY:'
|
|
110
|
-
: isDeployStage
|
|
111
|
-
? 'DEPLOY:'
|
|
112
|
-
: req.stage === 'verification'
|
|
113
|
-
? 'VERIFY:'
|
|
114
|
-
: 'PREP:';
|
|
115
|
-
// Create linked task
|
|
116
|
-
const { data: newTask } = await supabase
|
|
117
|
-
.from('tasks')
|
|
118
|
-
.insert({
|
|
119
|
-
project_id,
|
|
120
|
-
title: `${titlePrefix} ${req.title}`,
|
|
121
|
-
description: `[${req.type}] ${req.description || req.title}`,
|
|
122
|
-
priority: 1,
|
|
123
|
-
status: 'pending',
|
|
124
|
-
blocking: isBlocking,
|
|
125
|
-
created_by: 'agent',
|
|
126
|
-
created_by_session_id: currentSessionId,
|
|
127
|
-
})
|
|
128
|
-
.select('id')
|
|
129
|
-
.single();
|
|
130
|
-
if (newTask) {
|
|
131
|
-
// Link task to requirement WITHOUT changing status
|
|
132
|
-
// This keeps the requirement visible in the deployment steps list (permanent)
|
|
133
|
-
await supabase
|
|
134
|
-
.from('deployment_requirements')
|
|
135
|
-
.update({
|
|
136
|
-
converted_task_id: newTask.id,
|
|
137
|
-
})
|
|
138
|
-
.eq('id', req.id);
|
|
139
|
-
convertedTasks.push({
|
|
140
|
-
task_id: newTask.id,
|
|
141
|
-
requirement_id: req.id,
|
|
142
|
-
title: `${titlePrefix} ${req.title}`,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// Log progress
|
|
148
|
-
const convertedMsg = convertedTasks.length > 0
|
|
149
|
-
? ` (${convertedTasks.length} requirements converted to tasks)`
|
|
150
|
-
: '';
|
|
151
|
-
await supabase.from('progress_logs').insert({
|
|
152
|
-
project_id,
|
|
153
|
-
summary: `Deployment requested for ${environment} (${version_bump} bump from ${currentVersion})${convertedMsg}`,
|
|
154
|
-
details: notes || undefined,
|
|
155
|
-
created_by: 'agent',
|
|
156
|
-
created_by_session_id: currentSessionId,
|
|
36
|
+
git_ref
|
|
157
37
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
status: deployment.status,
|
|
163
|
-
environment: deployment.environment,
|
|
164
|
-
version_bump,
|
|
165
|
-
current_version: currentVersion,
|
|
166
|
-
converted_requirements: convertedTasks.length,
|
|
167
|
-
converted_tasks: convertedTasks.length > 0 ? convertedTasks : undefined,
|
|
168
|
-
message: convertedTasks.length > 0
|
|
169
|
-
? `Deployment created. ${convertedTasks.length} requirements converted to tasks. Run build/tests then call claim_deployment_validation.`
|
|
170
|
-
: 'Deployment created. Run build/tests then call claim_deployment_validation.',
|
|
171
|
-
},
|
|
172
|
-
};
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(response.error || 'Failed to request deployment');
|
|
40
|
+
}
|
|
41
|
+
return { result: response.data };
|
|
173
42
|
};
|
|
174
43
|
export const claimDeploymentValidation = async (args, ctx) => {
|
|
175
44
|
const { project_id } = args;
|
|
176
|
-
const {
|
|
177
|
-
const currentSessionId = session.currentSessionId;
|
|
45
|
+
const { session } = ctx;
|
|
178
46
|
validateRequired(project_id, 'project_id');
|
|
179
47
|
validateUUID(project_id, 'project_id');
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
.eq('project_id', project_id)
|
|
185
|
-
.eq('status', 'pending')
|
|
186
|
-
.single();
|
|
187
|
-
if (fetchError || !deployment) {
|
|
188
|
-
return {
|
|
189
|
-
result: {
|
|
190
|
-
success: false,
|
|
191
|
-
error: 'No pending deployment found',
|
|
192
|
-
hint: 'Use request_deployment to create a deployment first, or check_deployment_status to see current state',
|
|
193
|
-
},
|
|
194
|
-
};
|
|
48
|
+
const apiClient = getApiClient();
|
|
49
|
+
const response = await apiClient.claimDeploymentValidation(project_id, session.currentSessionId || undefined);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(response.error || 'Failed to claim deployment validation');
|
|
195
52
|
}
|
|
196
|
-
|
|
197
|
-
const { data: updated, error: updateError } = await supabase
|
|
198
|
-
.from('deployments')
|
|
199
|
-
.update({
|
|
200
|
-
status: 'validating',
|
|
201
|
-
validation_agent_session_id: currentSessionId,
|
|
202
|
-
validation_started_at: new Date().toISOString(),
|
|
203
|
-
})
|
|
204
|
-
.eq('id', deployment.id)
|
|
205
|
-
.eq('status', 'pending')
|
|
206
|
-
.select()
|
|
207
|
-
.single();
|
|
208
|
-
if (updateError || !updated) {
|
|
209
|
-
return {
|
|
210
|
-
result: {
|
|
211
|
-
success: false,
|
|
212
|
-
error: 'Failed to claim validation - deployment may have been claimed by another agent',
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
result: {
|
|
218
|
-
success: true,
|
|
219
|
-
deployment_id: deployment.id,
|
|
220
|
-
status: 'validating',
|
|
221
|
-
message: 'Validation claimed. Run build and tests, then call report_validation with results.',
|
|
222
|
-
},
|
|
223
|
-
};
|
|
53
|
+
return { result: response.data };
|
|
224
54
|
};
|
|
225
55
|
export const reportValidation = async (args, ctx) => {
|
|
226
56
|
const { project_id, build_passed, tests_passed, error_message } = args;
|
|
227
|
-
const {
|
|
228
|
-
const currentSessionId = session.currentSessionId;
|
|
57
|
+
const { session } = ctx;
|
|
229
58
|
validateRequired(project_id, 'project_id');
|
|
230
59
|
validateUUID(project_id, 'project_id');
|
|
231
60
|
if (build_passed === undefined) {
|
|
@@ -234,211 +63,43 @@ export const reportValidation = async (args, ctx) => {
|
|
|
234
63
|
hint: 'Set to true if the build succeeded, false otherwise',
|
|
235
64
|
});
|
|
236
65
|
}
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
.from('deployments')
|
|
240
|
-
.select('id')
|
|
241
|
-
.eq('project_id', project_id)
|
|
242
|
-
.eq('status', 'validating')
|
|
243
|
-
.single();
|
|
244
|
-
if (fetchError || !deployment) {
|
|
245
|
-
return {
|
|
246
|
-
result: {
|
|
247
|
-
success: false,
|
|
248
|
-
error: 'No deployment being validated. Use claim_deployment_validation first.',
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
const validationPassed = build_passed && (tests_passed !== false);
|
|
253
|
-
const newStatus = validationPassed ? 'ready' : 'failed';
|
|
254
|
-
const { error: updateError } = await supabase
|
|
255
|
-
.from('deployments')
|
|
256
|
-
.update({
|
|
257
|
-
status: newStatus,
|
|
66
|
+
const apiClient = getApiClient();
|
|
67
|
+
const response = await apiClient.reportValidation(project_id, {
|
|
258
68
|
build_passed,
|
|
259
|
-
tests_passed: tests_passed ??
|
|
260
|
-
|
|
261
|
-
validation_error: error_message || null,
|
|
262
|
-
})
|
|
263
|
-
.eq('id', deployment.id);
|
|
264
|
-
if (updateError)
|
|
265
|
-
throw updateError;
|
|
266
|
-
// Log result
|
|
267
|
-
await supabase.from('progress_logs').insert({
|
|
268
|
-
project_id,
|
|
269
|
-
summary: validationPassed
|
|
270
|
-
? `Deployment validation passed - ready to deploy`
|
|
271
|
-
: `Deployment validation failed: ${error_message || 'build/tests failed'}`,
|
|
272
|
-
details: `Build: ${build_passed ? 'passed' : 'failed'}, Tests: ${tests_passed === undefined ? 'skipped' : tests_passed ? 'passed' : 'failed'}`,
|
|
273
|
-
created_by: 'agent',
|
|
274
|
-
created_by_session_id: currentSessionId,
|
|
69
|
+
tests_passed: tests_passed ?? true,
|
|
70
|
+
error_message
|
|
275
71
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (!validationPassed) {
|
|
279
|
-
const failureType = !build_passed ? 'build' : 'test';
|
|
280
|
-
const { data: newTask } = await supabase
|
|
281
|
-
.from('tasks')
|
|
282
|
-
.insert({
|
|
283
|
-
project_id,
|
|
284
|
-
title: `Fix ${failureType} failure`,
|
|
285
|
-
description: error_message || `${failureType} failed during deployment validation`,
|
|
286
|
-
priority: 1,
|
|
287
|
-
status: 'pending',
|
|
288
|
-
created_by: 'agent',
|
|
289
|
-
created_by_session_id: currentSessionId,
|
|
290
|
-
estimated_minutes: 30,
|
|
291
|
-
})
|
|
292
|
-
.select('id')
|
|
293
|
-
.single();
|
|
294
|
-
createdTaskId = newTask?.id || null;
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(response.error || 'Failed to report validation');
|
|
295
74
|
}
|
|
296
|
-
return {
|
|
297
|
-
result: {
|
|
298
|
-
success: true,
|
|
299
|
-
status: newStatus,
|
|
300
|
-
passed: validationPassed,
|
|
301
|
-
...(createdTaskId && { fix_task_id: createdTaskId }),
|
|
302
|
-
},
|
|
303
|
-
};
|
|
75
|
+
return { result: response.data };
|
|
304
76
|
};
|
|
305
77
|
export const checkDeploymentStatus = async (args, ctx) => {
|
|
306
78
|
const { project_id } = args;
|
|
307
|
-
const { supabase } = ctx;
|
|
308
79
|
validateRequired(project_id, 'project_id');
|
|
309
80
|
validateUUID(project_id, 'project_id');
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
.
|
|
314
|
-
.eq('project_id', project_id)
|
|
315
|
-
.order('created_at', { ascending: false })
|
|
316
|
-
.limit(1)
|
|
317
|
-
.single();
|
|
318
|
-
if (error || !deployment) {
|
|
319
|
-
return {
|
|
320
|
-
result: {
|
|
321
|
-
has_deployment: false,
|
|
322
|
-
message: 'No deployments found for this project',
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
// Auto-timeout stale deployments
|
|
327
|
-
const DEPLOYMENT_TIMEOUT_MS = {
|
|
328
|
-
pending: 30 * 60 * 1000,
|
|
329
|
-
validating: 15 * 60 * 1000,
|
|
330
|
-
ready: 30 * 60 * 1000,
|
|
331
|
-
deploying: 10 * 60 * 1000,
|
|
332
|
-
};
|
|
333
|
-
if (!['deployed', 'failed'].includes(deployment.status)) {
|
|
334
|
-
const timeout = DEPLOYMENT_TIMEOUT_MS[deployment.status];
|
|
335
|
-
if (timeout) {
|
|
336
|
-
const startTime = deployment.status === 'deploying'
|
|
337
|
-
? deployment.deployment_started_at
|
|
338
|
-
: deployment.status === 'validating'
|
|
339
|
-
? deployment.validation_started_at
|
|
340
|
-
: deployment.created_at;
|
|
341
|
-
if (startTime && Date.now() - new Date(startTime).getTime() > timeout) {
|
|
342
|
-
const timeoutError = `Timed out: deployment was stuck in '${deployment.status}' state for too long`;
|
|
343
|
-
await supabase
|
|
344
|
-
.from('deployments')
|
|
345
|
-
.update({
|
|
346
|
-
status: 'failed',
|
|
347
|
-
deployment_error: timeoutError,
|
|
348
|
-
deployment_completed_at: new Date().toISOString(),
|
|
349
|
-
})
|
|
350
|
-
.eq('id', deployment.id);
|
|
351
|
-
deployment.status = 'failed';
|
|
352
|
-
deployment.deployment_error = timeoutError;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
81
|
+
const apiClient = getApiClient();
|
|
82
|
+
const response = await apiClient.checkDeploymentStatus(project_id);
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(response.error || 'Failed to check deployment status');
|
|
355
85
|
}
|
|
356
|
-
return {
|
|
357
|
-
result: {
|
|
358
|
-
has_deployment: true,
|
|
359
|
-
deployment: {
|
|
360
|
-
id: deployment.id,
|
|
361
|
-
status: deployment.status,
|
|
362
|
-
environment: deployment.environment,
|
|
363
|
-
requested_by: deployment.requested_by,
|
|
364
|
-
build_passed: deployment.build_passed,
|
|
365
|
-
tests_passed: deployment.tests_passed,
|
|
366
|
-
validation_error: deployment.validation_error,
|
|
367
|
-
deployment_error: deployment.deployment_error,
|
|
368
|
-
deployment_summary: deployment.deployment_summary,
|
|
369
|
-
notes: deployment.notes,
|
|
370
|
-
git_ref: deployment.git_ref,
|
|
371
|
-
created_at: deployment.created_at,
|
|
372
|
-
validation_started_at: deployment.validation_started_at,
|
|
373
|
-
validation_completed_at: deployment.validation_completed_at,
|
|
374
|
-
deployment_started_at: deployment.deployment_started_at,
|
|
375
|
-
deployment_completed_at: deployment.deployment_completed_at,
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
};
|
|
86
|
+
return { result: response.data };
|
|
379
87
|
};
|
|
380
88
|
export const startDeployment = async (args, ctx) => {
|
|
381
89
|
const { project_id } = args;
|
|
382
|
-
const {
|
|
383
|
-
const currentSessionId = session.currentSessionId;
|
|
90
|
+
const { session } = ctx;
|
|
384
91
|
validateRequired(project_id, 'project_id');
|
|
385
92
|
validateUUID(project_id, 'project_id');
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.select('id, environment')
|
|
391
|
-
.eq('project_id', project_id)
|
|
392
|
-
.eq('status', 'ready')
|
|
393
|
-
.single(),
|
|
394
|
-
supabase
|
|
395
|
-
.from('projects')
|
|
396
|
-
.select('deployment_instructions, git_main_branch')
|
|
397
|
-
.eq('id', project_id)
|
|
398
|
-
.single(),
|
|
399
|
-
]);
|
|
400
|
-
if (deploymentResult.error || !deploymentResult.data) {
|
|
401
|
-
return {
|
|
402
|
-
result: {
|
|
403
|
-
success: false,
|
|
404
|
-
error: 'No deployment ready. Must pass validation first.',
|
|
405
|
-
},
|
|
406
|
-
};
|
|
93
|
+
const apiClient = getApiClient();
|
|
94
|
+
const response = await apiClient.startDeployment(project_id, session.currentSessionId || undefined);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(response.error || 'Failed to start deployment');
|
|
407
97
|
}
|
|
408
|
-
|
|
409
|
-
const project = projectResult.data;
|
|
410
|
-
const { error: updateError } = await supabase
|
|
411
|
-
.from('deployments')
|
|
412
|
-
.update({
|
|
413
|
-
status: 'deploying',
|
|
414
|
-
deployment_started_at: new Date().toISOString(),
|
|
415
|
-
})
|
|
416
|
-
.eq('id', deployment.id);
|
|
417
|
-
if (updateError)
|
|
418
|
-
throw updateError;
|
|
419
|
-
await supabase.from('progress_logs').insert({
|
|
420
|
-
project_id,
|
|
421
|
-
summary: `Deployment to ${deployment.environment} started`,
|
|
422
|
-
created_by: 'agent',
|
|
423
|
-
created_by_session_id: currentSessionId,
|
|
424
|
-
});
|
|
425
|
-
const result = {
|
|
426
|
-
success: true,
|
|
427
|
-
status: 'deploying',
|
|
428
|
-
env: deployment.environment,
|
|
429
|
-
};
|
|
430
|
-
if (project?.deployment_instructions) {
|
|
431
|
-
result.instructions = project.deployment_instructions;
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
result.instructions = `No deployment instructions configured. Common steps:\n1. Push to ${project?.git_main_branch || 'main'} branch\n2. Or run your deploy command (e.g., fly deploy, vercel deploy)\n3. Call complete_deployment when done`;
|
|
435
|
-
}
|
|
436
|
-
return { result };
|
|
98
|
+
return { result: response.data };
|
|
437
99
|
};
|
|
438
100
|
export const completeDeployment = async (args, ctx) => {
|
|
439
101
|
const { project_id, success, summary } = args;
|
|
440
|
-
const {
|
|
441
|
-
const currentSessionId = session.currentSessionId;
|
|
102
|
+
const { session } = ctx;
|
|
442
103
|
validateRequired(project_id, 'project_id');
|
|
443
104
|
validateUUID(project_id, 'project_id');
|
|
444
105
|
if (success === undefined) {
|
|
@@ -447,117 +108,29 @@ export const completeDeployment = async (args, ctx) => {
|
|
|
447
108
|
hint: 'Set to true if deployment succeeded, false otherwise',
|
|
448
109
|
});
|
|
449
110
|
}
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
.eq('project_id', project_id)
|
|
455
|
-
.eq('status', 'deploying')
|
|
456
|
-
.single();
|
|
457
|
-
if (fetchError || !deployment) {
|
|
458
|
-
return {
|
|
459
|
-
result: {
|
|
460
|
-
success: false,
|
|
461
|
-
error: 'No deployment in progress. Use start_deployment first.',
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
const newStatus = success ? 'deployed' : 'failed';
|
|
466
|
-
let newVersion = null;
|
|
467
|
-
// If successful, calculate and store new version
|
|
468
|
-
if (success) {
|
|
469
|
-
const { data: project } = await supabase
|
|
470
|
-
.from('projects')
|
|
471
|
-
.select('current_version')
|
|
472
|
-
.eq('id', project_id)
|
|
473
|
-
.single();
|
|
474
|
-
const currentVersion = project?.current_version || '0.0.0';
|
|
475
|
-
const versionBump = deployment.version_bump || 'patch';
|
|
476
|
-
const parts = currentVersion.split('.').map((p) => parseInt(p, 10) || 0);
|
|
477
|
-
let [major, minor, patch] = [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
478
|
-
switch (versionBump) {
|
|
479
|
-
case 'major':
|
|
480
|
-
major += 1;
|
|
481
|
-
minor = 0;
|
|
482
|
-
patch = 0;
|
|
483
|
-
break;
|
|
484
|
-
case 'minor':
|
|
485
|
-
minor += 1;
|
|
486
|
-
patch = 0;
|
|
487
|
-
break;
|
|
488
|
-
default: patch += 1;
|
|
489
|
-
}
|
|
490
|
-
newVersion = `${major}.${minor}.${patch}`;
|
|
491
|
-
await supabase
|
|
492
|
-
.from('projects')
|
|
493
|
-
.update({ current_version: newVersion })
|
|
494
|
-
.eq('id', project_id);
|
|
495
|
-
}
|
|
496
|
-
const { error: updateError } = await supabase
|
|
497
|
-
.from('deployments')
|
|
498
|
-
.update({
|
|
499
|
-
status: newStatus,
|
|
500
|
-
version: newVersion,
|
|
501
|
-
deployment_completed_at: new Date().toISOString(),
|
|
502
|
-
deployment_summary: summary || null,
|
|
503
|
-
})
|
|
504
|
-
.eq('id', deployment.id);
|
|
505
|
-
if (updateError)
|
|
506
|
-
throw updateError;
|
|
507
|
-
await supabase.from('progress_logs').insert({
|
|
508
|
-
project_id,
|
|
509
|
-
summary: success
|
|
510
|
-
? `Deployed to ${deployment.environment}${newVersion ? ` v${newVersion}` : ''}`
|
|
511
|
-
: `Deployment failed`,
|
|
512
|
-
details: summary || undefined,
|
|
513
|
-
created_by: 'agent',
|
|
514
|
-
created_by_session_id: currentSessionId,
|
|
111
|
+
const apiClient = getApiClient();
|
|
112
|
+
const response = await apiClient.completeDeployment(project_id, {
|
|
113
|
+
success,
|
|
114
|
+
summary
|
|
515
115
|
});
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
...(newVersion && { version: newVersion }),
|
|
521
|
-
},
|
|
522
|
-
};
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(response.error || 'Failed to complete deployment');
|
|
118
|
+
}
|
|
119
|
+
return { result: response.data };
|
|
523
120
|
};
|
|
524
121
|
export const cancelDeployment = async (args, ctx) => {
|
|
525
122
|
const { project_id, reason } = args;
|
|
526
|
-
const { supabase, session } = ctx;
|
|
527
|
-
const currentSessionId = session.currentSessionId;
|
|
528
123
|
validateRequired(project_id, 'project_id');
|
|
529
124
|
validateUUID(project_id, 'project_id');
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
.
|
|
534
|
-
.not('status', 'in', '("deployed","failed")')
|
|
535
|
-
.single();
|
|
536
|
-
if (fetchError || !deployment) {
|
|
537
|
-
return { result: { success: false, error: 'No active deployment' } };
|
|
125
|
+
const apiClient = getApiClient();
|
|
126
|
+
const response = await apiClient.cancelDeployment(project_id, reason);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(response.error || 'Failed to cancel deployment');
|
|
538
129
|
}
|
|
539
|
-
|
|
540
|
-
.from('deployments')
|
|
541
|
-
.update({
|
|
542
|
-
status: 'failed',
|
|
543
|
-
deployment_error: `Cancelled: ${reason || 'unspecified'}`,
|
|
544
|
-
deployment_completed_at: new Date().toISOString(),
|
|
545
|
-
})
|
|
546
|
-
.eq('id', deployment.id);
|
|
547
|
-
if (updateError)
|
|
548
|
-
throw updateError;
|
|
549
|
-
await supabase.from('progress_logs').insert({
|
|
550
|
-
project_id,
|
|
551
|
-
summary: `Deployment cancelled${reason ? `: ${reason}` : ''}`,
|
|
552
|
-
created_by: 'agent',
|
|
553
|
-
created_by_session_id: currentSessionId,
|
|
554
|
-
});
|
|
555
|
-
return { result: { success: true } };
|
|
130
|
+
return { result: response.data };
|
|
556
131
|
};
|
|
557
132
|
export const addDeploymentRequirement = async (args, ctx) => {
|
|
558
|
-
const { project_id, type, title, description, file_path, stage = 'preparation', blocking = false } = args;
|
|
559
|
-
const { supabase, session } = ctx;
|
|
560
|
-
const currentSessionId = session.currentSessionId;
|
|
133
|
+
const { project_id, type, title, description, file_path, stage = 'preparation', blocking = false, recurring = false } = args;
|
|
561
134
|
validateRequired(project_id, 'project_id');
|
|
562
135
|
validateUUID(project_id, 'project_id');
|
|
563
136
|
validateRequired(type, 'type');
|
|
@@ -570,124 +143,51 @@ export const addDeploymentRequirement = async (args, ctx) => {
|
|
|
570
143
|
if (!validStages.includes(stage)) {
|
|
571
144
|
throw new ValidationError(`stage must be one of: ${validStages.join(', ')}`);
|
|
572
145
|
}
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
project_id,
|
|
577
|
-
type,
|
|
146
|
+
const apiClient = getApiClient();
|
|
147
|
+
const response = await apiClient.addDeploymentRequirement(project_id, {
|
|
148
|
+
type: type,
|
|
578
149
|
title,
|
|
579
|
-
description
|
|
580
|
-
file_path
|
|
581
|
-
stage,
|
|
150
|
+
description,
|
|
151
|
+
file_path,
|
|
152
|
+
stage: stage,
|
|
582
153
|
blocking,
|
|
583
|
-
|
|
584
|
-
})
|
|
585
|
-
.select('id, type, title, stage, blocking')
|
|
586
|
-
.single();
|
|
587
|
-
if (error)
|
|
588
|
-
throw new Error(`Failed to add requirement: ${error.message}`);
|
|
589
|
-
const blockingText = blocking ? ' (BLOCKING)' : '';
|
|
590
|
-
await supabase.from('progress_logs').insert({
|
|
591
|
-
project_id,
|
|
592
|
-
summary: `Added ${stage} deployment requirement${blockingText}: ${title}`,
|
|
593
|
-
details: `Type: ${type}, Stage: ${stage}${blocking ? ', Blocking: true' : ''}${file_path ? `, File: ${file_path}` : ''}`,
|
|
594
|
-
created_by: 'agent',
|
|
595
|
-
created_by_session_id: currentSessionId,
|
|
154
|
+
recurring
|
|
596
155
|
});
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
: stage === 'verification'
|
|
602
|
-
? 'Will run after deployment for verification.'
|
|
603
|
-
: 'Will run during preparation phase.';
|
|
604
|
-
return {
|
|
605
|
-
result: {
|
|
606
|
-
success: true,
|
|
607
|
-
requirement_id: requirement.id,
|
|
608
|
-
stage: requirement.stage,
|
|
609
|
-
message: `Added ${type} requirement. ${stageMessage}`,
|
|
610
|
-
},
|
|
611
|
-
};
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(response.error || 'Failed to add deployment requirement');
|
|
158
|
+
}
|
|
159
|
+
return { result: response.data };
|
|
612
160
|
};
|
|
613
161
|
export const completeDeploymentRequirement = async (args, ctx) => {
|
|
614
162
|
const { requirement_id } = args;
|
|
615
|
-
const { supabase, session } = ctx;
|
|
616
|
-
const currentSessionId = session.currentSessionId;
|
|
617
163
|
validateRequired(requirement_id, 'requirement_id');
|
|
618
164
|
validateUUID(requirement_id, 'requirement_id');
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
.
|
|
623
|
-
.single();
|
|
624
|
-
if (fetchError || !requirement) {
|
|
625
|
-
throw new Error('Requirement not found');
|
|
165
|
+
const apiClient = getApiClient();
|
|
166
|
+
const response = await apiClient.completeDeploymentRequirement(requirement_id);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(response.error || 'Failed to complete deployment requirement');
|
|
626
169
|
}
|
|
627
|
-
|
|
628
|
-
return {
|
|
629
|
-
result: {
|
|
630
|
-
success: false,
|
|
631
|
-
error: `Requirement is already ${requirement.status}`,
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
const { error: updateError } = await supabase
|
|
636
|
-
.from('deployment_requirements')
|
|
637
|
-
.update({
|
|
638
|
-
status: 'completed',
|
|
639
|
-
completed_at: new Date().toISOString(),
|
|
640
|
-
completed_by: currentSessionId || 'agent',
|
|
641
|
-
})
|
|
642
|
-
.eq('id', requirement_id);
|
|
643
|
-
if (updateError)
|
|
644
|
-
throw updateError;
|
|
645
|
-
return {
|
|
646
|
-
result: {
|
|
647
|
-
success: true,
|
|
648
|
-
requirement_id,
|
|
649
|
-
title: requirement.title,
|
|
650
|
-
},
|
|
651
|
-
};
|
|
170
|
+
return { result: response.data };
|
|
652
171
|
};
|
|
653
172
|
export const getDeploymentRequirements = async (args, ctx) => {
|
|
654
173
|
const { project_id, status = 'pending', stage } = args;
|
|
655
|
-
const { supabase } = ctx;
|
|
656
174
|
validateRequired(project_id, 'project_id');
|
|
657
175
|
validateUUID(project_id, 'project_id');
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
query = query.eq('status', status);
|
|
666
|
-
}
|
|
667
|
-
if (stage && stage !== 'all') {
|
|
668
|
-
query = query.eq('stage', stage);
|
|
176
|
+
const apiClient = getApiClient();
|
|
177
|
+
const response = await apiClient.getDeploymentRequirements(project_id, {
|
|
178
|
+
status: status,
|
|
179
|
+
stage: stage
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(response.error || 'Failed to get deployment requirements');
|
|
669
183
|
}
|
|
670
|
-
|
|
671
|
-
if (error)
|
|
672
|
-
throw new Error(`Failed to fetch requirements: ${error.message}`);
|
|
673
|
-
const preparationPending = requirements?.filter(r => r.status === 'pending' && r.stage === 'preparation').length || 0;
|
|
674
|
-
const deploymentPending = requirements?.filter(r => r.status === 'pending' && r.stage === 'deployment').length || 0;
|
|
675
|
-
return {
|
|
676
|
-
result: {
|
|
677
|
-
requirements: requirements || [],
|
|
678
|
-
preparation_pending: preparationPending,
|
|
679
|
-
deployment_pending: deploymentPending,
|
|
680
|
-
deployment_blocked: preparationPending > 0 || deploymentPending > 0,
|
|
681
|
-
},
|
|
682
|
-
};
|
|
184
|
+
return { result: response.data };
|
|
683
185
|
};
|
|
684
186
|
// ============================================================================
|
|
685
187
|
// Scheduled Deployments
|
|
686
188
|
// ============================================================================
|
|
687
189
|
export const scheduleDeployment = async (args, ctx) => {
|
|
688
190
|
const { project_id, environment = 'production', version_bump = 'patch', schedule_type = 'once', scheduled_at, auto_trigger = true, notes, git_ref, } = args;
|
|
689
|
-
const { supabase, session } = ctx;
|
|
690
|
-
const currentSessionId = session.currentSessionId;
|
|
691
191
|
validateRequired(project_id, 'project_id');
|
|
692
192
|
validateUUID(project_id, 'project_id');
|
|
693
193
|
validateRequired(scheduled_at, 'scheduled_at');
|
|
@@ -717,82 +217,34 @@ export const scheduleDeployment = async (args, ctx) => {
|
|
|
717
217
|
field: 'scheduled_at',
|
|
718
218
|
});
|
|
719
219
|
}
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
environment,
|
|
726
|
-
version_bump,
|
|
727
|
-
schedule_type,
|
|
220
|
+
const apiClient = getApiClient();
|
|
221
|
+
const response = await apiClient.scheduleDeployment(project_id, {
|
|
222
|
+
environment: environment,
|
|
223
|
+
version_bump: version_bump,
|
|
224
|
+
schedule_type: schedule_type,
|
|
728
225
|
scheduled_at: scheduledDate.toISOString(),
|
|
729
226
|
auto_trigger,
|
|
730
|
-
notes
|
|
731
|
-
git_ref
|
|
732
|
-
created_by: 'agent',
|
|
733
|
-
created_by_session_id: currentSessionId,
|
|
734
|
-
})
|
|
735
|
-
.select('id, scheduled_at, schedule_type')
|
|
736
|
-
.single();
|
|
737
|
-
if (error)
|
|
738
|
-
throw new Error(`Failed to create schedule: ${error.message}`);
|
|
739
|
-
// Log progress
|
|
740
|
-
await supabase.from('progress_logs').insert({
|
|
741
|
-
project_id,
|
|
742
|
-
summary: `Scheduled ${schedule_type} deployment to ${environment} for ${scheduledDate.toISOString()}`,
|
|
743
|
-
details: `Auto-trigger: ${auto_trigger}, Version bump: ${version_bump}`,
|
|
744
|
-
created_by: 'agent',
|
|
745
|
-
created_by_session_id: currentSessionId,
|
|
227
|
+
notes,
|
|
228
|
+
git_ref
|
|
746
229
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
scheduled_at: schedule.scheduled_at,
|
|
752
|
-
schedule_type: schedule.schedule_type,
|
|
753
|
-
auto_trigger,
|
|
754
|
-
message: auto_trigger
|
|
755
|
-
? 'Deployment scheduled. Will trigger automatically when time arrives.'
|
|
756
|
-
: 'Deployment scheduled. Manual trigger required from dashboard.',
|
|
757
|
-
},
|
|
758
|
-
};
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
throw new Error(response.error || 'Failed to schedule deployment');
|
|
232
|
+
}
|
|
233
|
+
return { result: response.data };
|
|
759
234
|
};
|
|
760
235
|
export const getScheduledDeployments = async (args, ctx) => {
|
|
761
236
|
const { project_id, include_disabled = false } = args;
|
|
762
|
-
const { supabase } = ctx;
|
|
763
237
|
validateRequired(project_id, 'project_id');
|
|
764
238
|
validateUUID(project_id, 'project_id');
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
.
|
|
769
|
-
.order('scheduled_at', { ascending: true });
|
|
770
|
-
if (!include_disabled) {
|
|
771
|
-
query = query.eq('enabled', true);
|
|
239
|
+
const apiClient = getApiClient();
|
|
240
|
+
const response = await apiClient.getScheduledDeployments(project_id, include_disabled);
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
throw new Error(response.error || 'Failed to get scheduled deployments');
|
|
772
243
|
}
|
|
773
|
-
|
|
774
|
-
if (error)
|
|
775
|
-
throw new Error(`Failed to fetch schedules: ${error.message}`);
|
|
776
|
-
const now = new Date();
|
|
777
|
-
const schedulesWithStatus = (schedules || []).map(s => ({
|
|
778
|
-
...s,
|
|
779
|
-
is_due: s.enabled && new Date(s.scheduled_at) <= now,
|
|
780
|
-
}));
|
|
781
|
-
const dueCount = schedulesWithStatus.filter(s => s.is_due && s.auto_trigger).length;
|
|
782
|
-
return {
|
|
783
|
-
result: {
|
|
784
|
-
schedules: schedulesWithStatus,
|
|
785
|
-
count: schedulesWithStatus.length,
|
|
786
|
-
due_count: dueCount,
|
|
787
|
-
...(dueCount > 0 && {
|
|
788
|
-
hint: 'There are due schedules. Call trigger_scheduled_deployment to execute.',
|
|
789
|
-
}),
|
|
790
|
-
},
|
|
791
|
-
};
|
|
244
|
+
return { result: response.data };
|
|
792
245
|
};
|
|
793
246
|
export const updateScheduledDeployment = async (args, ctx) => {
|
|
794
247
|
const { schedule_id, environment, version_bump, schedule_type, scheduled_at, auto_trigger, enabled, notes, git_ref, } = args;
|
|
795
|
-
const { supabase } = ctx;
|
|
796
248
|
validateRequired(schedule_id, 'schedule_id');
|
|
797
249
|
validateUUID(schedule_id, 'schedule_id');
|
|
798
250
|
const updates = {};
|
|
@@ -830,213 +282,46 @@ export const updateScheduledDeployment = async (args, ctx) => {
|
|
|
830
282
|
if (Object.keys(updates).length === 0) {
|
|
831
283
|
return { result: { success: false, error: 'No updates provided' } };
|
|
832
284
|
}
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
.
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
return { result: { success: true, schedule_id } };
|
|
285
|
+
const apiClient = getApiClient();
|
|
286
|
+
const response = await apiClient.updateScheduledDeployment(schedule_id, updates);
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new Error(response.error || 'Failed to update scheduled deployment');
|
|
289
|
+
}
|
|
290
|
+
return { result: response.data };
|
|
840
291
|
};
|
|
841
292
|
export const deleteScheduledDeployment = async (args, ctx) => {
|
|
842
293
|
const { schedule_id } = args;
|
|
843
|
-
const { supabase } = ctx;
|
|
844
294
|
validateRequired(schedule_id, 'schedule_id');
|
|
845
295
|
validateUUID(schedule_id, 'schedule_id');
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
.
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return { result: { success: true } };
|
|
296
|
+
const apiClient = getApiClient();
|
|
297
|
+
const response = await apiClient.deleteScheduledDeployment(schedule_id);
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
throw new Error(response.error || 'Failed to delete scheduled deployment');
|
|
300
|
+
}
|
|
301
|
+
return { result: response.data };
|
|
853
302
|
};
|
|
854
303
|
export const triggerScheduledDeployment = async (args, ctx) => {
|
|
855
304
|
const { schedule_id } = args;
|
|
856
|
-
const {
|
|
857
|
-
const currentSessionId = session.currentSessionId;
|
|
305
|
+
const { session } = ctx;
|
|
858
306
|
validateRequired(schedule_id, 'schedule_id');
|
|
859
307
|
validateUUID(schedule_id, 'schedule_id');
|
|
860
|
-
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
.
|
|
864
|
-
.eq('id', schedule_id)
|
|
865
|
-
.single();
|
|
866
|
-
if (fetchError || !schedule) {
|
|
867
|
-
return { result: { success: false, error: 'Schedule not found' } };
|
|
308
|
+
const apiClient = getApiClient();
|
|
309
|
+
const response = await apiClient.triggerScheduledDeployment(schedule_id, session.currentSessionId || undefined);
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
throw new Error(response.error || 'Failed to trigger scheduled deployment');
|
|
868
312
|
}
|
|
869
|
-
|
|
870
|
-
return { result: { success: false, error: 'Schedule is disabled' } };
|
|
871
|
-
}
|
|
872
|
-
// Check for existing active deployment
|
|
873
|
-
const { data: existingDeployment } = await supabase
|
|
874
|
-
.from('deployments')
|
|
875
|
-
.select('id, status')
|
|
876
|
-
.eq('project_id', schedule.project_id)
|
|
877
|
-
.not('status', 'in', '("deployed","failed")')
|
|
878
|
-
.single();
|
|
879
|
-
if (existingDeployment) {
|
|
880
|
-
return {
|
|
881
|
-
result: {
|
|
882
|
-
success: false,
|
|
883
|
-
error: 'A deployment is already in progress',
|
|
884
|
-
existing_deployment_id: existingDeployment.id,
|
|
885
|
-
hint: 'Wait for current deployment to complete or cancel it first',
|
|
886
|
-
},
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
// Create the deployment (similar to request_deployment)
|
|
890
|
-
const { data: deployment, error: deployError } = await supabase
|
|
891
|
-
.from('deployments')
|
|
892
|
-
.insert({
|
|
893
|
-
project_id: schedule.project_id,
|
|
894
|
-
environment: schedule.environment,
|
|
895
|
-
version_bump: schedule.version_bump,
|
|
896
|
-
notes: schedule.notes,
|
|
897
|
-
git_ref: schedule.git_ref,
|
|
898
|
-
requested_by: 'agent',
|
|
899
|
-
requesting_agent_session_id: currentSessionId,
|
|
900
|
-
})
|
|
901
|
-
.select('id, status')
|
|
902
|
-
.single();
|
|
903
|
-
if (deployError)
|
|
904
|
-
throw new Error(`Failed to create deployment: ${deployError.message}`);
|
|
905
|
-
// Auto-convert pending deployment requirements to tasks
|
|
906
|
-
const { data: pendingRequirements } = await supabase
|
|
907
|
-
.from('deployment_requirements')
|
|
908
|
-
.select('id, type, title, description, stage, blocking')
|
|
909
|
-
.eq('project_id', schedule.project_id)
|
|
910
|
-
.eq('status', 'pending')
|
|
911
|
-
.is('converted_task_id', null);
|
|
912
|
-
const convertedTasks = [];
|
|
913
|
-
if (pendingRequirements && pendingRequirements.length > 0) {
|
|
914
|
-
for (const req of pendingRequirements) {
|
|
915
|
-
const isDeployStage = req.stage === 'deployment';
|
|
916
|
-
const isBlocking = req.blocking ?? isDeployStage;
|
|
917
|
-
const titlePrefix = isBlocking
|
|
918
|
-
? 'DEPLOY:'
|
|
919
|
-
: isDeployStage
|
|
920
|
-
? 'DEPLOY:'
|
|
921
|
-
: req.stage === 'verification'
|
|
922
|
-
? 'VERIFY:'
|
|
923
|
-
: 'PREP:';
|
|
924
|
-
// Create linked task
|
|
925
|
-
const { data: newTask } = await supabase
|
|
926
|
-
.from('tasks')
|
|
927
|
-
.insert({
|
|
928
|
-
project_id: schedule.project_id,
|
|
929
|
-
title: `${titlePrefix} ${req.title}`,
|
|
930
|
-
description: `[${req.type}] ${req.description || req.title}`,
|
|
931
|
-
priority: 1,
|
|
932
|
-
status: 'pending',
|
|
933
|
-
blocking: isBlocking,
|
|
934
|
-
created_by: 'agent',
|
|
935
|
-
created_by_session_id: currentSessionId,
|
|
936
|
-
})
|
|
937
|
-
.select('id')
|
|
938
|
-
.single();
|
|
939
|
-
if (newTask) {
|
|
940
|
-
// Link task to requirement WITHOUT changing status
|
|
941
|
-
// This keeps the requirement visible in the deployment steps list (permanent)
|
|
942
|
-
await supabase
|
|
943
|
-
.from('deployment_requirements')
|
|
944
|
-
.update({
|
|
945
|
-
converted_task_id: newTask.id,
|
|
946
|
-
})
|
|
947
|
-
.eq('id', req.id);
|
|
948
|
-
convertedTasks.push({
|
|
949
|
-
task_id: newTask.id,
|
|
950
|
-
requirement_id: req.id,
|
|
951
|
-
title: `${titlePrefix} ${req.title}`,
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
// Update the schedule
|
|
957
|
-
const scheduleUpdates = {
|
|
958
|
-
last_triggered_at: new Date().toISOString(),
|
|
959
|
-
last_deployment_id: deployment.id,
|
|
960
|
-
trigger_count: schedule.trigger_count + 1,
|
|
961
|
-
};
|
|
962
|
-
// For recurring schedules, calculate next run time
|
|
963
|
-
if (schedule.schedule_type !== 'once') {
|
|
964
|
-
const currentScheduledAt = new Date(schedule.scheduled_at);
|
|
965
|
-
let nextScheduledAt;
|
|
966
|
-
switch (schedule.schedule_type) {
|
|
967
|
-
case 'daily':
|
|
968
|
-
nextScheduledAt = new Date(currentScheduledAt.getTime() + 24 * 60 * 60 * 1000);
|
|
969
|
-
break;
|
|
970
|
-
case 'weekly':
|
|
971
|
-
nextScheduledAt = new Date(currentScheduledAt.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
972
|
-
break;
|
|
973
|
-
case 'monthly':
|
|
974
|
-
nextScheduledAt = new Date(currentScheduledAt.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
975
|
-
break;
|
|
976
|
-
default:
|
|
977
|
-
nextScheduledAt = currentScheduledAt;
|
|
978
|
-
}
|
|
979
|
-
scheduleUpdates.scheduled_at = nextScheduledAt.toISOString();
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
// One-time schedule, disable it
|
|
983
|
-
scheduleUpdates.enabled = false;
|
|
984
|
-
}
|
|
985
|
-
await supabase
|
|
986
|
-
.from('scheduled_deployments')
|
|
987
|
-
.update(scheduleUpdates)
|
|
988
|
-
.eq('id', schedule_id);
|
|
989
|
-
// Log progress
|
|
990
|
-
const convertedMsg = convertedTasks.length > 0
|
|
991
|
-
? `, ${convertedTasks.length} requirements converted to tasks`
|
|
992
|
-
: '';
|
|
993
|
-
await supabase.from('progress_logs').insert({
|
|
994
|
-
project_id: schedule.project_id,
|
|
995
|
-
summary: `Triggered scheduled deployment to ${schedule.environment}${convertedMsg}`,
|
|
996
|
-
details: `Schedule: ${schedule.schedule_type}, Trigger #${schedule.trigger_count + 1}`,
|
|
997
|
-
created_by: 'agent',
|
|
998
|
-
created_by_session_id: currentSessionId,
|
|
999
|
-
});
|
|
1000
|
-
return {
|
|
1001
|
-
result: {
|
|
1002
|
-
success: true,
|
|
1003
|
-
deployment_id: deployment.id,
|
|
1004
|
-
schedule_id,
|
|
1005
|
-
schedule_type: schedule.schedule_type,
|
|
1006
|
-
next_scheduled_at: schedule.schedule_type !== 'once' ? scheduleUpdates.scheduled_at : null,
|
|
1007
|
-
converted_requirements: convertedTasks.length,
|
|
1008
|
-
converted_tasks: convertedTasks.length > 0 ? convertedTasks : undefined,
|
|
1009
|
-
message: convertedTasks.length > 0
|
|
1010
|
-
? `Deployment created from schedule. ${convertedTasks.length} requirements converted to tasks. Run validation then deploy.`
|
|
1011
|
-
: 'Deployment created from schedule. Run validation then deploy.',
|
|
1012
|
-
},
|
|
1013
|
-
};
|
|
313
|
+
return { result: response.data };
|
|
1014
314
|
};
|
|
1015
315
|
export const checkDueDeployments = async (args, ctx) => {
|
|
1016
316
|
const { project_id } = args;
|
|
1017
|
-
const { supabase } = ctx;
|
|
1018
317
|
validateRequired(project_id, 'project_id');
|
|
1019
318
|
validateUUID(project_id, 'project_id');
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
.eq('auto_trigger', true)
|
|
1027
|
-
.lte('scheduled_at', new Date().toISOString())
|
|
1028
|
-
.order('scheduled_at', { ascending: true });
|
|
1029
|
-
if (error)
|
|
1030
|
-
throw new Error(`Failed to check schedules: ${error.message}`);
|
|
1031
|
-
return {
|
|
1032
|
-
result: {
|
|
1033
|
-
due_schedules: dueSchedules || [],
|
|
1034
|
-
count: dueSchedules?.length || 0,
|
|
1035
|
-
...(dueSchedules && dueSchedules.length > 0 && {
|
|
1036
|
-
hint: `Call trigger_scheduled_deployment(schedule_id: "${dueSchedules[0].id}") to trigger the first due deployment`,
|
|
1037
|
-
}),
|
|
1038
|
-
},
|
|
1039
|
-
};
|
|
319
|
+
const apiClient = getApiClient();
|
|
320
|
+
const response = await apiClient.checkDueDeployments(project_id);
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
throw new Error(response.error || 'Failed to check due deployments');
|
|
323
|
+
}
|
|
324
|
+
return { result: response.data };
|
|
1040
325
|
};
|
|
1041
326
|
/**
|
|
1042
327
|
* Deployment handlers registry
|