@bevel-software/ael 0.1.0 → 0.2.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/package.json +1 -1
- package/src/cli/main.ts +11 -3
- package/src/executor/main.ts +50 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/ael",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The agentic execution layer of the Bevel platform: the one parser for .agent and .workflow files, the run contract, the harnesses, the interpreter the platform runs on Temporal, the executor, and the bevel-agent command line.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
package/src/cli/main.ts
CHANGED
|
@@ -17,6 +17,12 @@ import { OUTCOME_MARKER, readAll, runStep } from './run-step.js';
|
|
|
17
17
|
* What an executor execs inside a run container.
|
|
18
18
|
* bevel-agent executor an executor on this host: polls the executor
|
|
19
19
|
* queues and runs container steps on its engine.
|
|
20
|
+
* bevel-agent ci-executor
|
|
21
|
+
* the executor job of a CI pipeline: runs no step,
|
|
22
|
+
* starts a worker pipeline per run with work.
|
|
23
|
+
* bevel-agent ci-worker <run id>
|
|
24
|
+
* a worker job of a CI pipeline: that run's steps,
|
|
25
|
+
* in the job itself, until it waits or is over.
|
|
20
26
|
*
|
|
21
27
|
* Two doors in: `bin/bevel-agent.mjs` (what npm installs; registers tsx and
|
|
22
28
|
* calls `main`), and this file as the script tsx runs directly (the executor
|
|
@@ -50,12 +56,14 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
50
56
|
process.stdout.write(`${OUTCOME_MARKER}${JSON.stringify(outcome)}\n`);
|
|
51
57
|
return 0;
|
|
52
58
|
}
|
|
53
|
-
case 'executor':
|
|
59
|
+
case 'executor':
|
|
60
|
+
case 'ci-executor':
|
|
61
|
+
case 'ci-worker': {
|
|
54
62
|
const { executorMain } = await import('../executor/main.js');
|
|
55
|
-
return executorMain();
|
|
63
|
+
return executorMain(command, argv[1]);
|
|
56
64
|
}
|
|
57
65
|
default:
|
|
58
|
-
process.stderr.write('usage: bevel-agent run <file> [input] | run-step | executor
|
|
66
|
+
process.stderr.write('usage: bevel-agent run <file> [input] | run-step | executor | ci-executor | ci-worker <run id>\n');
|
|
59
67
|
return 2;
|
|
60
68
|
}
|
|
61
69
|
}
|
package/src/executor/main.ts
CHANGED
|
@@ -35,23 +35,55 @@ import { PERSONAL_EXECUTOR_NAME } from '../workflow/definition.js';
|
|
|
35
35
|
* beyond the defaults, such as running a rootless Docker daemon inside a step.
|
|
36
36
|
* Split on spaces and nothing else: no quoting, so no value with a space in it
|
|
37
37
|
*
|
|
38
|
-
* An executor inside a CI job, which is the environment already and ends with the work:
|
|
39
|
-
*
|
|
40
38
|
* EXECUTOR_IN_PLACE `true`: each step runs as a process of this executor, in a directory per run,
|
|
41
39
|
* with no container engine; the engine settings above (bundle, limits, run
|
|
42
40
|
* options, deployment) do not apply. One step at once unless EXECUTOR_MAX_STEPS says more
|
|
43
|
-
* EXECUTOR_EXIT_WHEN_IDLE seconds without
|
|
44
|
-
* EXECUTOR_RUN a run id: take that run's steps alone, one after another — a worker started
|
|
45
|
-
* for one run, which leaves (EXECUTOR_EXIT_WHEN_IDLE) when the run waits or is over
|
|
46
|
-
* EXECUTOR_START_WORKER a shell command: this executor runs no step. It looks for runs with a step
|
|
47
|
-
* waiting and runs the command once per run, to start a worker elsewhere (another
|
|
48
|
-
* pipeline); the command gets BEVEL_RUN_ID, BEVEL_WORKFLOW, BEVEL_STEP and
|
|
49
|
-
* BEVEL_ENVIRONMENT (the workflow's `environment:` as written: the worker job to start)
|
|
41
|
+
* EXECUTOR_EXIT_WHEN_IDLE seconds without work after which it leaves (default: never)
|
|
50
42
|
* EXECUTOR_TOKEN_FILE a file holding the key, in place of EXECUTOR_TOKEN: read once and removed, so
|
|
51
43
|
* a step running beside the executor finds it neither in a variable nor on disk
|
|
44
|
+
*
|
|
45
|
+
* In a CI pipeline the executor is two jobs under one name, each its own command, so neither can become
|
|
46
|
+
* the other through a variable set in the wrong place:
|
|
47
|
+
*
|
|
48
|
+
* `bevel-agent ci-executor` runs no step. It looks for runs with a step waiting and starts a worker for each:
|
|
49
|
+
*
|
|
50
|
+
* EXECUTOR_START_WORKER a shell command (required), run once per run, to start a worker elsewhere (another
|
|
51
|
+
* pipeline); it gets BEVEL_RUN_ID, BEVEL_WORKFLOW, BEVEL_STEP and BEVEL_ENVIRONMENT
|
|
52
|
+
* (the workflow's `environment:` as written: the worker job to start)
|
|
53
|
+
* EXECUTOR_EXIT_WHEN_IDLE seconds without a waiting run after which it leaves (default 480)
|
|
54
|
+
*
|
|
55
|
+
* `bevel-agent ci-worker <run id>` takes that run's steps alone, one after another, each in place, and leaves
|
|
56
|
+
* when none comes — the run waits at a gate, or is over:
|
|
57
|
+
*
|
|
58
|
+
* EXECUTOR_EXIT_WHEN_IDLE seconds without a step after which it leaves (default 90)
|
|
52
59
|
*/
|
|
53
|
-
export
|
|
60
|
+
export type ExecutorRole = 'executor' | 'ci-executor' | 'ci-worker';
|
|
61
|
+
|
|
62
|
+
/** A run id as an executor may name one. */
|
|
63
|
+
const RUN_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
64
|
+
|
|
65
|
+
export async function executorMain(role: ExecutorRole = 'executor', runArg?: string): Promise<number> {
|
|
54
66
|
const env = process.env;
|
|
67
|
+
// The role's own inputs first, before anything is read or removed: each command says what it lacks,
|
|
68
|
+
// and refuses the other one's settings rather than quietly becoming it.
|
|
69
|
+
const startWorker = env.EXECUTOR_START_WORKER?.trim();
|
|
70
|
+
const run = runArg?.trim();
|
|
71
|
+
if (role === 'executor' && (startWorker || env.EXECUTOR_RUN?.trim())) {
|
|
72
|
+
process.stderr.write('EXECUTOR_START_WORKER and EXECUTOR_RUN are not settings of `bevel-agent executor`: a CI pipeline runs `bevel-agent ci-executor` (starts workers) and `bevel-agent ci-worker <run id>` (works one run)\n');
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
if (role === 'ci-executor' && !startWorker) {
|
|
76
|
+
process.stderr.write('EXECUTOR_START_WORKER is required for `bevel-agent ci-executor`: the command that starts a worker pipeline for a run (it gets BEVEL_RUN_ID and BEVEL_ENVIRONMENT)\n');
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
if (role === 'ci-worker' && startWorker) {
|
|
80
|
+
process.stderr.write('EXECUTOR_START_WORKER is set on a worker job: a worker that started workers would start pipelines without end. Set it on the `bevel-agent ci-executor` job alone\n');
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
if (role === 'ci-worker' && (!run || !RUN_ID.test(run))) {
|
|
84
|
+
process.stderr.write('usage: bevel-agent ci-worker <run id> — the run this worker was started for (BEVEL_RUN_ID of the command that started it)\n');
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
55
87
|
const platformUrl = env.PLATFORM_URL?.trim().replace(/\/+$/, '');
|
|
56
88
|
if (!platformUrl) {
|
|
57
89
|
process.stderr.write("PLATFORM_URL is required: the platform's address, which every executor asks for its steps\n");
|
|
@@ -92,14 +124,14 @@ export async function executorMain(): Promise<number> {
|
|
|
92
124
|
const kind = rawKind;
|
|
93
125
|
const log = (line: string) => process.stderr.write(`[executor ${name}] ${line}\n`);
|
|
94
126
|
const idleSeconds = Number(env.EXECUTOR_EXIT_WHEN_IDLE);
|
|
95
|
-
|
|
127
|
+
// A CI job ends by itself, set or not: one that stayed would hold a runner until the job's own limit.
|
|
128
|
+
const idleDefaultMs = role === 'ci-executor' ? 480_000 : role === 'ci-worker' ? 90_000 : undefined;
|
|
129
|
+
const exitWhenIdleMs = Number.isFinite(idleSeconds) && idleSeconds > 0 ? idleSeconds * 1000 : idleDefaultMs;
|
|
96
130
|
const queueName = kind === 'deployment' ? `executor-${name}` : `personal-${name}`;
|
|
97
131
|
const stop = new AbortController();
|
|
98
132
|
process.once('SIGINT', () => stop.abort());
|
|
99
133
|
process.once('SIGTERM', () => stop.abort());
|
|
100
|
-
|
|
101
|
-
const startWorker = env.EXECUTOR_START_WORKER?.trim();
|
|
102
|
-
if (startWorker) {
|
|
134
|
+
if (role === 'ci-executor' && startWorker) {
|
|
103
135
|
await runWorkerStarter(
|
|
104
136
|
{
|
|
105
137
|
apiUrl: `${platformUrl}/api`,
|
|
@@ -116,7 +148,8 @@ export async function executorMain(): Promise<number> {
|
|
|
116
148
|
log('stopped');
|
|
117
149
|
return 0;
|
|
118
150
|
}
|
|
119
|
-
|
|
151
|
+
// A worker's job is the environment, always: there is no engine in it to ask for a container.
|
|
152
|
+
if (role === 'ci-worker' || /^(1|true|yes)$/i.test(env.EXECUTOR_IN_PLACE?.trim() ?? '')) {
|
|
120
153
|
const executor = createInPlaceExecutor({
|
|
121
154
|
queueName,
|
|
122
155
|
workspacesDir: path.join(tmpdir(), 'bevel-runs'),
|
|
@@ -126,7 +159,7 @@ export async function executorMain(): Promise<number> {
|
|
|
126
159
|
cancellation: personalExecutorHooks.cancellation,
|
|
127
160
|
});
|
|
128
161
|
log('steps run in place: as processes of this executor, with no container engine');
|
|
129
|
-
await runPersonalExecutor({ apiUrl: `${platformUrl}/api`, token, name, kind, slots: Number(env.EXECUTOR_MAX_STEPS) || 1, executor, log, exitWhenIdleMs, run }, stop.signal);
|
|
162
|
+
await runPersonalExecutor({ apiUrl: `${platformUrl}/api`, token, name, kind, slots: Number(env.EXECUTOR_MAX_STEPS) || 1, executor, log, exitWhenIdleMs, ...(role === 'ci-worker' ? { run } : {}) }, stop.signal);
|
|
130
163
|
log('stopped');
|
|
131
164
|
return 0;
|
|
132
165
|
}
|
|
@@ -150,7 +183,7 @@ export async function executorMain(): Promise<number> {
|
|
|
150
183
|
heartbeat: personalExecutorHooks.heartbeat,
|
|
151
184
|
cancellation: personalExecutorHooks.cancellation,
|
|
152
185
|
});
|
|
153
|
-
await runPersonalExecutor({ apiUrl: `${platformUrl}/api`, token, name, kind, slots: Number(env.EXECUTOR_MAX_STEPS) || 2, executor, log, exitWhenIdleMs
|
|
186
|
+
await runPersonalExecutor({ apiUrl: `${platformUrl}/api`, token, name, kind, slots: Number(env.EXECUTOR_MAX_STEPS) || 2, executor, log, exitWhenIdleMs }, stop.signal);
|
|
154
187
|
log('stopped');
|
|
155
188
|
return 0;
|
|
156
189
|
}
|