@cloud-cli/on 1.2.2 → 1.2.3
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 +24 -20
- package/dist/db-client.d.ts +14 -0
- package/dist/drivers/index.d.ts +2 -0
- package/dist/drivers/standard-process.driver.d.ts +10 -0
- package/dist/drivers/systemd.driver.d.ts +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/log-redactor.d.ts +6 -0
- package/dist/on.js +9240 -0
- package/dist/parser/include-resolver.d.ts +9 -0
- package/dist/parser/matrix-expander.d.ts +5 -0
- package/dist/parser/yaml-loader.d.ts +6 -0
- package/dist/plugins/github-status.plugin.d.ts +7 -0
- package/dist/plugins/manager.d.ts +7 -0
- package/dist/queue.d.ts +41 -0
- package/dist/reporters/html.reporter.d.ts +14 -0
- package/dist/reporters/json-file.reporter.d.ts +9 -0
- package/dist/reporters/slack.reporter.d.ts +15 -0
- package/dist/safe-eval.d.ts +28 -0
- package/dist/secrets.d.ts +15 -0
- package/dist/server/preprocessors/github.d.ts +5 -0
- package/dist/server/server.d.ts +32 -0
- package/dist/types.d.ts +181 -0
- package/dist/worker.d.ts +8 -0
- package/package.json +24 -20
- package/dist/config.js +0 -15
- package/dist/db-client.js +0 -38
- package/dist/drivers/index.js +0 -11
- package/dist/drivers/standard-process.driver.js +0 -156
- package/dist/drivers/systemd.driver.js +0 -151
- package/dist/evaluator/safe-eval.js +0 -213
- package/dist/index.js +0 -145
- package/dist/ingress/preprocessors/github.js +0 -30
- package/dist/ingress/server.js +0 -241
- package/dist/logging/redactor.js +0 -20
- package/dist/parser/include-resolver.js +0 -47
- package/dist/parser/matrix-expander.js +0 -43
- package/dist/parser/yaml-loader.js +0 -45
- package/dist/plugins/github-status.plugin.js +0 -19
- package/dist/plugins/manager.js +0 -21
- package/dist/plugins/types.js +0 -1
- package/dist/queue/dispatcher.js +0 -112
- package/dist/reporters/html.reporter.js +0 -108
- package/dist/reporters/json-file.reporter.js +0 -16
- package/dist/reporters/slack.reporter.js +0 -25
- package/dist/reporters/types.js +0 -1
- package/dist/runner/step-runner.js +0 -42
- package/dist/secrets/store.js +0 -40
- package/dist/types.js +0 -1
- package/dist/worker.js +0 -249
- /package/dist/{ingress/types.js → runner/step-runner.d.ts} +0 -0
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
export class StandardProcessDriver {
|
|
5
|
-
name = 'standard-process';
|
|
6
|
-
async isSupported() {
|
|
7
|
-
return true; // Supported on all OS platforms
|
|
8
|
-
}
|
|
9
|
-
async execute(ctx) {
|
|
10
|
-
const startTime = Date.now();
|
|
11
|
-
let logFd = null;
|
|
12
|
-
let logFilePath = '';
|
|
13
|
-
// 1. Guard Log Directory & File Handle Creation
|
|
14
|
-
try {
|
|
15
|
-
const logDir = path.join(ctx.workspacePath, '.logs');
|
|
16
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
17
|
-
logFilePath = path.join(logDir, `step-${ctx.stepId}.log`);
|
|
18
|
-
logFd = fs.openSync(logFilePath, 'a');
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
return {
|
|
22
|
-
done: Promise.resolve({
|
|
23
|
-
exitCode: 1,
|
|
24
|
-
durationMs: 0,
|
|
25
|
-
error: new Error(`Failed to initialize step log file: ${err.message}`),
|
|
26
|
-
}),
|
|
27
|
-
cancel: async () => { },
|
|
28
|
-
logFilePath: '',
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
// 2. Format Execution Command
|
|
32
|
-
let cmd;
|
|
33
|
-
let args;
|
|
34
|
-
if (ctx.image) {
|
|
35
|
-
cmd = 'docker';
|
|
36
|
-
args = [
|
|
37
|
-
'run',
|
|
38
|
-
'--rm',
|
|
39
|
-
'--init',
|
|
40
|
-
'-v',
|
|
41
|
-
`${ctx.workspacePath}:/workspace`,
|
|
42
|
-
'-w',
|
|
43
|
-
'/workspace',
|
|
44
|
-
'--entrypoint',
|
|
45
|
-
'sh',
|
|
46
|
-
ctx.image,
|
|
47
|
-
'-c',
|
|
48
|
-
ctx.command,
|
|
49
|
-
];
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
cmd = 'sh';
|
|
53
|
-
args = ['-c', ctx.command];
|
|
54
|
-
}
|
|
55
|
-
// 3. Spawn Detached Child Process
|
|
56
|
-
let child;
|
|
57
|
-
try {
|
|
58
|
-
child = spawn(cmd, args, {
|
|
59
|
-
cwd: ctx.workspacePath,
|
|
60
|
-
env: { ...process.env, ...ctx.env },
|
|
61
|
-
detached: true, // Creates separate Process Group ID
|
|
62
|
-
stdio: ['ignore', logFd, logFd],
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
catch (spawnErr) {
|
|
66
|
-
try {
|
|
67
|
-
if (logFd !== null)
|
|
68
|
-
fs.closeSync(logFd);
|
|
69
|
-
}
|
|
70
|
-
catch { }
|
|
71
|
-
return {
|
|
72
|
-
done: Promise.resolve({
|
|
73
|
-
exitCode: 1,
|
|
74
|
-
durationMs: Date.now() - startTime,
|
|
75
|
-
error: new Error(`Failed to spawn process: ${spawnErr.message}`),
|
|
76
|
-
}),
|
|
77
|
-
cancel: async () => { },
|
|
78
|
-
logFilePath,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
let isCancelled = false;
|
|
82
|
-
let timeoutTimer = null;
|
|
83
|
-
// 4. Safe Promise Resolution & Lifecycle Tracking
|
|
84
|
-
const done = new Promise((resolve) => {
|
|
85
|
-
let isResolved = false;
|
|
86
|
-
const safeResolve = (result) => {
|
|
87
|
-
if (isResolved)
|
|
88
|
-
return; // Prevent double-resolution
|
|
89
|
-
isResolved = true;
|
|
90
|
-
if (timeoutTimer)
|
|
91
|
-
clearTimeout(timeoutTimer);
|
|
92
|
-
// Always close log File Descriptor safely
|
|
93
|
-
try {
|
|
94
|
-
if (logFd !== null)
|
|
95
|
-
fs.closeSync(logFd);
|
|
96
|
-
}
|
|
97
|
-
catch { }
|
|
98
|
-
resolve(result);
|
|
99
|
-
};
|
|
100
|
-
// Optional step timeout
|
|
101
|
-
if (ctx.timeoutMs) {
|
|
102
|
-
timeoutTimer = setTimeout(() => {
|
|
103
|
-
isCancelled = true;
|
|
104
|
-
this.killProcessGroup(child);
|
|
105
|
-
}, ctx.timeoutMs);
|
|
106
|
-
// CRUCIAL: Unref timer so it doesn't hold event loop open
|
|
107
|
-
timeoutTimer.unref();
|
|
108
|
-
}
|
|
109
|
-
child.on('close', (code) => {
|
|
110
|
-
safeResolve({
|
|
111
|
-
exitCode: code ?? (isCancelled ? 130 : 1),
|
|
112
|
-
durationMs: Date.now() - startTime,
|
|
113
|
-
error: isCancelled ? new Error('Step timed out or was cancelled by user') : undefined,
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
child.on('error', (err) => {
|
|
117
|
-
safeResolve({
|
|
118
|
-
exitCode: 1,
|
|
119
|
-
durationMs: Date.now() - startTime,
|
|
120
|
-
error: err,
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
// 5. Cancellation Hook
|
|
125
|
-
const cancel = async () => {
|
|
126
|
-
isCancelled = true;
|
|
127
|
-
this.killProcessGroup(child);
|
|
128
|
-
};
|
|
129
|
-
return { done, cancel, logFilePath };
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Kills the entire process group tree (-PID) with unref escalation
|
|
133
|
-
*/
|
|
134
|
-
killProcessGroup(child) {
|
|
135
|
-
if (child.pid && !child.killed) {
|
|
136
|
-
try {
|
|
137
|
-
// Send SIGTERM to entire process group (-PID)
|
|
138
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
139
|
-
// Escalate to SIGKILL after 5 seconds if process tree is still alive
|
|
140
|
-
const killTimer = setTimeout(() => {
|
|
141
|
-
try {
|
|
142
|
-
if (child.pid && !child.killed) {
|
|
143
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch { }
|
|
147
|
-
}, 5000);
|
|
148
|
-
// CRUCIAL: Unref escalation timer so Node process can exit cleanly
|
|
149
|
-
killTimer.unref();
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// Process group may already be dead
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { spawn, exec } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
const execAsync = promisify(exec);
|
|
6
|
-
export class SystemdDriver {
|
|
7
|
-
name = 'systemd';
|
|
8
|
-
/**
|
|
9
|
-
* Check if systemd bus is available on Linux host
|
|
10
|
-
*/
|
|
11
|
-
async isSupported() {
|
|
12
|
-
try {
|
|
13
|
-
return fs.existsSync('/run/systemd/system');
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
async execute(ctx) {
|
|
20
|
-
const startTime = Date.now();
|
|
21
|
-
let logFd = null;
|
|
22
|
-
let logFilePath = '';
|
|
23
|
-
// 1. Guard Log Directory & File Handle Creation
|
|
24
|
-
try {
|
|
25
|
-
const logDir = path.join(ctx.workspacePath, '.logs');
|
|
26
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
27
|
-
logFilePath = path.join(logDir, `step-${ctx.stepId}.log`);
|
|
28
|
-
logFd = fs.openSync(logFilePath, 'a');
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
return {
|
|
32
|
-
done: Promise.resolve({
|
|
33
|
-
exitCode: 1,
|
|
34
|
-
durationMs: 0,
|
|
35
|
-
error: new Error(`Failed to initialize step log file: ${err.message}`),
|
|
36
|
-
}),
|
|
37
|
-
cancel: async () => { },
|
|
38
|
-
logFilePath: '',
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
// 2. Format Sanitized Systemd Unit Name
|
|
42
|
-
const unitName = `workflow-${ctx.jobId}-${ctx.stepId}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
43
|
-
// 3. Build systemd-run Flags
|
|
44
|
-
const systemdFlags = [
|
|
45
|
-
`--unit=${unitName}`,
|
|
46
|
-
'--wait', // Block until unit completes
|
|
47
|
-
'--pipe', // Stream stdio directly to file handle
|
|
48
|
-
`--working-directory=${ctx.workspacePath}`,
|
|
49
|
-
];
|
|
50
|
-
if (ctx.env) {
|
|
51
|
-
for (const [key, val] of Object.entries(ctx.env)) {
|
|
52
|
-
systemdFlags.push(`--setenv=${key}=${val}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (ctx.timeoutMs) {
|
|
56
|
-
const timeoutSec = Math.ceil(ctx.timeoutMs / 1000);
|
|
57
|
-
systemdFlags.push(`--property=RuntimeMaxSec=${timeoutSec}`);
|
|
58
|
-
}
|
|
59
|
-
// 4. Construct Command
|
|
60
|
-
let commandArgs;
|
|
61
|
-
if (ctx.image) {
|
|
62
|
-
commandArgs = [
|
|
63
|
-
'docker',
|
|
64
|
-
'run',
|
|
65
|
-
'--rm',
|
|
66
|
-
'--init',
|
|
67
|
-
`--name=${unitName}`, // Predictable container name for stopping
|
|
68
|
-
'-v',
|
|
69
|
-
`${ctx.workspacePath}:/workspace`,
|
|
70
|
-
'-w',
|
|
71
|
-
'/workspace',
|
|
72
|
-
ctx.image,
|
|
73
|
-
'sh',
|
|
74
|
-
'-c',
|
|
75
|
-
ctx.command,
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
commandArgs = ['sh', '-c', ctx.command];
|
|
80
|
-
}
|
|
81
|
-
// 5. Spawn systemd-run
|
|
82
|
-
let child;
|
|
83
|
-
try {
|
|
84
|
-
child = spawn('systemd-run', [...systemdFlags, '--', ...commandArgs], {
|
|
85
|
-
stdio: ['ignore', logFd, logFd],
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
catch (spawnErr) {
|
|
89
|
-
try {
|
|
90
|
-
if (logFd !== null)
|
|
91
|
-
fs.closeSync(logFd);
|
|
92
|
-
}
|
|
93
|
-
catch { }
|
|
94
|
-
return {
|
|
95
|
-
done: Promise.resolve({
|
|
96
|
-
exitCode: 1,
|
|
97
|
-
durationMs: Date.now() - startTime,
|
|
98
|
-
error: new Error(`Failed to spawn systemd-run: ${spawnErr.message}`),
|
|
99
|
-
}),
|
|
100
|
-
cancel: async () => { },
|
|
101
|
-
logFilePath,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
let isCancelled = false;
|
|
105
|
-
// 6. Safe Promise Resolution & File Handle Cleanup
|
|
106
|
-
const done = new Promise((resolve) => {
|
|
107
|
-
let isResolved = false;
|
|
108
|
-
const safeResolve = (result) => {
|
|
109
|
-
if (isResolved)
|
|
110
|
-
return; // Prevent double-resolution
|
|
111
|
-
isResolved = true;
|
|
112
|
-
try {
|
|
113
|
-
if (logFd !== null)
|
|
114
|
-
fs.closeSync(logFd);
|
|
115
|
-
}
|
|
116
|
-
catch { }
|
|
117
|
-
resolve(result);
|
|
118
|
-
};
|
|
119
|
-
child.on('close', (code) => {
|
|
120
|
-
safeResolve({
|
|
121
|
-
exitCode: code ?? (isCancelled ? 130 : 1),
|
|
122
|
-
durationMs: Date.now() - startTime,
|
|
123
|
-
error: isCancelled ? new Error('Step cancelled by user or systemd timeout') : undefined,
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
child.on('error', (err) => {
|
|
127
|
-
safeResolve({
|
|
128
|
-
exitCode: 1,
|
|
129
|
-
durationMs: Date.now() - startTime,
|
|
130
|
-
error: err,
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
// 7. Systemd / Docker Graceful Cancellation
|
|
135
|
-
const cancel = async () => {
|
|
136
|
-
isCancelled = true;
|
|
137
|
-
try {
|
|
138
|
-
if (ctx.image) {
|
|
139
|
-
// Stop docker container gracefully if running
|
|
140
|
-
await execAsync(`docker stop -t 2 ${unitName}`).catch(() => { });
|
|
141
|
-
}
|
|
142
|
-
// Stop systemd transient unit (sends SIGTERM -> SIGKILL to Cgroup tree)
|
|
143
|
-
await execAsync(`systemctl stop ${unitName}.service`).catch(() => { });
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Unit or container may already be stopped
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
return { done, cancel, logFilePath };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import * as acorn from 'acorn';
|
|
2
|
-
export const BUILTIN_HELPERS = {
|
|
3
|
-
String: (val) => String(val ?? ''),
|
|
4
|
-
Number: (val) => Number(val),
|
|
5
|
-
Boolean: (val) => Boolean(val),
|
|
6
|
-
JSON: {
|
|
7
|
-
parse: (str) => JSON.parse(str),
|
|
8
|
-
stringify: (obj) => JSON.stringify(obj, null, 2),
|
|
9
|
-
},
|
|
10
|
-
};
|
|
11
|
-
export class SafeExpressionEvaluator {
|
|
12
|
-
/**
|
|
13
|
-
* Deterministic Value Evaluator (Used for `env:`, `name:`, `image:`, `concurrency:`)
|
|
14
|
-
* - Native non-string types (booleans, numbers, objects) pass through untouched.
|
|
15
|
-
* - Strings WITHOUT `${` are returned as 100% literal strings (zero JS AST overhead).
|
|
16
|
-
* - Strings WITH `${` are evaluated strictly as ES Template Literals.
|
|
17
|
-
*/
|
|
18
|
-
static async evaluateValue(val, context = {}) {
|
|
19
|
-
if (typeof val !== 'string') {
|
|
20
|
-
return val;
|
|
21
|
-
}
|
|
22
|
-
// 1. Literal Passthrough: String does not contain `${`
|
|
23
|
-
if (!val.includes('${')) {
|
|
24
|
-
return val;
|
|
25
|
-
}
|
|
26
|
-
// 2. Dynamic Template Interpolation: Evaluates as ES Template Literal
|
|
27
|
-
const templateExpr = `\`${val}\``;
|
|
28
|
-
return this.evaluateExpression(templateExpr, context);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Deterministic Condition Evaluator (Used for `if:`)
|
|
32
|
-
* Strictly parses code as a JavaScript expression and coerces result to boolean.
|
|
33
|
-
* Throws an explicit AST Parse Error on invalid syntax (fails fast and loud).
|
|
34
|
-
*/
|
|
35
|
-
static async evaluateCondition(code, context = {}) {
|
|
36
|
-
const result = await this.evaluateExpression(code, context);
|
|
37
|
-
return Boolean(result);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Evaluates direct JS code (Used for `eval:` steps or internal expression resolution).
|
|
41
|
-
*/
|
|
42
|
-
static async evaluateExpression(code, context = {}) {
|
|
43
|
-
if (!code || typeof code !== 'string') {
|
|
44
|
-
return code;
|
|
45
|
-
}
|
|
46
|
-
const trimmed = code.trim();
|
|
47
|
-
let ast;
|
|
48
|
-
try {
|
|
49
|
-
ast = acorn.parseExpressionAt(trimmed, 0, {
|
|
50
|
-
ecmaVersion: 2020,
|
|
51
|
-
allowAwaitOutsideFunction: false,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch (parseErr) {
|
|
55
|
-
throw new Error(`Invalid expression syntax in '${trimmed}': ${parseErr.message}`);
|
|
56
|
-
}
|
|
57
|
-
return this.evalNodeAsync(ast, context);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Alias wrapper for backward compatibility with step runners
|
|
61
|
-
*/
|
|
62
|
-
static async evaluateAsync(code, context = {}) {
|
|
63
|
-
return this.evaluateExpression(code, context);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Asynchronous AST Node Walker with Security Guards
|
|
67
|
-
*/
|
|
68
|
-
static async evalNodeAsync(node, ctx) {
|
|
69
|
-
switch (node.type) {
|
|
70
|
-
// Primitive Literals: 123, "hello", true, null
|
|
71
|
-
case 'Literal':
|
|
72
|
-
return node.value;
|
|
73
|
-
// Variables / Identifiers: inputs, steps, String
|
|
74
|
-
case 'Identifier':
|
|
75
|
-
if (node.name in ctx)
|
|
76
|
-
return ctx[node.name];
|
|
77
|
-
if (node.name in BUILTIN_HELPERS)
|
|
78
|
-
return BUILTIN_HELPERS[node.name];
|
|
79
|
-
return undefined;
|
|
80
|
-
// Property Access: inputs.branch or secrets["TOKEN"]
|
|
81
|
-
case 'MemberExpression': {
|
|
82
|
-
const object = await this.evalNodeAsync(node.object, ctx);
|
|
83
|
-
if (object == null)
|
|
84
|
-
return undefined;
|
|
85
|
-
const property = node.computed ? await this.evalNodeAsync(node.property, ctx) : node.property.name;
|
|
86
|
-
// SECURITY GUARD: Block Prototype Pollution Escapes
|
|
87
|
-
if (['constructor', '__proto__', 'prototype'].includes(property)) {
|
|
88
|
-
throw new Error(`Security Guard Violation: Access to '${property}' is blocked.`);
|
|
89
|
-
}
|
|
90
|
-
return object[property];
|
|
91
|
-
}
|
|
92
|
-
// Function & Method Calls: slack_notify(...) or url.replace(...)
|
|
93
|
-
case 'CallExpression': {
|
|
94
|
-
let fn;
|
|
95
|
-
let targetObj = null;
|
|
96
|
-
if (node.callee.type === 'MemberExpression') {
|
|
97
|
-
targetObj = await this.evalNodeAsync(node.callee.object, ctx);
|
|
98
|
-
const prop = node.callee.computed
|
|
99
|
-
? await this.evalNodeAsync(node.callee.property, ctx)
|
|
100
|
-
: node.callee.property.name;
|
|
101
|
-
if (['constructor', '__proto__', 'prototype'].includes(prop)) {
|
|
102
|
-
throw new Error(`Security Guard Violation: Invoking method '${prop}' is blocked.`);
|
|
103
|
-
}
|
|
104
|
-
if (targetObj != null && typeof targetObj[prop] === 'function') {
|
|
105
|
-
fn = targetObj[prop];
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
throw new Error(`Property '${prop}' is not a callable function on target object.`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
else if (node.callee.type === 'Identifier') {
|
|
112
|
-
const fnName = node.callee.name;
|
|
113
|
-
fn = ctx[fnName] ?? BUILTIN_HELPERS[fnName];
|
|
114
|
-
if (!fn || typeof fn !== 'function') {
|
|
115
|
-
throw new Error(`Unknown function helper '${fnName}'.`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (!fn) {
|
|
119
|
-
throw new Error('Invalid function invocation target.');
|
|
120
|
-
}
|
|
121
|
-
const args = await Promise.all(node.arguments.map((arg) => this.evalNodeAsync(arg, ctx)));
|
|
122
|
-
return await fn.apply(targetObj, args);
|
|
123
|
-
}
|
|
124
|
-
// Binary Operators: a === b, x + y, p > q
|
|
125
|
-
case 'BinaryExpression': {
|
|
126
|
-
const left = await this.evalNodeAsync(node.left, ctx);
|
|
127
|
-
const right = await this.evalNodeAsync(node.right, ctx);
|
|
128
|
-
switch (node.operator) {
|
|
129
|
-
case '===':
|
|
130
|
-
case '==':
|
|
131
|
-
return left == right;
|
|
132
|
-
case '!==':
|
|
133
|
-
case '!=':
|
|
134
|
-
return left != right;
|
|
135
|
-
case '>':
|
|
136
|
-
return left > right;
|
|
137
|
-
case '<':
|
|
138
|
-
return left < right;
|
|
139
|
-
case '>=':
|
|
140
|
-
return left >= right;
|
|
141
|
-
case '<=':
|
|
142
|
-
return left <= right;
|
|
143
|
-
case '+':
|
|
144
|
-
return left + right;
|
|
145
|
-
case '-':
|
|
146
|
-
return left - right;
|
|
147
|
-
case '*':
|
|
148
|
-
return left * right;
|
|
149
|
-
case '/':
|
|
150
|
-
return left / right;
|
|
151
|
-
default:
|
|
152
|
-
throw new Error(`Unsupported binary operator: ${node.operator}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Logical Operators: a && b, x || y
|
|
156
|
-
case 'LogicalExpression': {
|
|
157
|
-
const left = await this.evalNodeAsync(node.left, ctx);
|
|
158
|
-
if (node.operator === '&&') {
|
|
159
|
-
return left ? await this.evalNodeAsync(node.right, ctx) : left;
|
|
160
|
-
}
|
|
161
|
-
if (node.operator === '||') {
|
|
162
|
-
return left ? left : await this.evalNodeAsync(node.right, ctx);
|
|
163
|
-
}
|
|
164
|
-
throw new Error(`Unsupported logical operator: ${node.operator}`);
|
|
165
|
-
}
|
|
166
|
-
// Unary Operators: !x, -y
|
|
167
|
-
case 'UnaryExpression': {
|
|
168
|
-
const argument = await this.evalNodeAsync(node.argument, ctx);
|
|
169
|
-
if (node.operator === '!')
|
|
170
|
-
return !argument;
|
|
171
|
-
if (node.operator === '-')
|
|
172
|
-
return -argument;
|
|
173
|
-
if (node.operator === '+')
|
|
174
|
-
return +argument;
|
|
175
|
-
throw new Error(`Unsupported unary operator: ${node.operator}`);
|
|
176
|
-
}
|
|
177
|
-
// Template Strings: `node:${matrix.version}-alpine`
|
|
178
|
-
case 'TemplateLiteral': {
|
|
179
|
-
const quasis = node.quasis.map((q) => q.value.cooked);
|
|
180
|
-
const expressions = await Promise.all(node.expressions.map((e) => this.evalNodeAsync(e, ctx)));
|
|
181
|
-
let result = '';
|
|
182
|
-
for (let i = 0; i < quasis.length; i++) {
|
|
183
|
-
result += quasis[i];
|
|
184
|
-
if (i < expressions.length) {
|
|
185
|
-
result += expressions[i] ?? '';
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return result;
|
|
189
|
-
}
|
|
190
|
-
// Array Literals: [1, 2, "three"]
|
|
191
|
-
case 'ArrayExpression': {
|
|
192
|
-
return Promise.all(node.elements.map((elem) => this.evalNodeAsync(elem, ctx)));
|
|
193
|
-
}
|
|
194
|
-
// Object Literals: { a: 1, b: "hello" }
|
|
195
|
-
case 'ObjectExpression': {
|
|
196
|
-
const obj = {};
|
|
197
|
-
for (const prop of node.properties) {
|
|
198
|
-
if (prop.type === 'Property') {
|
|
199
|
-
const key = prop.key.type === 'Identifier' ? prop.key.name : await this.evalNodeAsync(prop.key, ctx);
|
|
200
|
-
obj[key] = await this.evalNodeAsync(prop.value, ctx);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return obj;
|
|
204
|
-
}
|
|
205
|
-
// Optional Chaining: inputs?.repo
|
|
206
|
-
case 'ChainExpression': {
|
|
207
|
-
return this.evalNodeAsync(node.expression, ctx);
|
|
208
|
-
}
|
|
209
|
-
default:
|
|
210
|
-
throw new Error(`Security Guard Violation: AST Node type '${node.type}' is disallowed.`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
package/dist/index.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from 'node:util';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import fs, { statSync } from 'node:fs';
|
|
5
|
-
import { QueueManager } from './queue/dispatcher.js';
|
|
6
|
-
import { SecretStore } from './secrets/store.js';
|
|
7
|
-
import { WebhookServer } from './ingress/server.js';
|
|
8
|
-
import { WorkflowIncludeResolver } from './parser/include-resolver.js';
|
|
9
|
-
import { expandMatrix } from './parser/matrix-expander.js';
|
|
10
|
-
import { startWorkers } from './worker.js';
|
|
11
|
-
import { YamlLoader } from './parser/yaml-loader.js';
|
|
12
|
-
import { resolveConfig } from './config.js';
|
|
13
|
-
const { values, positionals } = parseArgs({
|
|
14
|
-
allowPositionals: true,
|
|
15
|
-
options: {
|
|
16
|
-
config: { type: 'string', short: 'c', default: process.env.RUNNER_CONFIG_PATH || './runner.config.mjs' },
|
|
17
|
-
database: { type: 'string', short: 'd', default: process.env.RUNNER_DATABASE_URL },
|
|
18
|
-
workflows: { type: 'string', short: 'w', default: process.env.RUNNER_WORKFLOWS_PATH || '.on/' },
|
|
19
|
-
port: { type: 'string', short: 'p', default: String(process.env.PORT || '11235') },
|
|
20
|
-
workers: { type: 'string', short: 'k', default: process.env.RUNNER_WORKERS || '5' },
|
|
21
|
-
help: { type: 'boolean', short: 'h' },
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
function printHelp() {
|
|
25
|
-
console.log(`
|
|
26
|
-
🏃 Runner CLI 🏃
|
|
27
|
-
|
|
28
|
-
Usage:
|
|
29
|
-
npx -y @cloud-cli/on <command> [options]
|
|
30
|
-
pnpm dlx -y @cloud-cli/on <command> [options]
|
|
31
|
-
|
|
32
|
-
Commands:
|
|
33
|
-
start Runs both Webhook Ingress Server and Workers (Default)
|
|
34
|
-
start-server Runs Webhook Ingress Server only (API Gateway mode)
|
|
35
|
-
start-workers Runs Worker Polling loops only (Scalable Worker mode)
|
|
36
|
-
validate Parses and validates workflow YAML files without running
|
|
37
|
-
|
|
38
|
-
Options:
|
|
39
|
-
-c, --config Path to runner.config.mjs (default: ./runner.config.mjs, env: RUNNER_CONFIG_PATH)
|
|
40
|
-
-d, --database SQLite Database URL (env: RUNNER_DATABASE_URL)
|
|
41
|
-
-w, --workflows Path to where your workflows are defined (default: .on/, env: RUNNER_WORKFLOWS_PATH)
|
|
42
|
-
-p, --port Port for Webhook Ingress Server (default: 11235, env: PORT)
|
|
43
|
-
-k, --workers Number of worker thread loops to spawn (default: 5, env: RUNNER_WORKERS)
|
|
44
|
-
-h, --help Show this help message
|
|
45
|
-
`);
|
|
46
|
-
}
|
|
47
|
-
if (values.help) {
|
|
48
|
-
printHelp();
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
async function loadConfig() {
|
|
52
|
-
const cliOverrides = {
|
|
53
|
-
port: Number(values.port),
|
|
54
|
-
sqliteUrl: values.database,
|
|
55
|
-
workflowsDir: values.workflows,
|
|
56
|
-
workersCount: values.workers ? Number(values.workers) : undefined,
|
|
57
|
-
};
|
|
58
|
-
let userFileConfig = {};
|
|
59
|
-
const configPath = path.resolve(values.config);
|
|
60
|
-
if (fs.existsSync(configPath) && statSync(configPath).isFile()) {
|
|
61
|
-
userFileConfig = (await import(configPath)).default || {};
|
|
62
|
-
}
|
|
63
|
-
// Merge CLI flags over file config over defaults
|
|
64
|
-
const finalConfig = resolveConfig({
|
|
65
|
-
...userFileConfig,
|
|
66
|
-
...cliOverrides,
|
|
67
|
-
});
|
|
68
|
-
if (!fs.existsSync(finalConfig.workflowsDir)) {
|
|
69
|
-
console.warn(`⚠️ Warning: Workflows directory '${finalConfig.workflowsDir}' not found.`);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
return finalConfig;
|
|
73
|
-
}
|
|
74
|
-
function onValidate(config) {
|
|
75
|
-
console.log('🔍 Validating Workflows in:', config.workflowsDir);
|
|
76
|
-
const resolver = new WorkflowIncludeResolver(config.workflowsDir);
|
|
77
|
-
const files = fs.readdirSync(config.workflowsDir).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
78
|
-
for (const file of files) {
|
|
79
|
-
const resolved = resolver.resolve(file);
|
|
80
|
-
const expanded = expandMatrix(resolved);
|
|
81
|
-
console.log(` ✅ ${file} -> Valid! (${expanded.length} job matrix variant(s) generated)`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async function onServe(config, workflows) {
|
|
85
|
-
const { queue, secrets } = await init();
|
|
86
|
-
WebhookServer.withPort({
|
|
87
|
-
queue,
|
|
88
|
-
secrets,
|
|
89
|
-
adminToken: config.adminToken,
|
|
90
|
-
workflows,
|
|
91
|
-
port: config.port,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
async function onWorkers(config) {
|
|
95
|
-
const { queue, secrets } = await init();
|
|
96
|
-
startWorkers(config.workersCount, queue, secrets, config);
|
|
97
|
-
}
|
|
98
|
-
async function init() {
|
|
99
|
-
const secrets = new SecretStore('./.env');
|
|
100
|
-
const queue = new QueueManager('cli-node');
|
|
101
|
-
await queue.init();
|
|
102
|
-
return { secrets, queue };
|
|
103
|
-
}
|
|
104
|
-
async function main() {
|
|
105
|
-
const command = positionals[0] || 'start';
|
|
106
|
-
const config = await loadConfig();
|
|
107
|
-
if (!config) {
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
switch (command) {
|
|
111
|
-
case 'validate': {
|
|
112
|
-
onValidate(config);
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case 'start-server': {
|
|
116
|
-
console.log('🌐 Starting Ingress Gateway...');
|
|
117
|
-
onServe(config, []);
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
case 'start-workers': {
|
|
121
|
-
console.log(`⚙️ Starting ${config.workersCount} Worker Loop(s)...`);
|
|
122
|
-
onWorkers(config);
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case 'start': {
|
|
126
|
-
console.log('🚀 Starting Full Runner Engine (Ingress + Workers)...');
|
|
127
|
-
const workflows = YamlLoader.from(config.workflowsDir);
|
|
128
|
-
onServe(config, workflows);
|
|
129
|
-
onWorkers(config);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
default:
|
|
133
|
-
console.error(`❌ Unknown command: '${command}'`);
|
|
134
|
-
printHelp();
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const cleanupAndExit = async (signal) => {
|
|
139
|
-
console.log(`\n🛑 Received ${signal}. Gracefully shutting down workers...`);
|
|
140
|
-
// Cancel active jobs / close DB connections here
|
|
141
|
-
process.exit(0);
|
|
142
|
-
};
|
|
143
|
-
process.on('SIGINT', () => cleanupAndExit('SIGINT'));
|
|
144
|
-
process.on('SIGTERM', () => cleanupAndExit('SIGTERM'));
|
|
145
|
-
main().catch(console.error);
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
export class GitHubPreprocessor {
|
|
3
|
-
name = 'github';
|
|
4
|
-
parse(headers, body, rawBodyBuffer, secret) {
|
|
5
|
-
// 1. HMAC Signature Verification
|
|
6
|
-
let isValid = true;
|
|
7
|
-
const signature = headers['x-hub-signature-256'];
|
|
8
|
-
if (secret && signature) {
|
|
9
|
-
const hmac = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBodyBuffer).digest('hex');
|
|
10
|
-
isValid = crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hmac));
|
|
11
|
-
}
|
|
12
|
-
// 2. Normalize GitHub Event & Headers
|
|
13
|
-
const event = headers['x-github-event'] || 'unknown';
|
|
14
|
-
const ref = body.ref || '';
|
|
15
|
-
const branch = ref.replace('refs/heads/', '').replace('refs/tags/', '');
|
|
16
|
-
return {
|
|
17
|
-
isValid,
|
|
18
|
-
event,
|
|
19
|
-
inputs: {
|
|
20
|
-
event,
|
|
21
|
-
branch,
|
|
22
|
-
clone_url: body.repository?.clone_url,
|
|
23
|
-
commit_sha: body.after || body.head_commit?.id,
|
|
24
|
-
author: body.pusher?.name || body.sender?.login,
|
|
25
|
-
action: body.action, // e.g., 'opened', 'synchronize' for PRs
|
|
26
|
-
},
|
|
27
|
-
rawBody: body,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
}
|