@cloud-cli/on 0.1.7 → 1.2.2
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 +241 -0
- package/dist/config.js +15 -0
- package/dist/db-client.js +38 -0
- package/dist/drivers/index.js +11 -0
- package/dist/drivers/standard-process.driver.js +156 -0
- package/dist/drivers/systemd.driver.js +151 -0
- package/dist/evaluator/safe-eval.js +213 -0
- package/dist/index.js +145 -0
- package/dist/ingress/preprocessors/github.js +30 -0
- package/dist/ingress/server.js +241 -0
- package/dist/ingress/types.js +1 -0
- package/dist/logging/redactor.js +20 -0
- package/dist/parser/include-resolver.js +47 -0
- package/dist/parser/matrix-expander.js +43 -0
- package/dist/parser/yaml-loader.js +45 -0
- package/dist/plugins/github-status.plugin.js +19 -0
- package/dist/plugins/manager.js +21 -0
- package/dist/plugins/types.js +1 -0
- package/dist/queue/dispatcher.js +112 -0
- package/dist/reporters/html.reporter.js +108 -0
- package/dist/reporters/json-file.reporter.js +16 -0
- package/dist/reporters/slack.reporter.js +25 -0
- package/dist/reporters/types.js +1 -0
- package/dist/runner/step-runner.js +42 -0
- package/dist/secrets/store.js +40 -0
- package/dist/types.js +1 -0
- package/dist/worker.js +249 -0
- package/package.json +31 -16
- package/dist/on.js +0 -5164
package/dist/worker.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { resolveDriver } from './drivers/index.js';
|
|
2
|
+
import { SafeExpressionEvaluator } from './evaluator/safe-eval.js';
|
|
3
|
+
export function startWorkers(count, queue, secrets, config) {
|
|
4
|
+
return Array.from({ length: count }, (_, i) => startWorkerLoop(`worker-${i + 1}`, queue, secrets, config));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Main worker loop: continuously polls the SQLite queue for pending jobs.
|
|
8
|
+
*/
|
|
9
|
+
export async function startWorkerLoop(workerId, queue, secrets, config) {
|
|
10
|
+
const driver = await resolveDriver();
|
|
11
|
+
console.log(`[${workerId}] 🚀 Worker started. Driver: ${driver.name}`);
|
|
12
|
+
while (true) {
|
|
13
|
+
try {
|
|
14
|
+
const job = await queue.claimNextJob();
|
|
15
|
+
if (!job) {
|
|
16
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
await processJob(workerId, job, queue, secrets, config, driver);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error(`[${workerId}] ⚠️ Worker execution loop error:`, error);
|
|
23
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Orchestrates the complete execution lifecycle for a claimed job.
|
|
29
|
+
*/
|
|
30
|
+
async function processJob(workerId, job, queue, secrets, config, driver) {
|
|
31
|
+
console.log(`\n[${workerId}] 📦 Claimed Job #${job.id} (Workflow: ${job.workflow_id})`);
|
|
32
|
+
const payload = typeof job.payload === 'string' ? JSON.parse(job.payload) : job.payload;
|
|
33
|
+
const steps = payload.steps || [];
|
|
34
|
+
const inputs = payload.inputs || {};
|
|
35
|
+
const jobStartTime = Date.now();
|
|
36
|
+
const executionContext = {
|
|
37
|
+
inputs,
|
|
38
|
+
env: { ...config.env },
|
|
39
|
+
secrets: secrets.getAll(),
|
|
40
|
+
steps: {},
|
|
41
|
+
};
|
|
42
|
+
const stepReports = [];
|
|
43
|
+
let jobFailed = false;
|
|
44
|
+
let isCancelled = false;
|
|
45
|
+
// Execute steps sequentially
|
|
46
|
+
for (let i = 0; i < steps.length; i++) {
|
|
47
|
+
const step = steps[i];
|
|
48
|
+
const stepResult = await executeSingleStep({
|
|
49
|
+
workerId,
|
|
50
|
+
jobId: job.id,
|
|
51
|
+
step,
|
|
52
|
+
stepIndex: i,
|
|
53
|
+
totalSteps: steps.length,
|
|
54
|
+
executionContext,
|
|
55
|
+
driver,
|
|
56
|
+
queue,
|
|
57
|
+
config,
|
|
58
|
+
});
|
|
59
|
+
stepReports.push(stepResult.report);
|
|
60
|
+
if (stepResult.isCancelled) {
|
|
61
|
+
isCancelled = true;
|
|
62
|
+
jobFailed = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (stepResult.failed) {
|
|
66
|
+
jobFailed = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Mark unexecuted steps as skipped if execution halted early
|
|
71
|
+
if (jobFailed && stepReports.length < steps.length) {
|
|
72
|
+
fillSkippedSteps(steps, stepReports.length, stepReports);
|
|
73
|
+
}
|
|
74
|
+
const finalStatus = isCancelled ? 'cancelled' : jobFailed ? 'failed' : 'success';
|
|
75
|
+
// Persist status and execution report to SQLite Queue
|
|
76
|
+
await queue.finishJob(job.id, finalStatus);
|
|
77
|
+
console.log(`[${workerId}] ✅ Job #${job.id} completed as: ${finalStatus}`);
|
|
78
|
+
const executionReport = buildExecutionReport(job, finalStatus, jobStartTime, inputs, executionContext.env, stepReports, payload);
|
|
79
|
+
await queue.saveReport(job.id, executionReport);
|
|
80
|
+
// Dispatch reports to all registered plugins/reporters
|
|
81
|
+
await dispatchReporters(workerId, config.reporters, executionReport);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Routes step execution to either JS Eval or Process Driver handler.
|
|
85
|
+
*/
|
|
86
|
+
async function executeSingleStep(params) {
|
|
87
|
+
const { workerId, step, stepIndex, totalSteps, executionContext } = params;
|
|
88
|
+
const stepId = step.id || `step-${stepIndex}`;
|
|
89
|
+
const stepName = step.name || stepId;
|
|
90
|
+
console.log(`[${workerId}] ▶️ Running step ${stepIndex + 1}/${totalSteps}: ${stepName}`);
|
|
91
|
+
if (step.eval) {
|
|
92
|
+
return executeEvalStep(stepId, stepName, step.eval, executionContext);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return executeRunStep({ ...params, stepId, stepName });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Executes an in-process JS 'eval:' step.
|
|
100
|
+
*/
|
|
101
|
+
async function executeEvalStep(stepId, stepName, evalExpr, executionContext) {
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
try {
|
|
104
|
+
const evalResult = await SafeExpressionEvaluator.evaluateExpression(evalExpr, executionContext);
|
|
105
|
+
executionContext.steps[stepId] = { status: 'success', outputs: evalResult };
|
|
106
|
+
console.log(`[${stepId}] ✅ JS Eval step complete.`);
|
|
107
|
+
return {
|
|
108
|
+
failed: false,
|
|
109
|
+
isCancelled: false,
|
|
110
|
+
report: {
|
|
111
|
+
id: stepId,
|
|
112
|
+
name: stepName,
|
|
113
|
+
status: 'success',
|
|
114
|
+
durationMs: Date.now() - startTime,
|
|
115
|
+
exitCode: 0,
|
|
116
|
+
outputs: evalResult || {},
|
|
117
|
+
logFilePath: '',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.error(`[${stepId}] ❌ JS Eval step failed:`, err.message);
|
|
123
|
+
executionContext.steps[stepId] = { status: 'failed', error: err.message };
|
|
124
|
+
return {
|
|
125
|
+
failed: true,
|
|
126
|
+
isCancelled: false,
|
|
127
|
+
report: {
|
|
128
|
+
id: stepId,
|
|
129
|
+
name: stepName,
|
|
130
|
+
status: 'failed',
|
|
131
|
+
durationMs: Date.now() - startTime,
|
|
132
|
+
exitCode: 1,
|
|
133
|
+
error: err.message,
|
|
134
|
+
outputs: {},
|
|
135
|
+
logFilePath: '',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Executes an out-of-process 'run:' step via the system execution driver.
|
|
142
|
+
*/
|
|
143
|
+
async function executeRunStep(params) {
|
|
144
|
+
const { workerId, jobId, step, stepId, stepName, executionContext, driver, queue, config } = params;
|
|
145
|
+
// Evaluate step environment variables using deterministic evaluateValue rule
|
|
146
|
+
const evaluatedStepEnv = {};
|
|
147
|
+
if (step.env) {
|
|
148
|
+
for (const [key, val] of Object.entries(step.env)) {
|
|
149
|
+
evaluatedStepEnv[key] = String(await SafeExpressionEvaluator.evaluateValue(val, executionContext));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const stepCtx = {
|
|
153
|
+
jobId: jobId.toString(),
|
|
154
|
+
stepId,
|
|
155
|
+
workspacePath: `${config.storagePath || '/tmp/workspaces'}/job-${jobId}`,
|
|
156
|
+
command: step.run,
|
|
157
|
+
image: step.image,
|
|
158
|
+
env: {
|
|
159
|
+
...executionContext.env,
|
|
160
|
+
...evaluatedStepEnv,
|
|
161
|
+
},
|
|
162
|
+
timeoutMs: step.timeoutMs,
|
|
163
|
+
};
|
|
164
|
+
const handle = await driver.execute(stepCtx);
|
|
165
|
+
let isCancelled = false;
|
|
166
|
+
// Periodically poll SQLite for mid-run job cancellation signals
|
|
167
|
+
const cancelCheckInterval = setInterval(async () => {
|
|
168
|
+
if (await queue.isCancelled(+jobId)) {
|
|
169
|
+
console.log(`[${workerId}] 🛑 Job #${jobId} was cancelled! Halting step execution.`);
|
|
170
|
+
isCancelled = true;
|
|
171
|
+
clearInterval(cancelCheckInterval);
|
|
172
|
+
await handle.cancel();
|
|
173
|
+
}
|
|
174
|
+
}, 3000);
|
|
175
|
+
const result = await handle.done;
|
|
176
|
+
clearInterval(cancelCheckInterval);
|
|
177
|
+
const failed = result.exitCode !== 0 || isCancelled;
|
|
178
|
+
const stepStatus = result.exitCode === 0 ? 'success' : isCancelled ? 'cancelled' : 'failed';
|
|
179
|
+
if (failed) {
|
|
180
|
+
console.error(`[${workerId}] ❌ Step [${stepId}] failed with status: ${stepStatus}`);
|
|
181
|
+
}
|
|
182
|
+
executionContext.steps[stepId] = { status: stepStatus, exitCode: result.exitCode };
|
|
183
|
+
return {
|
|
184
|
+
failed,
|
|
185
|
+
isCancelled,
|
|
186
|
+
report: {
|
|
187
|
+
id: stepId,
|
|
188
|
+
name: stepName,
|
|
189
|
+
status: stepStatus,
|
|
190
|
+
durationMs: result.durationMs,
|
|
191
|
+
exitCode: result.exitCode,
|
|
192
|
+
error: result.error?.message,
|
|
193
|
+
outputs: executionContext.steps[stepId]?.outputs || {},
|
|
194
|
+
logFilePath: handle.logFilePath,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Fills skipped status for remaining unexecuted steps.
|
|
200
|
+
*/
|
|
201
|
+
function fillSkippedSteps(steps, startIndex, stepReports) {
|
|
202
|
+
for (let j = startIndex; j < steps.length; j++) {
|
|
203
|
+
const skippedStep = steps[j];
|
|
204
|
+
const stepId = skippedStep.id || `step-${j}`;
|
|
205
|
+
stepReports.push({
|
|
206
|
+
id: stepId,
|
|
207
|
+
name: skippedStep.name || stepId,
|
|
208
|
+
status: 'skipped',
|
|
209
|
+
durationMs: 0,
|
|
210
|
+
exitCode: 0,
|
|
211
|
+
outputs: {},
|
|
212
|
+
logFilePath: '',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Assembles the frozen WorkflowExecutionReport JSON payload.
|
|
218
|
+
*/
|
|
219
|
+
function buildExecutionReport(job, status, startTime, inputs, environment, stepReports, payload) {
|
|
220
|
+
return {
|
|
221
|
+
jobId: job.id.toString(),
|
|
222
|
+
workflowName: job.workflow_id,
|
|
223
|
+
status: status,
|
|
224
|
+
durationMs: Date.now() - startTime,
|
|
225
|
+
startedAt: new Date(startTime).toISOString(),
|
|
226
|
+
finishedAt: new Date().toISOString(),
|
|
227
|
+
inputs,
|
|
228
|
+
environment,
|
|
229
|
+
steps: stepReports,
|
|
230
|
+
artifacts: [],
|
|
231
|
+
rerunToken: JSON.stringify({ jobId: job.id, payload }),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Dispatches the execution report concurrently to all registered reporters.
|
|
236
|
+
*/
|
|
237
|
+
async function dispatchReporters(workerId, reporters = [], report) {
|
|
238
|
+
if (!Array.isArray(reporters) || reporters.length === 0)
|
|
239
|
+
return;
|
|
240
|
+
console.log(`[${workerId}] 📢 Dispatching execution report to ${reporters.length} reporter(s)...`);
|
|
241
|
+
await Promise.allSettled(reporters.map(async (reporter) => {
|
|
242
|
+
try {
|
|
243
|
+
await reporter.report(report);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error(`[${workerId}] ⚠️ Reporter '${reporter.name || 'unknown'}' failed:`, err.message);
|
|
247
|
+
}
|
|
248
|
+
}));
|
|
249
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-cli/on",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "CLI entry point for the on plugin ecosystem.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
|
|
6
|
+
"packageManager": "pnpm@10.30.3",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./*": "./dist/*"
|
|
8
10
|
},
|
|
9
|
-
"main": "./dist/on.js",
|
|
10
11
|
"files": [
|
|
11
12
|
"dist"
|
|
12
13
|
],
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"publishConfig": {
|
|
17
|
-
"access": "public",
|
|
18
|
-
"provenance": true
|
|
19
|
-
},
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"repository": {
|
|
22
|
-
"url": "https://github.com/cloud-cli/on"
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"on": "dist/index.js"
|
|
23
17
|
},
|
|
24
18
|
"dependencies": {
|
|
25
|
-
"
|
|
19
|
+
"acorn": "^8.17.0",
|
|
20
|
+
"ansi_up": "^6.0.6",
|
|
21
|
+
"dotenv": "^17.4.2",
|
|
26
22
|
"yaml": "^2.8.2"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"nx": "nx",
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"test": "echo true || npm wf start",
|
|
29
|
+
"wf": "pnpm tsx src/index.ts -w tests -p 11235"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@changesets/cli": "2.30.0",
|
|
33
|
+
"@eslint/js": "10.0.1",
|
|
34
|
+
"@types/node": "^26.0.0",
|
|
35
|
+
"tsx": "^4.23.1",
|
|
36
|
+
"typescript": "^7.0.0",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "8.57.1",
|
|
38
|
+
"@typescript-eslint/parser": "8.57.1",
|
|
39
|
+
"typescript-eslint": "8.57.1",
|
|
40
|
+
"vite": "^7.3.5",
|
|
41
|
+
"vitest": "4.1.0"
|
|
27
42
|
}
|
|
28
|
-
}
|
|
43
|
+
}
|