@ebowwa/workflows 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 +257 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4641 -0
- package/dist/mcp/index.d.ts +57 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +17545 -0
- package/dist/runner.d.ts +62 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/sandbox.d.ts +67 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/scheduler.d.ts +93 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/types.d.ts +823 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +72 -0
- package/src/mcp/index.ts +572 -0
- package/src/runner.ts +308 -0
- package/src/sandbox.ts +305 -0
- package/src/scheduler.ts +287 -0
- package/src/types.ts +218 -0
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Scheduler with dependency resolution
|
|
3
|
+
*
|
|
4
|
+
* Handles job ordering, parallel execution where possible,
|
|
5
|
+
* and dependency graph management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Job, JobRun, JobRunStatus, StepRunStatus } from './types.js';
|
|
9
|
+
|
|
10
|
+
export interface JobNode {
|
|
11
|
+
id: string;
|
|
12
|
+
job: Job;
|
|
13
|
+
dependencies: string[];
|
|
14
|
+
dependents: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExecutionPlan {
|
|
18
|
+
/** Jobs that can run in parallel, grouped by level */
|
|
19
|
+
levels: string[][];
|
|
20
|
+
/** Total number of jobs */
|
|
21
|
+
total: number;
|
|
22
|
+
/** Whether the graph has cycles */
|
|
23
|
+
hasCycles: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a dependency graph from jobs
|
|
28
|
+
*/
|
|
29
|
+
export function buildDependencyGraph(jobs: Record<string, Job>): Map<string, JobNode> {
|
|
30
|
+
const graph = new Map<string, JobNode>();
|
|
31
|
+
|
|
32
|
+
// Create nodes
|
|
33
|
+
for (const [id, job] of Object.entries(jobs)) {
|
|
34
|
+
graph.set(id, {
|
|
35
|
+
id,
|
|
36
|
+
job,
|
|
37
|
+
dependencies: job.needs ?? [],
|
|
38
|
+
dependents: [],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Build reverse dependencies (dependents)
|
|
43
|
+
for (const [id, node] of graph) {
|
|
44
|
+
for (const depId of node.dependencies) {
|
|
45
|
+
const depNode = graph.get(depId);
|
|
46
|
+
if (depNode) {
|
|
47
|
+
depNode.dependents.push(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return graph;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create an execution plan from jobs
|
|
57
|
+
* Groups jobs by dependency level for parallel execution
|
|
58
|
+
*/
|
|
59
|
+
export function createExecutionPlan(jobs: Record<string, Job>): ExecutionPlan {
|
|
60
|
+
const graph = buildDependencyGraph(jobs);
|
|
61
|
+
const levels: string[][] = [];
|
|
62
|
+
const visited = new Set<string>();
|
|
63
|
+
const inProgress = new Set<string>();
|
|
64
|
+
|
|
65
|
+
// Detect cycles using DFS
|
|
66
|
+
let hasCycles = false;
|
|
67
|
+
function detectCycles(nodeId: string, path: string[]): boolean {
|
|
68
|
+
if (path.includes(nodeId)) {
|
|
69
|
+
hasCycles = true;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (visited.has(nodeId)) return false;
|
|
73
|
+
|
|
74
|
+
const node = graph.get(nodeId);
|
|
75
|
+
if (!node) return false;
|
|
76
|
+
|
|
77
|
+
path.push(nodeId);
|
|
78
|
+
for (const depId of node.dependencies) {
|
|
79
|
+
if (detectCycles(depId, [...path])) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
visited.add(nodeId);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for cycles
|
|
88
|
+
for (const nodeId of graph.keys()) {
|
|
89
|
+
if (!visited.has(nodeId)) {
|
|
90
|
+
detectCycles(nodeId, []);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Reset visited for level calculation
|
|
95
|
+
visited.clear();
|
|
96
|
+
|
|
97
|
+
// Calculate levels using topological sort
|
|
98
|
+
function getLevel(nodeId: string): number {
|
|
99
|
+
if (visited.has(nodeId)) {
|
|
100
|
+
return levels.findIndex(level => level.includes(nodeId));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const node = graph.get(nodeId);
|
|
104
|
+
if (!node) return 0;
|
|
105
|
+
|
|
106
|
+
// If no dependencies, level 0
|
|
107
|
+
if (node.dependencies.length === 0) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Level = max(dependency levels) + 1
|
|
112
|
+
let maxDepLevel = -1;
|
|
113
|
+
for (const depId of node.dependencies) {
|
|
114
|
+
const depLevel = getLevel(depId);
|
|
115
|
+
maxDepLevel = Math.max(maxDepLevel, depLevel);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return maxDepLevel + 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Group nodes by level
|
|
122
|
+
for (const nodeId of graph.keys()) {
|
|
123
|
+
const level = getLevel(nodeId);
|
|
124
|
+
|
|
125
|
+
while (levels.length <= level) {
|
|
126
|
+
levels.push([]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
levels[level].push(nodeId);
|
|
130
|
+
visited.add(nodeId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
levels,
|
|
135
|
+
total: graph.size,
|
|
136
|
+
hasCycles,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get jobs that are ready to run (all dependencies satisfied)
|
|
142
|
+
*/
|
|
143
|
+
export function getReadyJobs(
|
|
144
|
+
graph: Map<string, JobNode>,
|
|
145
|
+
completed: Set<string>,
|
|
146
|
+
running: Set<string>,
|
|
147
|
+
failed: Set<string>
|
|
148
|
+
): string[] {
|
|
149
|
+
const ready: string[] = [];
|
|
150
|
+
|
|
151
|
+
for (const [id, node] of graph) {
|
|
152
|
+
// Skip if already completed, running, or failed
|
|
153
|
+
if (completed.has(id) || running.has(id) || failed.has(id)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if all dependencies are satisfied
|
|
158
|
+
const depsSatisfied = node.dependencies.every(depId => completed.has(depId));
|
|
159
|
+
const depsFailed = node.dependencies.some(depId => failed.has(depId));
|
|
160
|
+
|
|
161
|
+
// If any dependency failed, this job should be skipped
|
|
162
|
+
if (depsFailed) {
|
|
163
|
+
// Mark as failed (will be skipped)
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (depsSatisfied) {
|
|
168
|
+
ready.push(id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return ready;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Job Scheduler class
|
|
177
|
+
*/
|
|
178
|
+
export class JobScheduler {
|
|
179
|
+
private graph: Map<string, JobNode>;
|
|
180
|
+
private completed = new Set<string>();
|
|
181
|
+
private running = new Set<string>();
|
|
182
|
+
private failed = new Set<string>();
|
|
183
|
+
private skipped = new Set<string>();
|
|
184
|
+
|
|
185
|
+
constructor(jobs: Record<string, Job>) {
|
|
186
|
+
this.graph = buildDependencyGraph(jobs);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get the execution plan
|
|
191
|
+
*/
|
|
192
|
+
getPlan(): ExecutionPlan {
|
|
193
|
+
const jobs: Record<string, Job> = {};
|
|
194
|
+
for (const [id, node] of this.graph) {
|
|
195
|
+
jobs[id] = node.job;
|
|
196
|
+
}
|
|
197
|
+
return createExecutionPlan(jobs);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get jobs ready to run
|
|
202
|
+
*/
|
|
203
|
+
getReadyJobs(): string[] {
|
|
204
|
+
return getReadyJobs(this.graph, this.completed, this.running, this.failed);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Mark a job as started
|
|
209
|
+
*/
|
|
210
|
+
startJob(jobId: string): void {
|
|
211
|
+
this.running.add(jobId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Mark a job as completed
|
|
216
|
+
*/
|
|
217
|
+
completeJob(jobId: string, success: boolean): void {
|
|
218
|
+
this.running.delete(jobId);
|
|
219
|
+
|
|
220
|
+
if (success) {
|
|
221
|
+
this.completed.add(jobId);
|
|
222
|
+
} else {
|
|
223
|
+
this.failed.add(jobId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Skip a job (due to failed dependencies)
|
|
229
|
+
*/
|
|
230
|
+
skipJob(jobId: string): void {
|
|
231
|
+
this.skipped.add(jobId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if all jobs are done
|
|
236
|
+
*/
|
|
237
|
+
isComplete(): boolean {
|
|
238
|
+
const total = this.graph.size;
|
|
239
|
+
const done = this.completed.size + this.failed.size + this.skipped.size;
|
|
240
|
+
return done === total;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get status summary
|
|
245
|
+
*/
|
|
246
|
+
getStatus(): {
|
|
247
|
+
total: number;
|
|
248
|
+
completed: number;
|
|
249
|
+
running: number;
|
|
250
|
+
failed: number;
|
|
251
|
+
skipped: number;
|
|
252
|
+
pending: number;
|
|
253
|
+
} {
|
|
254
|
+
const total = this.graph.size;
|
|
255
|
+
const pending = total - this.completed.size - this.running.size - this.failed.size - this.skipped.size;
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
total,
|
|
259
|
+
completed: this.completed.size,
|
|
260
|
+
running: this.running.size,
|
|
261
|
+
failed: this.failed.size,
|
|
262
|
+
skipped: this.skipped.size,
|
|
263
|
+
pending,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get job by ID
|
|
269
|
+
*/
|
|
270
|
+
getJob(jobId: string): Job | undefined {
|
|
271
|
+
return this.graph.get(jobId)?.job;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all job IDs
|
|
276
|
+
*/
|
|
277
|
+
getJobIds(): string[] {
|
|
278
|
+
return Array.from(this.graph.keys());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check for cycles in the dependency graph
|
|
283
|
+
*/
|
|
284
|
+
hasCycles(): boolean {
|
|
285
|
+
return this.getPlan().hasCycles;
|
|
286
|
+
}
|
|
287
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for CI/CD Workflow System
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Trigger Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export const TriggerSchema = z.discriminatedUnion('type', [
|
|
12
|
+
z.object({ type: z.literal('manual') }),
|
|
13
|
+
z.object({ type: z.literal('schedule'), cron: z.string() }),
|
|
14
|
+
z.object({ type: z.literal('webhook'), path: z.string() }),
|
|
15
|
+
z.object({ type: z.literal('git'), events: z.array(z.enum(['push', 'pr', 'merge'])) }),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export type Trigger = z.infer<typeof TriggerSchema>;
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Step Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
export const StepSchema = z.object({
|
|
25
|
+
id: z.string().optional(),
|
|
26
|
+
name: z.string().optional(),
|
|
27
|
+
run: z.string(),
|
|
28
|
+
cwd: z.string().optional(),
|
|
29
|
+
env: z.record(z.string()).optional(),
|
|
30
|
+
condition: z.string().optional(), // Expression for conditional execution
|
|
31
|
+
retry: z.number().optional(),
|
|
32
|
+
timeout: z.number().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type Step = z.infer<typeof StepSchema>;
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Job Types
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
export const JobSchema = z.object({
|
|
42
|
+
name: z.string(),
|
|
43
|
+
runsOn: z.enum(['bun', 'tsx', 'rust', 'bash']),
|
|
44
|
+
steps: z.array(StepSchema),
|
|
45
|
+
needs: z.array(z.string()).optional(), // Job dependencies
|
|
46
|
+
env: z.record(z.string()).optional(),
|
|
47
|
+
timeout: z.number().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type Job = z.infer<typeof JobSchema>;
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Workflow Types
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
export const WorkflowSchema = z.object({
|
|
57
|
+
id: z.string(),
|
|
58
|
+
name: z.string(),
|
|
59
|
+
description: z.string().optional(),
|
|
60
|
+
triggers: z.array(TriggerSchema),
|
|
61
|
+
jobs: z.record(z.string(), JobSchema),
|
|
62
|
+
env: z.record(z.string()).optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export type Workflow = z.infer<typeof WorkflowSchema>;
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Run Types (Execution State)
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export const StepRunStatusSchema = z.enum(['pending', 'running', 'success', 'failed', 'skipped']);
|
|
72
|
+
export type StepRunStatus = z.infer<typeof StepRunStatusSchema>;
|
|
73
|
+
|
|
74
|
+
export const JobRunStatusSchema = z.enum(['pending', 'running', 'success', 'failed', 'skipped', 'cancelled']);
|
|
75
|
+
export type JobRunStatus = z.infer<typeof JobRunStatusSchema>;
|
|
76
|
+
|
|
77
|
+
export const WorkflowRunStatusSchema = z.enum(['pending', 'running', 'success', 'failed', 'cancelled']);
|
|
78
|
+
export type WorkflowRunStatus = z.infer<typeof WorkflowRunStatusSchema>;
|
|
79
|
+
|
|
80
|
+
export const StepRunSchema = z.object({
|
|
81
|
+
id: z.string(),
|
|
82
|
+
stepId: z.string(),
|
|
83
|
+
status: StepRunStatusSchema,
|
|
84
|
+
exitCode: z.number().optional(),
|
|
85
|
+
duration: z.number().optional(),
|
|
86
|
+
startedAt: z.date().optional(),
|
|
87
|
+
finishedAt: z.date().optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export type StepRun = z.infer<typeof StepRunSchema>;
|
|
91
|
+
|
|
92
|
+
export const LogEntrySchema = z.object({
|
|
93
|
+
timestamp: z.date(),
|
|
94
|
+
jobId: z.string(),
|
|
95
|
+
stepId: z.string(),
|
|
96
|
+
stream: z.enum(['stdout', 'stderr']),
|
|
97
|
+
content: z.string(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export type LogEntry = z.infer<typeof LogEntrySchema>;
|
|
101
|
+
|
|
102
|
+
export const JobRunSchema = z.object({
|
|
103
|
+
id: z.string(),
|
|
104
|
+
jobId: z.string(),
|
|
105
|
+
status: JobRunStatusSchema,
|
|
106
|
+
steps: z.array(StepRunSchema),
|
|
107
|
+
logs: z.array(LogEntrySchema).optional(),
|
|
108
|
+
startedAt: z.date().optional(),
|
|
109
|
+
finishedAt: z.date().optional(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export type JobRun = z.infer<typeof JobRunSchema>;
|
|
113
|
+
|
|
114
|
+
export const WorkflowRunSchema = z.object({
|
|
115
|
+
id: z.string(),
|
|
116
|
+
workflowId: z.string(),
|
|
117
|
+
status: WorkflowRunStatusSchema,
|
|
118
|
+
trigger: TriggerSchema,
|
|
119
|
+
jobs: z.record(z.string(), JobRunSchema),
|
|
120
|
+
startedAt: z.date(),
|
|
121
|
+
finishedAt: z.date().optional(),
|
|
122
|
+
env: z.record(z.string()).optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export type WorkflowRun = z.infer<typeof WorkflowRunSchema>;
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Execution Context Types
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
export interface StepContext {
|
|
132
|
+
workspace: string;
|
|
133
|
+
env: Record<string, string>;
|
|
134
|
+
jobId: string;
|
|
135
|
+
stepIndex: number;
|
|
136
|
+
runId: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface JobContext {
|
|
140
|
+
workspace: string;
|
|
141
|
+
env: Record<string, string>;
|
|
142
|
+
runId: string;
|
|
143
|
+
jobId: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface StepResult {
|
|
147
|
+
exitCode: number;
|
|
148
|
+
stdout: string;
|
|
149
|
+
stderr: string;
|
|
150
|
+
duration: number;
|
|
151
|
+
succeeded: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// WebSocket Message Types
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
export const ServerMessageSchema = z.discriminatedUnion('type', [
|
|
159
|
+
z.object({ type: z.literal('job_started'), jobId: z.string() }),
|
|
160
|
+
z.object({ type: z.literal('step_started'), jobId: z.string(), stepId: z.string() }),
|
|
161
|
+
z.object({ type: z.literal('log'), jobId: z.string(), stepId: z.string(), content: z.string(), stream: z.enum(['stdout', 'stderr']) }),
|
|
162
|
+
z.object({ type: z.literal('step_finished'), jobId: z.string(), stepId: z.string(), exitCode: z.number(), duration: z.number() }),
|
|
163
|
+
z.object({ type: z.literal('job_finished'), jobId: z.string(), status: z.string() }),
|
|
164
|
+
z.object({ type: z.literal('workflow_finished'), status: z.string() }),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
export type ServerMessage = z.infer<typeof ServerMessageSchema>;
|
|
168
|
+
|
|
169
|
+
export const ClientMessageSchema = z.discriminatedUnion('type', [
|
|
170
|
+
z.object({ type: z.literal('subscribe'), runId: z.string() }),
|
|
171
|
+
z.object({ type: z.literal('cancel') }),
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
export type ClientMessage = z.infer<typeof ClientMessageSchema>;
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Event Emitter Types
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
export type WorkflowEventType =
|
|
181
|
+
| 'workflow:started'
|
|
182
|
+
| 'workflow:finished'
|
|
183
|
+
| 'job:started'
|
|
184
|
+
| 'job:finished'
|
|
185
|
+
| 'step:started'
|
|
186
|
+
| 'step:log'
|
|
187
|
+
| 'step:finished';
|
|
188
|
+
|
|
189
|
+
export interface WorkflowEvent {
|
|
190
|
+
type: WorkflowEventType;
|
|
191
|
+
runId: string;
|
|
192
|
+
jobId?: string;
|
|
193
|
+
stepId?: string;
|
|
194
|
+
data?: unknown;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// API Types
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
export interface CreateWorkflowRequest {
|
|
202
|
+
workflow: Workflow;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface TriggerWorkflowRequest {
|
|
206
|
+
trigger?: Trigger;
|
|
207
|
+
env?: Record<string, string>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface WorkflowListResponse {
|
|
211
|
+
workflows: Workflow[];
|
|
212
|
+
total: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface RunListResponse {
|
|
216
|
+
runs: WorkflowRun[];
|
|
217
|
+
total: number;
|
|
218
|
+
}
|