@vibescope/mcp-server 0.0.1 → 0.1.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 +1114 -0
- package/dist/api-client.js +698 -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 +106 -476
- 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 +112 -828
- package/dist/handlers/discovery.js +31 -0
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +39 -134
- package/dist/handlers/findings.js +43 -67
- 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 +99 -585
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +274 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +230 -900
- package/dist/handlers/tool-docs.d.ts +8 -0
- package/dist/handlers/tool-docs.js +657 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +26 -153
- package/dist/index.js +473 -160
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +4 -0
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1752 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +210 -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 +194 -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 +140 -895
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +32 -0
- package/src/handlers/fallback.test.ts +128 -361
- package/src/handlers/fallback.ts +62 -148
- package/src/handlers/findings.test.ts +127 -345
- package/src/handlers/findings.ts +49 -66
- 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 +241 -206
- package/src/handlers/session.ts +110 -657
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +497 -0
- package/src/handlers/tasks.test.ts +608 -353
- package/src/handlers/tasks.ts +248 -1025
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +189 -572
- package/src/handlers/validation.ts +29 -166
- package/src/index.ts +473 -184
- package/src/knowledge.ts +107 -9
- package/src/tools.ts +2506 -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/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
133
|
const { project_id, type, title, description, file_path, stage = 'preparation', blocking = false } = args;
|
|
559
|
-
const { supabase, session } = ctx;
|
|
560
|
-
const currentSessionId = session.currentSessionId;
|
|
561
134
|
validateRequired(project_id, 'project_id');
|
|
562
135
|
validateUUID(project_id, 'project_id');
|
|
563
136
|
validateRequired(type, 'type');
|
|
@@ -570,124 +143,50 @@ 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,
|
|
582
|
-
blocking
|
|
583
|
-
created_by_session_id: currentSessionId,
|
|
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,
|
|
150
|
+
description,
|
|
151
|
+
file_path,
|
|
152
|
+
stage: stage,
|
|
153
|
+
blocking
|
|
596
154
|
});
|
|
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
|
-
};
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(response.error || 'Failed to add deployment requirement');
|
|
157
|
+
}
|
|
158
|
+
return { result: response.data };
|
|
612
159
|
};
|
|
613
160
|
export const completeDeploymentRequirement = async (args, ctx) => {
|
|
614
161
|
const { requirement_id } = args;
|
|
615
|
-
const { supabase, session } = ctx;
|
|
616
|
-
const currentSessionId = session.currentSessionId;
|
|
617
162
|
validateRequired(requirement_id, 'requirement_id');
|
|
618
163
|
validateUUID(requirement_id, 'requirement_id');
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
.
|
|
623
|
-
.single();
|
|
624
|
-
if (fetchError || !requirement) {
|
|
625
|
-
throw new Error('Requirement not found');
|
|
164
|
+
const apiClient = getApiClient();
|
|
165
|
+
const response = await apiClient.completeDeploymentRequirement(requirement_id);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(response.error || 'Failed to complete deployment requirement');
|
|
626
168
|
}
|
|
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
|
-
};
|
|
169
|
+
return { result: response.data };
|
|
652
170
|
};
|
|
653
171
|
export const getDeploymentRequirements = async (args, ctx) => {
|
|
654
172
|
const { project_id, status = 'pending', stage } = args;
|
|
655
|
-
const { supabase } = ctx;
|
|
656
173
|
validateRequired(project_id, 'project_id');
|
|
657
174
|
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);
|
|
175
|
+
const apiClient = getApiClient();
|
|
176
|
+
const response = await apiClient.getDeploymentRequirements(project_id, {
|
|
177
|
+
status: status,
|
|
178
|
+
stage: stage
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(response.error || 'Failed to get deployment requirements');
|
|
669
182
|
}
|
|
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
|
-
};
|
|
183
|
+
return { result: response.data };
|
|
683
184
|
};
|
|
684
185
|
// ============================================================================
|
|
685
186
|
// Scheduled Deployments
|
|
686
187
|
// ============================================================================
|
|
687
188
|
export const scheduleDeployment = async (args, ctx) => {
|
|
688
189
|
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
190
|
validateRequired(project_id, 'project_id');
|
|
692
191
|
validateUUID(project_id, 'project_id');
|
|
693
192
|
validateRequired(scheduled_at, 'scheduled_at');
|
|
@@ -717,82 +216,34 @@ export const scheduleDeployment = async (args, ctx) => {
|
|
|
717
216
|
field: 'scheduled_at',
|
|
718
217
|
});
|
|
719
218
|
}
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
environment,
|
|
726
|
-
version_bump,
|
|
727
|
-
schedule_type,
|
|
219
|
+
const apiClient = getApiClient();
|
|
220
|
+
const response = await apiClient.scheduleDeployment(project_id, {
|
|
221
|
+
environment: environment,
|
|
222
|
+
version_bump: version_bump,
|
|
223
|
+
schedule_type: schedule_type,
|
|
728
224
|
scheduled_at: scheduledDate.toISOString(),
|
|
729
225
|
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,
|
|
226
|
+
notes,
|
|
227
|
+
git_ref
|
|
746
228
|
});
|
|
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
|
-
};
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
throw new Error(response.error || 'Failed to schedule deployment');
|
|
231
|
+
}
|
|
232
|
+
return { result: response.data };
|
|
759
233
|
};
|
|
760
234
|
export const getScheduledDeployments = async (args, ctx) => {
|
|
761
235
|
const { project_id, include_disabled = false } = args;
|
|
762
|
-
const { supabase } = ctx;
|
|
763
236
|
validateRequired(project_id, 'project_id');
|
|
764
237
|
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);
|
|
238
|
+
const apiClient = getApiClient();
|
|
239
|
+
const response = await apiClient.getScheduledDeployments(project_id, include_disabled);
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(response.error || 'Failed to get scheduled deployments');
|
|
772
242
|
}
|
|
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
|
-
};
|
|
243
|
+
return { result: response.data };
|
|
792
244
|
};
|
|
793
245
|
export const updateScheduledDeployment = async (args, ctx) => {
|
|
794
246
|
const { schedule_id, environment, version_bump, schedule_type, scheduled_at, auto_trigger, enabled, notes, git_ref, } = args;
|
|
795
|
-
const { supabase } = ctx;
|
|
796
247
|
validateRequired(schedule_id, 'schedule_id');
|
|
797
248
|
validateUUID(schedule_id, 'schedule_id');
|
|
798
249
|
const updates = {};
|
|
@@ -830,213 +281,46 @@ export const updateScheduledDeployment = async (args, ctx) => {
|
|
|
830
281
|
if (Object.keys(updates).length === 0) {
|
|
831
282
|
return { result: { success: false, error: 'No updates provided' } };
|
|
832
283
|
}
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
.
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
return { result: { success: true, schedule_id } };
|
|
284
|
+
const apiClient = getApiClient();
|
|
285
|
+
const response = await apiClient.updateScheduledDeployment(schedule_id, updates);
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
throw new Error(response.error || 'Failed to update scheduled deployment');
|
|
288
|
+
}
|
|
289
|
+
return { result: response.data };
|
|
840
290
|
};
|
|
841
291
|
export const deleteScheduledDeployment = async (args, ctx) => {
|
|
842
292
|
const { schedule_id } = args;
|
|
843
|
-
const { supabase } = ctx;
|
|
844
293
|
validateRequired(schedule_id, 'schedule_id');
|
|
845
294
|
validateUUID(schedule_id, 'schedule_id');
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
.
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return { result: { success: true } };
|
|
295
|
+
const apiClient = getApiClient();
|
|
296
|
+
const response = await apiClient.deleteScheduledDeployment(schedule_id);
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
throw new Error(response.error || 'Failed to delete scheduled deployment');
|
|
299
|
+
}
|
|
300
|
+
return { result: response.data };
|
|
853
301
|
};
|
|
854
302
|
export const triggerScheduledDeployment = async (args, ctx) => {
|
|
855
303
|
const { schedule_id } = args;
|
|
856
|
-
const {
|
|
857
|
-
const currentSessionId = session.currentSessionId;
|
|
304
|
+
const { session } = ctx;
|
|
858
305
|
validateRequired(schedule_id, 'schedule_id');
|
|
859
306
|
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' } };
|
|
307
|
+
const apiClient = getApiClient();
|
|
308
|
+
const response = await apiClient.triggerScheduledDeployment(schedule_id, session.currentSessionId || undefined);
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
throw new Error(response.error || 'Failed to trigger scheduled deployment');
|
|
868
311
|
}
|
|
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
|
-
};
|
|
312
|
+
return { result: response.data };
|
|
1014
313
|
};
|
|
1015
314
|
export const checkDueDeployments = async (args, ctx) => {
|
|
1016
315
|
const { project_id } = args;
|
|
1017
|
-
const { supabase } = ctx;
|
|
1018
316
|
validateRequired(project_id, 'project_id');
|
|
1019
317
|
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
|
-
};
|
|
318
|
+
const apiClient = getApiClient();
|
|
319
|
+
const response = await apiClient.checkDueDeployments(project_id);
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
throw new Error(response.error || 'Failed to check due deployments');
|
|
322
|
+
}
|
|
323
|
+
return { result: response.data };
|
|
1040
324
|
};
|
|
1041
325
|
/**
|
|
1042
326
|
* Deployment handlers registry
|