@kb-labs/workflow-engine 2.94.0 → 2.98.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/dist/index.d.ts +67 -143
- package/dist/index.js +336 -313
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFile, mkdir, writeFile, readdir } from 'fs/promises';
|
|
1
|
+
import { readFile, access, mkdir, writeFile, readdir } from 'fs/promises';
|
|
2
2
|
import { resolve, join, basename } from 'path';
|
|
3
3
|
import { parse, stringify } from 'yaml';
|
|
4
|
-
import { WorkflowSpecSchema } from '@kb-labs/workflow-contracts';
|
|
4
|
+
import { WorkflowSpecSchema, evaluateExpression } from '@kb-labs/workflow-contracts';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { WORKFLOW_REDIS_CHANNEL, EVENT_NAMES, IDEMPOTENCY_TTL_ENV, CONCURRENCY_TTL_ENV } from '@kb-labs/workflow-constants';
|
|
7
7
|
import { createFileSystemArtifactClient } from '@kb-labs/workflow-artifacts';
|
|
@@ -317,7 +317,8 @@ var RunCoordinator = class {
|
|
|
317
317
|
needs,
|
|
318
318
|
pendingDependencies: [...needs],
|
|
319
319
|
blocked: needs.length > 0,
|
|
320
|
-
priority
|
|
320
|
+
priority,
|
|
321
|
+
if: jobSpec.if
|
|
321
322
|
});
|
|
322
323
|
}
|
|
323
324
|
const workflowRun = {
|
|
@@ -857,7 +858,38 @@ var WorkflowEngine = class {
|
|
|
857
858
|
this.logger.info("Job re-queued for retry", { runId, jobId });
|
|
858
859
|
}, backoffMs);
|
|
859
860
|
} else {
|
|
860
|
-
await this.
|
|
861
|
+
await this.deadLetterJob(runId, jobId, error);
|
|
862
|
+
await this.cancelDownstreamJobs(runId, job.jobName, `upstream job "${job.jobName}" failed`);
|
|
863
|
+
await this.checkRunCompletion(runId);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Recursively cancel jobs blocked on a failed/cancelled upstream job, so the
|
|
868
|
+
* DAG does not leave dependents stuck in 'queued'. Cancellation cascades:
|
|
869
|
+
* a cancelled job also cancels its own dependents.
|
|
870
|
+
*/
|
|
871
|
+
async cancelDownstreamJobs(runId, failedJobName, reason) {
|
|
872
|
+
const released = await this.stateStore.releaseBlockedJobs(runId, failedJobName);
|
|
873
|
+
for (const downstreamJob of released) {
|
|
874
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
875
|
+
await this.stateStore.updateJob(runId, downstreamJob.id, (draft) => {
|
|
876
|
+
draft.status = "cancelled";
|
|
877
|
+
draft.blocked = false;
|
|
878
|
+
draft.finishedAt = now;
|
|
879
|
+
draft.error = { message: reason, timestamp: now };
|
|
880
|
+
});
|
|
881
|
+
await this.events.publish({
|
|
882
|
+
type: EVENT_NAMES.job.cancelled,
|
|
883
|
+
runId,
|
|
884
|
+
jobId: downstreamJob.id,
|
|
885
|
+
payload: { jobName: downstreamJob.jobName, reason }
|
|
886
|
+
});
|
|
887
|
+
this.logger.info("Cancelled dependent job after upstream failure", {
|
|
888
|
+
runId,
|
|
889
|
+
jobId: downstreamJob.id,
|
|
890
|
+
unlockedBy: failedJobName
|
|
891
|
+
});
|
|
892
|
+
await this.cancelDownstreamJobs(runId, downstreamJob.jobName, reason);
|
|
861
893
|
}
|
|
862
894
|
}
|
|
863
895
|
/**
|
|
@@ -879,7 +911,15 @@ var WorkflowEngine = class {
|
|
|
879
911
|
draft.status = "running";
|
|
880
912
|
draft.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
881
913
|
});
|
|
882
|
-
this.
|
|
914
|
+
await this.stateStore.updateRun(runId, (draft) => {
|
|
915
|
+
if (draft.status === "queued") {
|
|
916
|
+
draft.status = "running";
|
|
917
|
+
if (!draft.startedAt) {
|
|
918
|
+
draft.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
this.logger.info("Job started", { runId, jobId });
|
|
883
923
|
const run = await this.getRun(runId);
|
|
884
924
|
const job = run?.jobs.find((j) => j.id === jobId);
|
|
885
925
|
this.analytics?.track("workflow.job.started", {
|
|
@@ -924,14 +964,65 @@ var WorkflowEngine = class {
|
|
|
924
964
|
payload: { jobName: job?.jobName, durationMs: duration }
|
|
925
965
|
});
|
|
926
966
|
if (job?.jobName) {
|
|
967
|
+
const run2 = await this.stateStore.getRun(runId);
|
|
968
|
+
const exprCtx = this.buildExpressionContext(run2);
|
|
927
969
|
const released = await this.stateStore.releaseBlockedJobs(runId, job.jobName);
|
|
928
970
|
for (const releasedJob of released) {
|
|
929
|
-
|
|
930
|
-
|
|
971
|
+
if (releasedJob.if && !this.evaluateJobIf(releasedJob.if, exprCtx)) {
|
|
972
|
+
await this.skipJob(runId, releasedJob);
|
|
973
|
+
} else {
|
|
974
|
+
await this.scheduler.enqueueJob(runId, releasedJob, releasedJob.priority ?? "normal");
|
|
975
|
+
this.logger.info("Unblocked dependent job", { runId, jobId: releasedJob.id, unlockedBy: job.jobName });
|
|
976
|
+
}
|
|
931
977
|
}
|
|
932
978
|
}
|
|
933
979
|
await this.checkRunCompletion(runId);
|
|
934
980
|
}
|
|
981
|
+
/** Build a minimal ExpressionContext from run state (for job-level if evaluation). */
|
|
982
|
+
buildExpressionContext(run) {
|
|
983
|
+
const ctx = {
|
|
984
|
+
env: run?.env ?? {},
|
|
985
|
+
trigger: run?.trigger ?? { type: "manual" },
|
|
986
|
+
inputs: run?.inputs ?? {},
|
|
987
|
+
steps: {}
|
|
988
|
+
};
|
|
989
|
+
if (run) {
|
|
990
|
+
for (const j of run.jobs) {
|
|
991
|
+
for (const s of j.steps) {
|
|
992
|
+
if (s.status === "success" && s.spec.id) {
|
|
993
|
+
ctx.steps[s.spec.id] = { outputs: s.outputs ?? {} };
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return ctx;
|
|
999
|
+
}
|
|
1000
|
+
/** Evaluate a job-level `if:` expression. Strips ${{ }} wrapper if present. */
|
|
1001
|
+
evaluateJobIf(condition, ctx) {
|
|
1002
|
+
const raw = condition.trim().replace(/^\$\{\{\s*/, "").replace(/\s*\}\}$/, "");
|
|
1003
|
+
return evaluateExpression(raw, ctx);
|
|
1004
|
+
}
|
|
1005
|
+
/** Mark a job as skipped (success) and release any jobs blocked on it. */
|
|
1006
|
+
async skipJob(runId, job) {
|
|
1007
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1008
|
+
await this.stateStore.updateJob(runId, job.id, (draft) => {
|
|
1009
|
+
draft.status = "success";
|
|
1010
|
+
draft.startedAt = now;
|
|
1011
|
+
draft.finishedAt = now;
|
|
1012
|
+
});
|
|
1013
|
+
this.logger.info("Job skipped: if condition false", { runId, jobId: job.id, condition: job.if });
|
|
1014
|
+
const downstream = await this.stateStore.releaseBlockedJobs(runId, job.jobName);
|
|
1015
|
+
const run = await this.stateStore.getRun(runId);
|
|
1016
|
+
const exprCtx = this.buildExpressionContext(run);
|
|
1017
|
+
for (const downstreamJob of downstream) {
|
|
1018
|
+
if (downstreamJob.if && !this.evaluateJobIf(downstreamJob.if, exprCtx)) {
|
|
1019
|
+
await this.skipJob(runId, downstreamJob);
|
|
1020
|
+
} else {
|
|
1021
|
+
await this.scheduler.enqueueJob(runId, downstreamJob, downstreamJob.priority ?? "normal");
|
|
1022
|
+
this.logger.info("Unblocked dependent job", { runId, jobId: downstreamJob.id, unlockedBy: job.jobName });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
935
1026
|
/**
|
|
936
1027
|
* Check if all jobs in a run are completed and update run status accordingly.
|
|
937
1028
|
*/
|
|
@@ -947,7 +1038,7 @@ var WorkflowEngine = class {
|
|
|
947
1038
|
return;
|
|
948
1039
|
}
|
|
949
1040
|
if (allSuccess) {
|
|
950
|
-
await this.stateStore.updateRun(runId, (draft) => {
|
|
1041
|
+
const updated = await this.stateStore.updateRun(runId, (draft) => {
|
|
951
1042
|
draft.status = "success";
|
|
952
1043
|
draft.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
953
1044
|
return draft;
|
|
@@ -965,10 +1056,24 @@ var WorkflowEngine = class {
|
|
|
965
1056
|
runId,
|
|
966
1057
|
payload: { status: "success", name: run.name }
|
|
967
1058
|
});
|
|
1059
|
+
if (updated) {
|
|
1060
|
+
await this.snapshotTerminalRun(updated);
|
|
1061
|
+
}
|
|
968
1062
|
} else if (anyFailed) {
|
|
969
|
-
|
|
1063
|
+
const failedJob = run.jobs.find((j) => j.status === "failed");
|
|
1064
|
+
const updated = await this.stateStore.updateRun(runId, (draft) => {
|
|
970
1065
|
draft.status = "failed";
|
|
971
1066
|
draft.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1067
|
+
if (failedJob?.error) {
|
|
1068
|
+
draft.result = {
|
|
1069
|
+
status: "failed",
|
|
1070
|
+
summary: `Job ${failedJob.jobName} failed`,
|
|
1071
|
+
error: {
|
|
1072
|
+
message: failedJob.error.message,
|
|
1073
|
+
details: failedJob.error.stack ? { stack: failedJob.error.stack } : void 0
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
972
1077
|
return draft;
|
|
973
1078
|
});
|
|
974
1079
|
this.logger.info("Workflow run failed", { runId });
|
|
@@ -984,6 +1089,9 @@ var WorkflowEngine = class {
|
|
|
984
1089
|
runId,
|
|
985
1090
|
payload: { status: "failed", name: run.name }
|
|
986
1091
|
});
|
|
1092
|
+
if (updated) {
|
|
1093
|
+
await this.snapshotTerminalRun(updated);
|
|
1094
|
+
}
|
|
987
1095
|
}
|
|
988
1096
|
}
|
|
989
1097
|
/**
|
|
@@ -1215,22 +1323,14 @@ var WorkflowEngine = class {
|
|
|
1215
1323
|
/**
|
|
1216
1324
|
* Move permanently failed job to Dead Letter Queue.
|
|
1217
1325
|
*/
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
message: error.message,
|
|
1227
|
-
details: {
|
|
1228
|
-
stack: error.stack
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
};
|
|
1232
|
-
draft.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1233
|
-
});
|
|
1326
|
+
/**
|
|
1327
|
+
* Record a dead-letter entry for a job that exhausted its retries. This is an
|
|
1328
|
+
* observability artifact only — it does NOT change the run status. The run
|
|
1329
|
+
* outcome is decided by checkRunCompletion (→ 'failed'). 'dlq' as a run status
|
|
1330
|
+
* is reserved for infrastructure failures and is no longer set here (B-029).
|
|
1331
|
+
*/
|
|
1332
|
+
async deadLetterJob(runId, jobId, error) {
|
|
1333
|
+
this.logger.warn("Job dead-lettered after max retries", { runId, jobId });
|
|
1234
1334
|
const dlqKey = `workflow:dlq:${runId}:${jobId}`;
|
|
1235
1335
|
const run = await this.stateStore.getRun(runId);
|
|
1236
1336
|
const job = run?.jobs.find((j) => j.id === jobId);
|
|
@@ -1250,10 +1350,6 @@ var WorkflowEngine = class {
|
|
|
1250
1350
|
7 * 24 * 60 * 60 * 1e3
|
|
1251
1351
|
// TTL 7 days
|
|
1252
1352
|
);
|
|
1253
|
-
const updatedRun = await this.stateStore.getRun(runId);
|
|
1254
|
-
if (updatedRun) {
|
|
1255
|
-
await this.publishRunEvent(EVENT_NAMES.run.failed, updatedRun);
|
|
1256
|
-
}
|
|
1257
1353
|
}
|
|
1258
1354
|
async updateRun(runId, mutator) {
|
|
1259
1355
|
const updated = await this.stateStore.updateRun(runId, mutator);
|
|
@@ -1280,9 +1376,41 @@ var WorkflowEngine = class {
|
|
|
1280
1376
|
status === "failed" ? EVENT_NAMES.run.failed : status === "cancelled" ? EVENT_NAMES.run.cancelled : EVENT_NAMES.run.finished,
|
|
1281
1377
|
updated
|
|
1282
1378
|
);
|
|
1379
|
+
await this.snapshotTerminalRun(updated);
|
|
1283
1380
|
}
|
|
1284
1381
|
return updated;
|
|
1285
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Persist a snapshot for a run that just reached a terminal state, so it
|
|
1385
|
+
* can be replayed from any step later (`replayRun`, used by both
|
|
1386
|
+
* `workflow runs restart --from-step` and `rerun --failed-only`). Only
|
|
1387
|
+
* 'success'/'failed' carry meaningful step output data worth snapshotting.
|
|
1388
|
+
*
|
|
1389
|
+
* Shared by finalizeRun() and checkRunCompletion() — checkRunCompletion is
|
|
1390
|
+
* the run-completion path actually driven by the worker in production, so
|
|
1391
|
+
* without this call here, no snapshot is ever created outside of tests
|
|
1392
|
+
* that call finalizeRun() directly (issue #263).
|
|
1393
|
+
*/
|
|
1394
|
+
async snapshotTerminalRun(run) {
|
|
1395
|
+
if (run.status !== "success" && run.status !== "failed") {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const stepOutputs = {};
|
|
1399
|
+
for (const job of run.jobs) {
|
|
1400
|
+
for (const step of job.steps) {
|
|
1401
|
+
if (step.outputs && Object.keys(step.outputs).length > 0) {
|
|
1402
|
+
stepOutputs[step.id] = step.outputs;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
await this.snapshotStorage.createSnapshot(run, stepOutputs, run.env ?? {}).catch((err) => {
|
|
1407
|
+
this.logger.error(
|
|
1408
|
+
"Failed to create run snapshot \u2014 restart --from-step and rerun --failed-only will not work for this run",
|
|
1409
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
1410
|
+
{ runId: run.id, status: run.status }
|
|
1411
|
+
);
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1286
1414
|
async nextJob() {
|
|
1287
1415
|
return this.scheduler.dequeueJob();
|
|
1288
1416
|
}
|
|
@@ -1303,13 +1431,13 @@ var WorkflowEngine = class {
|
|
|
1303
1431
|
/**
|
|
1304
1432
|
* Publish a log entry for real-time streaming to Studio UI.
|
|
1305
1433
|
*/
|
|
1306
|
-
async publishLog(runId, jobId, stepId, entry) {
|
|
1434
|
+
async publishLog(runId, jobId, stepId, entry, stepName) {
|
|
1307
1435
|
await this.events.publish({
|
|
1308
1436
|
type: EVENT_NAMES.log.appended,
|
|
1309
1437
|
runId,
|
|
1310
1438
|
jobId,
|
|
1311
1439
|
stepId,
|
|
1312
|
-
payload: entry
|
|
1440
|
+
payload: stepName ? { ...entry, stepName } : entry
|
|
1313
1441
|
});
|
|
1314
1442
|
}
|
|
1315
1443
|
/**
|
|
@@ -1363,16 +1491,22 @@ var WorkflowEngine = class {
|
|
|
1363
1491
|
restoredRun.env = snapshot.env;
|
|
1364
1492
|
}
|
|
1365
1493
|
if (options.fromStepId) {
|
|
1494
|
+
const stepExists = restoredRun.jobs.some(
|
|
1495
|
+
(job) => job.steps.some((s) => s.id === options.fromStepId)
|
|
1496
|
+
);
|
|
1497
|
+
if (!stepExists) {
|
|
1498
|
+
throw new Error(`Step not found: ${options.fromStepId}`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (options.fromStepId) {
|
|
1502
|
+
let foundStep = false;
|
|
1366
1503
|
for (const job of restoredRun.jobs) {
|
|
1367
|
-
let foundStep = false;
|
|
1368
1504
|
for (const step of job.steps) {
|
|
1369
1505
|
if (step.id === options.fromStepId) {
|
|
1370
1506
|
foundStep = true;
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
step.finishedAt = void 0;
|
|
1375
|
-
}
|
|
1507
|
+
step.status = "queued";
|
|
1508
|
+
step.startedAt = void 0;
|
|
1509
|
+
step.finishedAt = void 0;
|
|
1376
1510
|
continue;
|
|
1377
1511
|
}
|
|
1378
1512
|
if (!foundStep) {
|
|
@@ -1396,16 +1530,41 @@ var WorkflowEngine = class {
|
|
|
1396
1530
|
}
|
|
1397
1531
|
}
|
|
1398
1532
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1533
|
+
const isJobComplete = (job) => job.steps.every((s) => s.status === "success" || s.status === "skipped");
|
|
1534
|
+
for (const job of restoredRun.jobs) {
|
|
1535
|
+
if (!job.needs || job.needs.length === 0) {
|
|
1536
|
+
job.blocked = false;
|
|
1537
|
+
job.pendingDependencies = [];
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
const pendingDeps = job.needs.filter((depName) => {
|
|
1541
|
+
const depJob = restoredRun.jobs.find((j) => j.jobName === depName);
|
|
1542
|
+
return depJob !== void 0 && !isJobComplete(depJob);
|
|
1543
|
+
});
|
|
1544
|
+
job.pendingDependencies = pendingDeps;
|
|
1545
|
+
job.blocked = pendingDeps.length > 0;
|
|
1546
|
+
}
|
|
1547
|
+
const newRunId = randomUUID();
|
|
1548
|
+
const newNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
1549
|
+
const newRun = {
|
|
1550
|
+
...restoredRun,
|
|
1551
|
+
id: newRunId,
|
|
1552
|
+
status: "queued",
|
|
1553
|
+
createdAt: newNow,
|
|
1554
|
+
queuedAt: newNow,
|
|
1555
|
+
startedAt: void 0,
|
|
1556
|
+
finishedAt: void 0,
|
|
1557
|
+
durationMs: void 0,
|
|
1558
|
+
result: void 0
|
|
1559
|
+
};
|
|
1560
|
+
await this.stateStore.saveRun(newRun);
|
|
1561
|
+
await this.scheduler.scheduleRun(newRun);
|
|
1562
|
+
this.logger.info("Run replayed as new run from snapshot", {
|
|
1563
|
+
originalRunId: runId,
|
|
1564
|
+
newRunId,
|
|
1406
1565
|
fromStepId: options.fromStepId
|
|
1407
1566
|
});
|
|
1408
|
-
return
|
|
1567
|
+
return newRun;
|
|
1409
1568
|
}
|
|
1410
1569
|
/**
|
|
1411
1570
|
* Delete a snapshot
|
|
@@ -1688,11 +1847,82 @@ var ArtifactMerger = class {
|
|
|
1688
1847
|
}
|
|
1689
1848
|
};
|
|
1690
1849
|
|
|
1850
|
+
// src/manifest-converter.ts
|
|
1851
|
+
var ManifestConverter = class {
|
|
1852
|
+
convertWorkflowHandler(pluginId, handler, pluginRoot) {
|
|
1853
|
+
const id = `${pluginId}/${handler.id}`;
|
|
1854
|
+
return {
|
|
1855
|
+
kind: "workflow",
|
|
1856
|
+
id,
|
|
1857
|
+
source: "manifest",
|
|
1858
|
+
pluginId,
|
|
1859
|
+
manifestPath: pluginRoot,
|
|
1860
|
+
name: handler.describe ?? handler.id,
|
|
1861
|
+
description: handler.describe,
|
|
1862
|
+
tags: ["plugin", pluginId],
|
|
1863
|
+
triggers: [{ type: "manual" }],
|
|
1864
|
+
handler: handler.handler,
|
|
1865
|
+
status: "active",
|
|
1866
|
+
permissions: handler.permissions,
|
|
1867
|
+
input: handler.input,
|
|
1868
|
+
output: handler.output
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
convertJobHandler(pluginId, handler, pluginRoot) {
|
|
1872
|
+
const id = `${pluginId}:job:${handler.id}`;
|
|
1873
|
+
return {
|
|
1874
|
+
kind: "job",
|
|
1875
|
+
id,
|
|
1876
|
+
source: "manifest",
|
|
1877
|
+
pluginId,
|
|
1878
|
+
manifestPath: pluginRoot,
|
|
1879
|
+
name: handler.describe ?? handler.id,
|
|
1880
|
+
description: handler.describe,
|
|
1881
|
+
tags: ["plugin", "job", pluginId],
|
|
1882
|
+
triggers: [{ type: "manual" }],
|
|
1883
|
+
handler: handler.handler,
|
|
1884
|
+
status: "active",
|
|
1885
|
+
permissions: handler.permissions,
|
|
1886
|
+
input: handler.input,
|
|
1887
|
+
output: handler.output
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
convertCronSchedule(pluginId, cronDecl, pluginRoot) {
|
|
1891
|
+
const id = `${pluginId}:cron:${cronDecl.id}`;
|
|
1892
|
+
const schedule = {
|
|
1893
|
+
cron: cronDecl.schedule,
|
|
1894
|
+
enabled: cronDecl.enabled ?? true
|
|
1895
|
+
};
|
|
1896
|
+
return {
|
|
1897
|
+
kind: "cron",
|
|
1898
|
+
id,
|
|
1899
|
+
source: "manifest",
|
|
1900
|
+
pluginId,
|
|
1901
|
+
manifestPath: pluginRoot,
|
|
1902
|
+
name: cronDecl.describe ?? cronDecl.id,
|
|
1903
|
+
description: cronDecl.describe,
|
|
1904
|
+
tags: ["plugin", "cron", pluginId],
|
|
1905
|
+
triggers: [
|
|
1906
|
+
{
|
|
1907
|
+
type: "schedule",
|
|
1908
|
+
config: { cron: cronDecl.schedule, timezone: cronDecl.timezone }
|
|
1909
|
+
}
|
|
1910
|
+
],
|
|
1911
|
+
handler: void 0,
|
|
1912
|
+
schedule,
|
|
1913
|
+
status: cronDecl.enabled ?? true ? "active" : "disabled",
|
|
1914
|
+
permissions: cronDecl.permissions
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1691
1919
|
// src/manifest-scanner.ts
|
|
1920
|
+
var CACHE_KEY_WORKFLOWS = "manifest-scanner:v1:workflows";
|
|
1692
1921
|
var ManifestScanner = class {
|
|
1693
1922
|
cliApi;
|
|
1694
1923
|
platform;
|
|
1695
1924
|
cacheTtlMs;
|
|
1925
|
+
converter = new ManifestConverter();
|
|
1696
1926
|
constructor(options) {
|
|
1697
1927
|
this.cliApi = options.cliApi;
|
|
1698
1928
|
this.platform = options.platform;
|
|
@@ -1700,13 +1930,14 @@ var ManifestScanner = class {
|
|
|
1700
1930
|
}
|
|
1701
1931
|
/**
|
|
1702
1932
|
* Scan all installed plugins for workflows and jobs.
|
|
1933
|
+
* Purpose: listing — populates the REST API GET /api/v1/workflows catalog.
|
|
1934
|
+
* (CronDiscovery in daemon handles scheduling — different output, different consumer.)
|
|
1703
1935
|
*
|
|
1704
1936
|
* Returns unified WorkflowRuntime representations.
|
|
1705
1937
|
*/
|
|
1706
1938
|
async scanPlugins() {
|
|
1707
|
-
const cacheKey = "manifest-scanner:workflows";
|
|
1708
1939
|
if (this.platform.cache) {
|
|
1709
|
-
const cached = await this.platform.cache.get(
|
|
1940
|
+
const cached = await this.platform.cache.get(CACHE_KEY_WORKFLOWS);
|
|
1710
1941
|
if (cached) {
|
|
1711
1942
|
this.platform.logger?.debug("ManifestScanner: Using cached workflows", { count: cached.length });
|
|
1712
1943
|
return cached;
|
|
@@ -1738,11 +1969,11 @@ var ManifestScanner = class {
|
|
|
1738
1969
|
}
|
|
1739
1970
|
try {
|
|
1740
1971
|
if (converter === "workflow") {
|
|
1741
|
-
workflows.push(this.convertWorkflowHandler(entity.ref.pluginId, entity.declaration, root));
|
|
1972
|
+
workflows.push(this.converter.convertWorkflowHandler(entity.ref.pluginId, entity.declaration, root));
|
|
1742
1973
|
} else if (converter === "job") {
|
|
1743
|
-
workflows.push(this.convertJobHandler(entity.ref.pluginId, entity.declaration, root));
|
|
1974
|
+
workflows.push(this.converter.convertJobHandler(entity.ref.pluginId, entity.declaration, root));
|
|
1744
1975
|
} else {
|
|
1745
|
-
workflows.push(this.convertCronSchedule(entity.ref.pluginId, entity.declaration, root));
|
|
1976
|
+
workflows.push(this.converter.convertCronSchedule(entity.ref.pluginId, entity.declaration, root));
|
|
1746
1977
|
}
|
|
1747
1978
|
} catch (err) {
|
|
1748
1979
|
this.platform.logger?.warn("ManifestScanner: Failed to convert entity", {
|
|
@@ -1781,91 +2012,10 @@ var ManifestScanner = class {
|
|
|
1781
2012
|
templates: templatesCount
|
|
1782
2013
|
});
|
|
1783
2014
|
if (this.platform.cache) {
|
|
1784
|
-
await this.platform.cache.set(
|
|
2015
|
+
await this.platform.cache.set(CACHE_KEY_WORKFLOWS, workflows, this.cacheTtlMs);
|
|
1785
2016
|
}
|
|
1786
2017
|
return workflows;
|
|
1787
2018
|
}
|
|
1788
|
-
/**
|
|
1789
|
-
* Convert workflow handler declaration to WorkflowRuntime.
|
|
1790
|
-
*/
|
|
1791
|
-
convertWorkflowHandler(pluginId, handler, pluginRoot) {
|
|
1792
|
-
const id = `${pluginId}/${handler.id}`;
|
|
1793
|
-
return {
|
|
1794
|
-
id,
|
|
1795
|
-
source: "manifest",
|
|
1796
|
-
pluginId,
|
|
1797
|
-
manifestPath: pluginRoot,
|
|
1798
|
-
name: handler.describe ?? handler.id,
|
|
1799
|
-
description: handler.describe,
|
|
1800
|
-
tags: ["plugin", pluginId],
|
|
1801
|
-
triggers: [
|
|
1802
|
-
{ type: "manual" }
|
|
1803
|
-
// Workflow handlers can always be triggered manually
|
|
1804
|
-
],
|
|
1805
|
-
handler: handler.handler,
|
|
1806
|
-
status: "active",
|
|
1807
|
-
permissions: handler.permissions,
|
|
1808
|
-
input: handler.input,
|
|
1809
|
-
output: handler.output
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* Convert job handler declaration to WorkflowRuntime.
|
|
1814
|
-
*/
|
|
1815
|
-
convertJobHandler(pluginId, handler, pluginRoot) {
|
|
1816
|
-
const id = `${pluginId}:job:${handler.id}`;
|
|
1817
|
-
return {
|
|
1818
|
-
id,
|
|
1819
|
-
source: "manifest",
|
|
1820
|
-
pluginId,
|
|
1821
|
-
manifestPath: pluginRoot,
|
|
1822
|
-
name: handler.describe ?? handler.id,
|
|
1823
|
-
description: handler.describe,
|
|
1824
|
-
tags: ["plugin", "job", pluginId],
|
|
1825
|
-
triggers: [
|
|
1826
|
-
{ type: "manual" }
|
|
1827
|
-
// Job handlers are invoked on-demand via ctx.api.jobs.submit()
|
|
1828
|
-
],
|
|
1829
|
-
handler: handler.handler,
|
|
1830
|
-
status: "active",
|
|
1831
|
-
permissions: handler.permissions,
|
|
1832
|
-
input: handler.input,
|
|
1833
|
-
output: handler.output
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Convert cron schedule declaration to WorkflowRuntime.
|
|
1838
|
-
*/
|
|
1839
|
-
convertCronSchedule(pluginId, cronDecl, pluginRoot) {
|
|
1840
|
-
const id = `${pluginId}:cron:${cronDecl.id}`;
|
|
1841
|
-
const schedule = {
|
|
1842
|
-
cron: cronDecl.schedule,
|
|
1843
|
-
enabled: cronDecl.enabled ?? true
|
|
1844
|
-
};
|
|
1845
|
-
return {
|
|
1846
|
-
id,
|
|
1847
|
-
source: "manifest",
|
|
1848
|
-
pluginId,
|
|
1849
|
-
manifestPath: pluginRoot,
|
|
1850
|
-
name: cronDecl.describe ?? cronDecl.id,
|
|
1851
|
-
description: cronDecl.describe,
|
|
1852
|
-
tags: ["plugin", "cron", pluginId],
|
|
1853
|
-
triggers: [
|
|
1854
|
-
{
|
|
1855
|
-
type: "schedule",
|
|
1856
|
-
config: { cron: cronDecl.schedule, timezone: cronDecl.timezone }
|
|
1857
|
-
}
|
|
1858
|
-
],
|
|
1859
|
-
// Note: Cron schedules reference a job type to execute
|
|
1860
|
-
// The actual handler path comes from the job declaration
|
|
1861
|
-
handler: void 0,
|
|
1862
|
-
// Will be resolved at execution time via job type
|
|
1863
|
-
schedule,
|
|
1864
|
-
status: cronDecl.enabled ?? true ? "active" : "disabled",
|
|
1865
|
-
permissions: cronDecl.permissions
|
|
1866
|
-
};
|
|
1867
|
-
}
|
|
1868
|
-
// Legacy convertLegacyJob method removed - use cron schedules instead
|
|
1869
2019
|
/**
|
|
1870
2020
|
* Scan all installed plugins for job handlers only.
|
|
1871
2021
|
*
|
|
@@ -1895,7 +2045,7 @@ var ManifestScanner = class {
|
|
|
1895
2045
|
*/
|
|
1896
2046
|
async clearCache() {
|
|
1897
2047
|
if (this.platform.cache) {
|
|
1898
|
-
await this.platform.cache.delete(
|
|
2048
|
+
await this.platform.cache.delete(CACHE_KEY_WORKFLOWS);
|
|
1899
2049
|
this.platform.logger?.debug("ManifestScanner: Cache cleared");
|
|
1900
2050
|
}
|
|
1901
2051
|
}
|
|
@@ -2007,7 +2157,10 @@ var WorkflowRepository = class {
|
|
|
2007
2157
|
* Delete workflow.
|
|
2008
2158
|
*/
|
|
2009
2159
|
async delete(id) {
|
|
2010
|
-
const path = this.getWorkflowPath(id);
|
|
2160
|
+
const path = await this.getWorkflowPath(id);
|
|
2161
|
+
if (!path) {
|
|
2162
|
+
throw new Error(`Invalid workflow ID for deletion: ${id}`);
|
|
2163
|
+
}
|
|
2011
2164
|
try {
|
|
2012
2165
|
await this.platform.storage.delete(path);
|
|
2013
2166
|
this.platform.logger?.info("WorkflowRepository: Deleted workflow", { id });
|
|
@@ -2078,21 +2231,22 @@ var WorkflowRepository = class {
|
|
|
2078
2231
|
status
|
|
2079
2232
|
});
|
|
2080
2233
|
}
|
|
2081
|
-
getWorkflowPath(id) {
|
|
2234
|
+
async getWorkflowPath(id) {
|
|
2235
|
+
if (id.includes("/") || id.includes("\\") || id.includes("..") || id.startsWith(".")) {
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2082
2238
|
const ymlPath = join(this.absoluteStorageDir, `${id}.yml`);
|
|
2083
2239
|
const yamlPath = join(this.absoluteStorageDir, `${id}.yaml`);
|
|
2084
|
-
|
|
2085
|
-
return ymlPath;
|
|
2086
|
-
}
|
|
2087
|
-
return yamlPath;
|
|
2240
|
+
return access(ymlPath).then(() => ymlPath).catch(() => yamlPath);
|
|
2088
2241
|
}
|
|
2089
2242
|
async saveWorkflow(id, workflow) {
|
|
2090
|
-
const path = this.getWorkflowPath(id);
|
|
2243
|
+
const path = await this.getWorkflowPath(id);
|
|
2244
|
+
if (!path) {
|
|
2245
|
+
throw new Error(`Invalid workflow ID for storage: ${id}`);
|
|
2246
|
+
}
|
|
2091
2247
|
const yaml = stringify(workflow, { indent: 2 });
|
|
2092
2248
|
try {
|
|
2093
|
-
|
|
2094
|
-
await mkdir(this.absoluteStorageDir, { recursive: true });
|
|
2095
|
-
}
|
|
2249
|
+
await mkdir(this.absoluteStorageDir, { recursive: true });
|
|
2096
2250
|
await writeFile(path, yaml, "utf-8");
|
|
2097
2251
|
} catch (error) {
|
|
2098
2252
|
this.platform.logger?.error(
|
|
@@ -2104,17 +2258,17 @@ var WorkflowRepository = class {
|
|
|
2104
2258
|
}
|
|
2105
2259
|
}
|
|
2106
2260
|
async loadWorkflow(id) {
|
|
2107
|
-
const path = this.getWorkflowPath(id);
|
|
2261
|
+
const path = await this.getWorkflowPath(id);
|
|
2262
|
+
if (!path) {
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2108
2265
|
try {
|
|
2109
|
-
if (!existsSync(path)) {
|
|
2110
|
-
return null;
|
|
2111
|
-
}
|
|
2112
2266
|
const content = await readFile(path, "utf-8");
|
|
2113
2267
|
const parsed = parse(content);
|
|
2114
2268
|
if (parsed.id && parsed.spec && parsed.createdAt) {
|
|
2115
|
-
return parsed;
|
|
2269
|
+
return { ...parsed, id };
|
|
2116
2270
|
}
|
|
2117
|
-
const
|
|
2271
|
+
const candidateSpec = {
|
|
2118
2272
|
name: parsed.name,
|
|
2119
2273
|
version: parsed.version || "1.0.0",
|
|
2120
2274
|
description: parsed.description,
|
|
@@ -2125,8 +2279,20 @@ var WorkflowRepository = class {
|
|
|
2125
2279
|
env: parsed.env,
|
|
2126
2280
|
secrets: parsed.secrets
|
|
2127
2281
|
};
|
|
2282
|
+
const validation = WorkflowSpecSchema.safeParse(candidateSpec);
|
|
2283
|
+
if (!validation.success) {
|
|
2284
|
+
this.platform.logger?.warn(
|
|
2285
|
+
`WorkflowRepository: skipping invalid workflow "${id}" \u2014 ${validation.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`,
|
|
2286
|
+
{ id, path }
|
|
2287
|
+
);
|
|
2288
|
+
return null;
|
|
2289
|
+
}
|
|
2290
|
+
const spec = validation.data;
|
|
2128
2291
|
const stored = {
|
|
2129
|
-
id
|
|
2292
|
+
// The filename is the canonical id for file-based workflows. Using the
|
|
2293
|
+
// inner `id:` here made list() report an id that get()/getWorkflowPath()
|
|
2294
|
+
// (which resolve by filename) could not find → POST /runs 404 (B-016).
|
|
2295
|
+
id,
|
|
2130
2296
|
spec,
|
|
2131
2297
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2132
2298
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2147,9 +2313,6 @@ var WorkflowRepository = class {
|
|
|
2147
2313
|
}
|
|
2148
2314
|
async listWorkflowFiles() {
|
|
2149
2315
|
try {
|
|
2150
|
-
if (!existsSync(this.absoluteStorageDir)) {
|
|
2151
|
-
return [];
|
|
2152
|
-
}
|
|
2153
2316
|
const files = await readdir(this.absoluteStorageDir);
|
|
2154
2317
|
return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
2155
2318
|
} catch (error) {
|
|
@@ -2212,7 +2375,9 @@ var WorkflowRepository = class {
|
|
|
2212
2375
|
// Store full spec for execution
|
|
2213
2376
|
input: spec,
|
|
2214
2377
|
// Expose declared input schema for REST API / Studio UI
|
|
2215
|
-
inputSchema: spec.inputs
|
|
2378
|
+
inputSchema: spec.inputs,
|
|
2379
|
+
version: spec.version,
|
|
2380
|
+
updatedAt: stored.updatedAt
|
|
2216
2381
|
};
|
|
2217
2382
|
}
|
|
2218
2383
|
};
|
|
@@ -2265,15 +2430,30 @@ var WorkflowService = class {
|
|
|
2265
2430
|
}
|
|
2266
2431
|
/**
|
|
2267
2432
|
* Get workflow by ID (from either source).
|
|
2433
|
+
*
|
|
2434
|
+
* Lookup order:
|
|
2435
|
+
* 1. Standalone by filename-derived id (canonical, fast path)
|
|
2436
|
+
* 2. Standalone by `name:` field — allows `--workflow-id=dev-cycle` even
|
|
2437
|
+
* when the file is named `03-dev-cycle.yml` (F8 ergonomics fix)
|
|
2438
|
+
* 3. Manifest-based by id
|
|
2439
|
+
* 4. Manifest-based by name field
|
|
2268
2440
|
*/
|
|
2269
2441
|
async get(id) {
|
|
2270
2442
|
const standalone = await this.repository.get(id);
|
|
2271
2443
|
if (standalone) {
|
|
2272
2444
|
return standalone;
|
|
2273
2445
|
}
|
|
2446
|
+
const allStandalone = await this.repository.list();
|
|
2447
|
+
const byName = allStandalone.find((w) => w.name === id);
|
|
2448
|
+
if (byName) {
|
|
2449
|
+
return byName;
|
|
2450
|
+
}
|
|
2274
2451
|
const manifestWorkflows = await this.scanner.scanPlugins();
|
|
2275
2452
|
const manifest = manifestWorkflows.find((w) => w.id === id);
|
|
2276
|
-
|
|
2453
|
+
if (manifest) {
|
|
2454
|
+
return manifest;
|
|
2455
|
+
}
|
|
2456
|
+
return manifestWorkflows.find((w) => w.name === id) ?? null;
|
|
2277
2457
|
}
|
|
2278
2458
|
/**
|
|
2279
2459
|
* Create standalone workflow.
|
|
@@ -2417,163 +2597,6 @@ var WorkflowService = class {
|
|
|
2417
2597
|
this.platform.logger?.info("WorkflowService: Manifest cache cleared");
|
|
2418
2598
|
}
|
|
2419
2599
|
};
|
|
2420
|
-
|
|
2421
|
-
// src/workflow-schedule-manager.ts
|
|
2422
|
-
var WorkflowScheduleManager = class {
|
|
2423
|
-
cronManager;
|
|
2424
|
-
workflowService;
|
|
2425
|
-
executor;
|
|
2426
|
-
platform;
|
|
2427
|
-
constructor(options) {
|
|
2428
|
-
this.cronManager = options.cronManager;
|
|
2429
|
-
this.workflowService = options.workflowService;
|
|
2430
|
-
this.executor = options.executor;
|
|
2431
|
-
this.platform = options.platform;
|
|
2432
|
-
}
|
|
2433
|
-
/**
|
|
2434
|
-
* Register all scheduled workflows with CronManager.
|
|
2435
|
-
*
|
|
2436
|
-
* Scans both manifest-based jobs and standalone workflows with schedules.
|
|
2437
|
-
*/
|
|
2438
|
-
async registerAll() {
|
|
2439
|
-
const workflows = await this.workflowService.listAll();
|
|
2440
|
-
const scheduled = workflows.filter(
|
|
2441
|
-
(w) => w.schedule && w.schedule.enabled && w.status === "active"
|
|
2442
|
-
);
|
|
2443
|
-
this.platform.logger?.info("WorkflowScheduleManager: Registering scheduled workflows", {
|
|
2444
|
-
total: workflows.length,
|
|
2445
|
-
scheduled: scheduled.length
|
|
2446
|
-
});
|
|
2447
|
-
await Promise.all(scheduled.map((workflow) => this.register(workflow)));
|
|
2448
|
-
}
|
|
2449
|
-
/**
|
|
2450
|
-
* Register single workflow schedule.
|
|
2451
|
-
*/
|
|
2452
|
-
async register(workflow) {
|
|
2453
|
-
if (!workflow.schedule || !workflow.schedule.enabled) {
|
|
2454
|
-
this.platform.logger?.warn("WorkflowScheduleManager: Cannot register workflow without schedule", {
|
|
2455
|
-
id: workflow.id
|
|
2456
|
-
});
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
const cronId = this.getCronId(workflow.id);
|
|
2460
|
-
const schedule = workflow.schedule.cron;
|
|
2461
|
-
this.cronManager.register(cronId, schedule, async (context) => {
|
|
2462
|
-
this.platform.logger?.info("WorkflowScheduleManager: Executing scheduled workflow", {
|
|
2463
|
-
workflowId: workflow.id,
|
|
2464
|
-
workflowName: workflow.name,
|
|
2465
|
-
runCount: context.runCount
|
|
2466
|
-
});
|
|
2467
|
-
try {
|
|
2468
|
-
const result = await this.executor.execute({
|
|
2469
|
-
workflowId: workflow.id,
|
|
2470
|
-
trigger: "schedule",
|
|
2471
|
-
input: {}
|
|
2472
|
-
});
|
|
2473
|
-
this.platform.logger?.info("WorkflowScheduleManager: Workflow execution started", {
|
|
2474
|
-
workflowId: workflow.id,
|
|
2475
|
-
runId: result.runId
|
|
2476
|
-
});
|
|
2477
|
-
} catch (error) {
|
|
2478
|
-
this.platform.logger?.error(
|
|
2479
|
-
"WorkflowScheduleManager: Workflow execution failed",
|
|
2480
|
-
error instanceof Error ? error : void 0,
|
|
2481
|
-
{
|
|
2482
|
-
workflowId: workflow.id,
|
|
2483
|
-
workflowName: workflow.name
|
|
2484
|
-
}
|
|
2485
|
-
);
|
|
2486
|
-
}
|
|
2487
|
-
});
|
|
2488
|
-
this.platform.logger?.debug("WorkflowScheduleManager: Registered workflow", {
|
|
2489
|
-
workflowId: workflow.id,
|
|
2490
|
-
schedule
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
/**
|
|
2494
|
-
* Unregister workflow schedule.
|
|
2495
|
-
*/
|
|
2496
|
-
async unregister(workflowId) {
|
|
2497
|
-
const cronId = this.getCronId(workflowId);
|
|
2498
|
-
this.cronManager.unregister(cronId);
|
|
2499
|
-
this.platform.logger?.debug("WorkflowScheduleManager: Unregistered workflow", {
|
|
2500
|
-
workflowId
|
|
2501
|
-
});
|
|
2502
|
-
}
|
|
2503
|
-
/**
|
|
2504
|
-
* Re-register all schedules (refresh).
|
|
2505
|
-
*
|
|
2506
|
-
* Useful after workflow changes or service restart.
|
|
2507
|
-
*/
|
|
2508
|
-
async refresh() {
|
|
2509
|
-
const allJobs = this.cronManager.list();
|
|
2510
|
-
for (const job of allJobs) {
|
|
2511
|
-
if (job.id.startsWith("workflow:")) {
|
|
2512
|
-
this.cronManager.unregister(job.id);
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
await this.registerAll();
|
|
2516
|
-
this.platform.logger?.info("WorkflowScheduleManager: Refreshed all schedules");
|
|
2517
|
-
}
|
|
2518
|
-
/**
|
|
2519
|
-
* Get next run time for scheduled workflow.
|
|
2520
|
-
*/
|
|
2521
|
-
getNextRun(workflowId) {
|
|
2522
|
-
const cronId = this.getCronId(workflowId);
|
|
2523
|
-
const job = this.cronManager.list().find((j) => j.id === cronId);
|
|
2524
|
-
return job?.nextRun ?? null;
|
|
2525
|
-
}
|
|
2526
|
-
/**
|
|
2527
|
-
* Get last run time for scheduled workflow.
|
|
2528
|
-
*/
|
|
2529
|
-
getLastRun(workflowId) {
|
|
2530
|
-
const cronId = this.getCronId(workflowId);
|
|
2531
|
-
const job = this.cronManager.list().find((j) => j.id === cronId);
|
|
2532
|
-
return job?.lastRun ?? null;
|
|
2533
|
-
}
|
|
2534
|
-
/**
|
|
2535
|
-
* Pause scheduled workflow.
|
|
2536
|
-
*/
|
|
2537
|
-
pause(workflowId) {
|
|
2538
|
-
const cronId = this.getCronId(workflowId);
|
|
2539
|
-
this.cronManager.pause(cronId);
|
|
2540
|
-
this.platform.logger?.info("WorkflowScheduleManager: Paused workflow schedule", {
|
|
2541
|
-
workflowId
|
|
2542
|
-
});
|
|
2543
|
-
}
|
|
2544
|
-
/**
|
|
2545
|
-
* Resume paused workflow schedule.
|
|
2546
|
-
*/
|
|
2547
|
-
resume(workflowId) {
|
|
2548
|
-
const cronId = this.getCronId(workflowId);
|
|
2549
|
-
this.cronManager.resume(cronId);
|
|
2550
|
-
this.platform.logger?.info("WorkflowScheduleManager: Resumed workflow schedule", {
|
|
2551
|
-
workflowId
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
/**
|
|
2555
|
-
* List all scheduled workflows.
|
|
2556
|
-
*/
|
|
2557
|
-
listScheduled() {
|
|
2558
|
-
return this.cronManager.list().filter((job) => job.id.startsWith("workflow:")).map((job) => ({
|
|
2559
|
-
workflowId: this.getWorkflowId(job.id),
|
|
2560
|
-
schedule: job.schedule,
|
|
2561
|
-
status: job.status,
|
|
2562
|
-
lastRun: job.lastRun,
|
|
2563
|
-
nextRun: job.nextRun,
|
|
2564
|
-
runCount: job.runCount
|
|
2565
|
-
}));
|
|
2566
|
-
}
|
|
2567
|
-
// ============================================================================
|
|
2568
|
-
// Private helpers
|
|
2569
|
-
// ============================================================================
|
|
2570
|
-
getCronId(workflowId) {
|
|
2571
|
-
return `workflow:${workflowId}`;
|
|
2572
|
-
}
|
|
2573
|
-
getWorkflowId(cronId) {
|
|
2574
|
-
return cronId.replace("workflow:", "");
|
|
2575
|
-
}
|
|
2576
|
-
};
|
|
2577
2600
|
var WorkflowRegistry = class {
|
|
2578
2601
|
constructor(options) {
|
|
2579
2602
|
this.options = options;
|
|
@@ -3121,6 +3144,6 @@ var JobManager = class {
|
|
|
3121
3144
|
}
|
|
3122
3145
|
};
|
|
3123
3146
|
|
|
3124
|
-
export { ArtifactMerger, ConcurrencyManager, EnvSecretProvider, EventBusBridge, JobManager, ManifestScanner, RunCoordinator, RunSnapshotStorage, Scheduler, StateStore, WorkflowEngine, WorkflowLoader, WorkflowRegistry, WorkflowRepository,
|
|
3147
|
+
export { ArtifactMerger, ConcurrencyManager, EnvSecretProvider, EventBusBridge, JobManager, ManifestConverter, ManifestScanner, RunCoordinator, RunSnapshotStorage, Scheduler, StateStore, WorkflowEngine, WorkflowLoader, WorkflowRegistry, WorkflowRepository, WorkflowService, calculateBackoff, createDefaultSecretProvider, shouldRetry };
|
|
3125
3148
|
//# sourceMappingURL=index.js.map
|
|
3126
3149
|
//# sourceMappingURL=index.js.map
|