takos-actions-engine 1.0.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/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +3477 -0
- package/coverage/coverage-final.json +20 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +176 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/context/base.ts.html +1792 -0
- package/coverage/src/context/env.ts.html +1243 -0
- package/coverage/src/context/index.html +161 -0
- package/coverage/src/context/index.ts.html +229 -0
- package/coverage/src/context/secrets.ts.html +1276 -0
- package/coverage/src/index.html +131 -0
- package/coverage/src/index.ts.html +502 -0
- package/coverage/src/parser/expression.ts.html +2854 -0
- package/coverage/src/parser/index.html +161 -0
- package/coverage/src/parser/index.ts.html +163 -0
- package/coverage/src/parser/validator.ts.html +1588 -0
- package/coverage/src/parser/workflow.ts.html +616 -0
- package/coverage/src/scheduler/dependency.ts.html +1138 -0
- package/coverage/src/scheduler/index.html +221 -0
- package/coverage/src/scheduler/index.ts.html +214 -0
- package/coverage/src/scheduler/job-context.ts.html +265 -0
- package/coverage/src/scheduler/job-policy.ts.html +559 -0
- package/coverage/src/scheduler/job.ts.html +1816 -0
- package/coverage/src/scheduler/listener-registry.ts.html +199 -0
- package/coverage/src/scheduler/step.ts.html +2206 -0
- package/coverage/src/scheduler/steps-context.ts.html +217 -0
- package/coverage/src/types.ts.html +1897 -0
- package/coverage/src/utils/index.html +116 -0
- package/coverage/src/utils/needs.ts.html +127 -0
- package/dist/__tests__/context/env.test.d.ts +2 -0
- package/dist/__tests__/context/env.test.d.ts.map +1 -0
- package/dist/__tests__/context/env.test.js +28 -0
- package/dist/__tests__/context/env.test.js.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +50 -0
- package/dist/__tests__/index.test.js.map +1 -0
- package/dist/__tests__/parser/expression.test.d.ts +2 -0
- package/dist/__tests__/parser/expression.test.d.ts.map +1 -0
- package/dist/__tests__/parser/expression.test.js +116 -0
- package/dist/__tests__/parser/expression.test.js.map +1 -0
- package/dist/__tests__/parser/workflow.test.d.ts +2 -0
- package/dist/__tests__/parser/workflow.test.d.ts.map +1 -0
- package/dist/__tests__/parser/workflow.test.js +134 -0
- package/dist/__tests__/parser/workflow.test.js.map +1 -0
- package/dist/__tests__/scheduler/dependency.test.d.ts +2 -0
- package/dist/__tests__/scheduler/dependency.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/dependency.test.js +41 -0
- package/dist/__tests__/scheduler/dependency.test.js.map +1 -0
- package/dist/__tests__/scheduler/job-context.test.d.ts +2 -0
- package/dist/__tests__/scheduler/job-context.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/job-context.test.js +108 -0
- package/dist/__tests__/scheduler/job-context.test.js.map +1 -0
- package/dist/__tests__/scheduler/job-policy.test.d.ts +2 -0
- package/dist/__tests__/scheduler/job-policy.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/job-policy.test.js +159 -0
- package/dist/__tests__/scheduler/job-policy.test.js.map +1 -0
- package/dist/__tests__/scheduler/job.test.d.ts +2 -0
- package/dist/__tests__/scheduler/job.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/job.test.js +826 -0
- package/dist/__tests__/scheduler/job.test.js.map +1 -0
- package/dist/__tests__/scheduler/listener-registry.test.d.ts +2 -0
- package/dist/__tests__/scheduler/listener-registry.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/listener-registry.test.js +79 -0
- package/dist/__tests__/scheduler/listener-registry.test.js.map +1 -0
- package/dist/__tests__/scheduler/step.test.d.ts +2 -0
- package/dist/__tests__/scheduler/step.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/step.test.js +209 -0
- package/dist/__tests__/scheduler/step.test.js.map +1 -0
- package/dist/__tests__/scheduler/steps-context.test.d.ts +2 -0
- package/dist/__tests__/scheduler/steps-context.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler/steps-context.test.js +43 -0
- package/dist/__tests__/scheduler/steps-context.test.js.map +1 -0
- package/dist/constants.d.ts +47 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +53 -0
- package/dist/constants.js.map +1 -0
- package/dist/context.d.ts +37 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +105 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/evaluator-builtins.d.ts +14 -0
- package/dist/parser/evaluator-builtins.d.ts.map +1 -0
- package/dist/parser/evaluator-builtins.js +258 -0
- package/dist/parser/evaluator-builtins.js.map +1 -0
- package/dist/parser/evaluator.d.ts +38 -0
- package/dist/parser/evaluator.d.ts.map +1 -0
- package/dist/parser/evaluator.js +257 -0
- package/dist/parser/evaluator.js.map +1 -0
- package/dist/parser/expression.d.ts +20 -0
- package/dist/parser/expression.d.ts.map +1 -0
- package/dist/parser/expression.js +128 -0
- package/dist/parser/expression.js.map +1 -0
- package/dist/parser/tokenizer.d.ts +26 -0
- package/dist/parser/tokenizer.d.ts.map +1 -0
- package/dist/parser/tokenizer.js +162 -0
- package/dist/parser/tokenizer.js.map +1 -0
- package/dist/parser/validator.d.ts +13 -0
- package/dist/parser/validator.d.ts.map +1 -0
- package/dist/parser/validator.js +383 -0
- package/dist/parser/validator.js.map +1 -0
- package/dist/parser/workflow.d.ts +30 -0
- package/dist/parser/workflow.d.ts.map +1 -0
- package/dist/parser/workflow.js +152 -0
- package/dist/parser/workflow.js.map +1 -0
- package/dist/scheduler/dependency.d.ts +37 -0
- package/dist/scheduler/dependency.d.ts.map +1 -0
- package/dist/scheduler/dependency.js +133 -0
- package/dist/scheduler/dependency.js.map +1 -0
- package/dist/scheduler/job-policy.d.ts +23 -0
- package/dist/scheduler/job-policy.d.ts.map +1 -0
- package/dist/scheduler/job-policy.js +117 -0
- package/dist/scheduler/job-policy.js.map +1 -0
- package/dist/scheduler/job.d.ts +151 -0
- package/dist/scheduler/job.d.ts.map +1 -0
- package/dist/scheduler/job.js +348 -0
- package/dist/scheduler/job.js.map +1 -0
- package/dist/scheduler/step-output-parser.d.ts +14 -0
- package/dist/scheduler/step-output-parser.d.ts.map +1 -0
- package/dist/scheduler/step-output-parser.js +70 -0
- package/dist/scheduler/step-output-parser.js.map +1 -0
- package/dist/scheduler/step.d.ts +74 -0
- package/dist/scheduler/step.d.ts.map +1 -0
- package/dist/scheduler/step.js +387 -0
- package/dist/scheduler/step.js.map +1 -0
- package/dist/types.d.ts +499 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-models.d.ts +504 -0
- package/dist/workflow-models.d.ts.map +1 -0
- package/dist/workflow-models.js +5 -0
- package/dist/workflow-models.js.map +1 -0
- package/package.json +29 -0
- package/src/__tests__/context/env.test.ts +38 -0
- package/src/__tests__/index.test.ts +55 -0
- package/src/__tests__/parser/expression.test.ts +151 -0
- package/src/__tests__/parser/workflow.test.ts +151 -0
- package/src/__tests__/scheduler/dependency.test.ts +51 -0
- package/src/__tests__/scheduler/job-context.test.ts +119 -0
- package/src/__tests__/scheduler/job-policy.test.ts +195 -0
- package/src/__tests__/scheduler/job.test.ts +1014 -0
- package/src/__tests__/scheduler/listener-registry.test.ts +95 -0
- package/src/__tests__/scheduler/step.test.ts +258 -0
- package/src/__tests__/scheduler/steps-context.test.ts +49 -0
- package/src/constants.ts +61 -0
- package/src/context.ts +153 -0
- package/src/index.ts +64 -0
- package/src/parser/evaluator-builtins.ts +315 -0
- package/src/parser/evaluator.ts +333 -0
- package/src/parser/expression.ts +154 -0
- package/src/parser/tokenizer.ts +191 -0
- package/src/parser/validator.ts +444 -0
- package/src/parser/workflow.ts +176 -0
- package/src/scheduler/dependency.ts +180 -0
- package/src/scheduler/job-policy.ts +198 -0
- package/src/scheduler/job.ts +523 -0
- package/src/scheduler/step-output-parser.ts +94 -0
- package/src/scheduler/step.ts +543 -0
- package/src/workflow-models.ts +593 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { JobScheduler, type JobSchedulerEvent } from '../../scheduler/job.js';
|
|
4
|
+
import { createBaseContext } from '../../context.js';
|
|
5
|
+
import type { Workflow } from '../../workflow-models.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal workflow used purely to construct a JobScheduler instance.
|
|
9
|
+
*/
|
|
10
|
+
function createMinimalWorkflow(): Workflow {
|
|
11
|
+
return {
|
|
12
|
+
name: 'listener-test',
|
|
13
|
+
on: 'push',
|
|
14
|
+
jobs: {
|
|
15
|
+
a: { 'runs-on': 'ubuntu-latest', steps: [{ run: 'echo ok' }] },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('JobScheduler listener management', () => {
|
|
21
|
+
it('keeps current emit stable when a listener is removed during emit', async () => {
|
|
22
|
+
const scheduler = new JobScheduler(createMinimalWorkflow());
|
|
23
|
+
const callOrder: string[] = [];
|
|
24
|
+
|
|
25
|
+
let unsubscribeSecond = () => {};
|
|
26
|
+
|
|
27
|
+
scheduler.on(() => {
|
|
28
|
+
callOrder.push('first');
|
|
29
|
+
unsubscribeSecond();
|
|
30
|
+
});
|
|
31
|
+
unsubscribeSecond = scheduler.on(() => {
|
|
32
|
+
callOrder.push('second');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Run triggers multiple emit calls; capture events from the first emit
|
|
36
|
+
await scheduler.run(createBaseContext());
|
|
37
|
+
|
|
38
|
+
// The first event emitted is 'workflow:start'. Both listeners should have
|
|
39
|
+
// been called for that first emit because removal happens on a snapshot.
|
|
40
|
+
expect(callOrder[0]).toBe('first');
|
|
41
|
+
expect(callOrder[1]).toBe('second');
|
|
42
|
+
// After the first emit, the second listener should be gone.
|
|
43
|
+
// Subsequent emits should only include 'first'.
|
|
44
|
+
const afterFirstEmit = callOrder.slice(2);
|
|
45
|
+
expect(afterFirstEmit.every((v) => v === 'first')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('defers listeners added during emit until the next emit cycle', async () => {
|
|
49
|
+
const scheduler = new JobScheduler(createMinimalWorkflow());
|
|
50
|
+
const callOrder: string[] = [];
|
|
51
|
+
|
|
52
|
+
const lateListener = () => {
|
|
53
|
+
callOrder.push('late');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let addedLate = false;
|
|
57
|
+
scheduler.on(() => {
|
|
58
|
+
callOrder.push('first');
|
|
59
|
+
if (!addedLate) {
|
|
60
|
+
scheduler.on(lateListener);
|
|
61
|
+
addedLate = true;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await scheduler.run(createBaseContext());
|
|
66
|
+
|
|
67
|
+
// 'late' should NOT appear for the first emit, only for subsequent emits.
|
|
68
|
+
expect(callOrder[0]).toBe('first');
|
|
69
|
+
expect(callOrder[1]).not.toBe('late');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('continues calling remaining listeners even if an earlier listener throws', async () => {
|
|
73
|
+
const scheduler = new JobScheduler(createMinimalWorkflow());
|
|
74
|
+
const callOrder: string[] = [];
|
|
75
|
+
|
|
76
|
+
scheduler.on(() => {
|
|
77
|
+
callOrder.push('first');
|
|
78
|
+
throw new Error('listener failed');
|
|
79
|
+
});
|
|
80
|
+
scheduler.on(() => {
|
|
81
|
+
callOrder.push('second');
|
|
82
|
+
});
|
|
83
|
+
scheduler.on(() => {
|
|
84
|
+
callOrder.push('third');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Should not throw despite a listener throwing
|
|
88
|
+
await expect(scheduler.run(createBaseContext())).resolves.toBeDefined();
|
|
89
|
+
|
|
90
|
+
// All three listeners should have been called for at least the first emit
|
|
91
|
+
expect(callOrder.includes('first')).toBe(true);
|
|
92
|
+
expect(callOrder.includes('second')).toBe(true);
|
|
93
|
+
expect(callOrder.includes('third')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { appendFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createBaseContext } from '../../context.js';
|
|
8
|
+
import type { Step } from '../../workflow-models.js';
|
|
9
|
+
import { StepRunner } from '../../scheduler/step.js';
|
|
10
|
+
|
|
11
|
+
async function withProcessPlatform<T>(
|
|
12
|
+
platform: NodeJS.Platform,
|
|
13
|
+
run: () => Promise<T>
|
|
14
|
+
): Promise<T> {
|
|
15
|
+
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
16
|
+
if (!platformDescriptor) {
|
|
17
|
+
throw new Error('Unable to read process.platform descriptor');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Object.defineProperty(process, 'platform', { value: platform });
|
|
21
|
+
try {
|
|
22
|
+
return await run();
|
|
23
|
+
} finally {
|
|
24
|
+
Object.defineProperty(process, 'platform', platformDescriptor);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('step output parsing', () => {
|
|
29
|
+
it('parses legacy and simple outputs while ignoring malformed lines', async () => {
|
|
30
|
+
const stdout = [
|
|
31
|
+
'::set-output name=legacy::from-legacy',
|
|
32
|
+
'::set-output name=legacy_empty::',
|
|
33
|
+
'simple_output=from-simple',
|
|
34
|
+
'legacy=from-simple-duplicate',
|
|
35
|
+
'not-valid=value',
|
|
36
|
+
'empty=',
|
|
37
|
+
].join('\n');
|
|
38
|
+
|
|
39
|
+
const runner = new StepRunner({
|
|
40
|
+
shellExecutor: async () => ({
|
|
41
|
+
exitCode: 0,
|
|
42
|
+
stdout,
|
|
43
|
+
stderr: '',
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const step: Step = { id: 'parse-outputs', run: 'echo output' };
|
|
48
|
+
const result = await runner.runStep(step, createBaseContext());
|
|
49
|
+
|
|
50
|
+
expect(result.outputs).toEqual({
|
|
51
|
+
legacy: 'from-legacy',
|
|
52
|
+
legacy_empty: '',
|
|
53
|
+
simple_output: 'from-simple',
|
|
54
|
+
empty: '',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles long legacy output lines', async () => {
|
|
59
|
+
const longName = 'A'.repeat(20_000);
|
|
60
|
+
const stdout = `::set-output name=${longName}::value`;
|
|
61
|
+
|
|
62
|
+
const runner = new StepRunner({
|
|
63
|
+
shellExecutor: async () => ({
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
stdout,
|
|
66
|
+
stderr: '',
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const step: Step = { id: 'long-outputs', run: 'echo output' };
|
|
71
|
+
const result = await runner.runStep(step, createBaseContext());
|
|
72
|
+
|
|
73
|
+
expect(result.outputs[longName]).toBe('value');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('reads command-file outputs and supports empty initial GitHub vars', async () => {
|
|
77
|
+
const capturedEnv: Array<Record<string, string> | undefined> = [];
|
|
78
|
+
const runner = new StepRunner({
|
|
79
|
+
shellExecutor: async (_command, options) => {
|
|
80
|
+
capturedEnv.push(options.env);
|
|
81
|
+
const outputFile = options.env?.GITHUB_OUTPUT;
|
|
82
|
+
expect(outputFile).toBeTruthy();
|
|
83
|
+
appendFileSync(outputFile!, 'from_file=hello\n');
|
|
84
|
+
appendFileSync(outputFile!, 'multi<<EOF\nline1\nline2\nEOF\n');
|
|
85
|
+
return {
|
|
86
|
+
exitCode: 0,
|
|
87
|
+
stdout: 'from_stdout=ok',
|
|
88
|
+
stderr: '',
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const context = createBaseContext({ env: {} });
|
|
94
|
+
const step: Step = { id: 'command-file-outputs', run: 'echo output' };
|
|
95
|
+
const result = await runner.runStep(step, context);
|
|
96
|
+
|
|
97
|
+
expect(result.outputs).toEqual({
|
|
98
|
+
from_stdout: 'ok',
|
|
99
|
+
from_file: 'hello',
|
|
100
|
+
multi: 'line1\nline2',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const firstEnv = capturedEnv[0];
|
|
104
|
+
expect(firstEnv?.GITHUB_ENV).toBeTruthy();
|
|
105
|
+
expect(firstEnv?.GITHUB_OUTPUT).toBeTruthy();
|
|
106
|
+
expect(firstEnv?.GITHUB_PATH).toBeTruthy();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('parses command-file heredoc outputs written with CRLF line endings', async () => {
|
|
110
|
+
const runner = new StepRunner({
|
|
111
|
+
shellExecutor: async (_command, options) => {
|
|
112
|
+
const outputFile = options.env?.GITHUB_OUTPUT;
|
|
113
|
+
expect(outputFile).toBeTruthy();
|
|
114
|
+
appendFileSync(outputFile!, 'multi<<EOF\r\nline1\r\nline2\r\nEOF\r\n');
|
|
115
|
+
return {
|
|
116
|
+
exitCode: 0,
|
|
117
|
+
stdout: '',
|
|
118
|
+
stderr: '',
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const context = createBaseContext({ env: {} });
|
|
124
|
+
const step: Step = { id: 'command-file-outputs-crlf', run: 'echo output' };
|
|
125
|
+
const result = await runner.runStep(step, context);
|
|
126
|
+
|
|
127
|
+
expect(result.outputs).toEqual({
|
|
128
|
+
multi: 'line1\nline2',
|
|
129
|
+
});
|
|
130
|
+
expect(result.outputs.multi.includes('\r')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('step default executors', () => {
|
|
135
|
+
it('uses pwsh by default on win32', async () => {
|
|
136
|
+
let observedShell: Step['shell'] | undefined;
|
|
137
|
+
|
|
138
|
+
await withProcessPlatform('win32', async () => {
|
|
139
|
+
const runner = new StepRunner({
|
|
140
|
+
shellExecutor: async (_command, options) => {
|
|
141
|
+
observedShell = options.shell;
|
|
142
|
+
return { exitCode: 0, stdout: '', stderr: '' };
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await runner.runStep({ id: 'win32-default-shell', run: 'echo ok' }, createBaseContext());
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(observedShell).toBe('pwsh');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('uses bash by default on non-win32 platforms', async () => {
|
|
153
|
+
let observedShell: Step['shell'] | undefined;
|
|
154
|
+
|
|
155
|
+
await withProcessPlatform('linux', async () => {
|
|
156
|
+
const runner = new StepRunner({
|
|
157
|
+
shellExecutor: async (_command, options) => {
|
|
158
|
+
observedShell = options.shell;
|
|
159
|
+
return { exitCode: 0, stdout: '', stderr: '' };
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await runner.runStep({ id: 'non-win32-default-shell', run: 'echo ok' }, createBaseContext());
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(observedShell).toBe('bash');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('prioritizes explicit shell configuration over platform defaults', async () => {
|
|
170
|
+
const observedShells: Array<Step['shell'] | undefined> = [];
|
|
171
|
+
|
|
172
|
+
await withProcessPlatform('win32', async () => {
|
|
173
|
+
const runner = new StepRunner({
|
|
174
|
+
defaultShell: 'bash',
|
|
175
|
+
shellExecutor: async (_command, options) => {
|
|
176
|
+
observedShells.push(options.shell);
|
|
177
|
+
return { exitCode: 0, stdout: '', stderr: '' };
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await runner.runStep({ id: 'configured-default-shell', run: 'echo ok' }, createBaseContext());
|
|
182
|
+
await runner.runStep(
|
|
183
|
+
{ id: 'step-explicit-shell', run: 'echo ok', shell: 'cmd' },
|
|
184
|
+
createBaseContext()
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(observedShells).toEqual(['bash', 'cmd']);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('respects working directory and env for default shell executor', async () => {
|
|
192
|
+
const workingDirectory = mkdtempSync(join(tmpdir(), 'actions-engine-step-'));
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const runner = new StepRunner({
|
|
196
|
+
workingDirectory,
|
|
197
|
+
defaultShell: 'bash',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const step: Step = {
|
|
201
|
+
id: 'default-shell',
|
|
202
|
+
run: 'echo "cwd=$PWD"; echo "from_env=$TAKOS_TEST_ENV"',
|
|
203
|
+
env: {
|
|
204
|
+
TAKOS_TEST_ENV: 'from-step',
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = await runner.runStep(step, createBaseContext());
|
|
209
|
+
|
|
210
|
+
expect(result.conclusion).toBe('success');
|
|
211
|
+
expect(result.outputs.cwd).toBe(workingDirectory);
|
|
212
|
+
expect(result.outputs.from_env).toBe('from-step');
|
|
213
|
+
} finally {
|
|
214
|
+
rmSync(workingDirectory, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns failure when default shell executor times out', async () => {
|
|
219
|
+
const runner = new StepRunner();
|
|
220
|
+
const step: Step = {
|
|
221
|
+
id: 'timeout-shell',
|
|
222
|
+
run: 'node -e "setTimeout(() => {}, 5000)"',
|
|
223
|
+
'timeout-minutes': 0.001,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = await runner.runStep(step, createBaseContext());
|
|
227
|
+
|
|
228
|
+
expect(result.conclusion).toBe('failure');
|
|
229
|
+
expect(result.error).toContain('Exit code: 124');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('supports builtin checkout action without a custom resolver', async () => {
|
|
233
|
+
const runner = new StepRunner();
|
|
234
|
+
const context = createBaseContext();
|
|
235
|
+
const step: Step = {
|
|
236
|
+
id: 'builtin-checkout',
|
|
237
|
+
uses: 'actions/checkout@v4',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const result = await runner.runStep(step, context);
|
|
241
|
+
|
|
242
|
+
expect(result.conclusion).toBe('success');
|
|
243
|
+
expect(result.outputs.path).toBe(context.github.workspace);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('fails explicitly for unsupported default actions', async () => {
|
|
247
|
+
const runner = new StepRunner();
|
|
248
|
+
const step: Step = {
|
|
249
|
+
id: 'unsupported-action',
|
|
250
|
+
uses: 'actions/cache@v4',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const result = await runner.runStep(step, createBaseContext());
|
|
254
|
+
|
|
255
|
+
expect(result.conclusion).toBe('failure');
|
|
256
|
+
expect(result.error).toContain('Unsupported action: actions/cache@v4');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { StepResult } from '../../workflow-models.js';
|
|
4
|
+
import {
|
|
5
|
+
buildStepsContext,
|
|
6
|
+
} from '../../scheduler/job-policy.js';
|
|
7
|
+
|
|
8
|
+
function createStepResult(overrides: Partial<StepResult> = {}): StepResult {
|
|
9
|
+
return {
|
|
10
|
+
id: 'step',
|
|
11
|
+
status: 'completed',
|
|
12
|
+
outputs: {},
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('steps-context helpers', () => {
|
|
18
|
+
it('builds context entries from step results and ignores anonymous steps', () => {
|
|
19
|
+
const firstOutputs = { first: '1' };
|
|
20
|
+
const secondOutputs = { second: '2' };
|
|
21
|
+
const stepsContext = buildStepsContext([
|
|
22
|
+
createStepResult({
|
|
23
|
+
id: 'build',
|
|
24
|
+
outputs: firstOutputs,
|
|
25
|
+
conclusion: 'success',
|
|
26
|
+
}),
|
|
27
|
+
createStepResult({
|
|
28
|
+
id: undefined,
|
|
29
|
+
outputs: { ignored: 'true' },
|
|
30
|
+
conclusion: 'failure',
|
|
31
|
+
}),
|
|
32
|
+
createStepResult({
|
|
33
|
+
id: 'build',
|
|
34
|
+
outputs: secondOutputs,
|
|
35
|
+
conclusion: 'failure',
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
expect(stepsContext).toEqual({
|
|
40
|
+
build: {
|
|
41
|
+
outputs: { second: '2' },
|
|
42
|
+
outcome: 'failure',
|
|
43
|
+
conclusion: 'failure',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
expect(stepsContext.build.outputs).not.toBe(secondOutputs);
|
|
47
|
+
expect(stepsContext.build.outputs).not.toBe(firstOutputs);
|
|
48
|
+
});
|
|
49
|
+
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized constants for the actions-engine package.
|
|
3
|
+
*
|
|
4
|
+
* All magic numbers and hard-coded limits are defined here so that they are
|
|
5
|
+
* easy to find, document, and tune without touching implementation files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Parser limits
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of `evaluate()` calls allowed per expression evaluation.
|
|
14
|
+
* Guards against runaway recursion or adversarial expressions that attempt to
|
|
15
|
+
* consume unbounded CPU time (e.g. deeply nested bracket accesses).
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_EVALUATE_CALLS = 10_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maximum depth of chained property / bracket accesses (e.g. `a.b.c[0].d`).
|
|
21
|
+
* Prevents stack overflow and excessive object traversal from malicious or
|
|
22
|
+
* accidentally deep expressions.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_PARSE_ACCESS_DEPTH = 128;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maximum byte length of a JSON string accepted by the `fromJSON()` expression
|
|
28
|
+
* function. Capped at 1 MB to prevent out-of-memory conditions when an
|
|
29
|
+
* attacker-controlled value is passed to `JSON.parse`.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_FROM_JSON_SIZE = 1_048_576; // 1 MB
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maximum byte length of an expression (the content inside `${{ }}`).
|
|
35
|
+
* Set to 64 KB, which is well above any reasonable expression while still
|
|
36
|
+
* preventing denial-of-service via extremely long input strings.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_EXPRESSION_SIZE = 64 * 1024; // 64 KB
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Scheduler / step-runner limits
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default step timeout in minutes, used when a step does not specify
|
|
46
|
+
* `timeout-minutes`. Matches the GitHub Actions default of 360 minutes
|
|
47
|
+
* (6 hours).
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_TIMEOUT_MINUTES = 360;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Multiplier to convert minutes to milliseconds for `setTimeout` calls.
|
|
53
|
+
*/
|
|
54
|
+
export const MINUTES_TO_MS = 60_000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maximum size in bytes for GITHUB_ENV / GITHUB_OUTPUT / GITHUB_PATH command
|
|
58
|
+
* files written by step scripts. Capped at 10 MB to prevent a single step
|
|
59
|
+
* from exhausting memory when the runner reads these files back.
|
|
60
|
+
*/
|
|
61
|
+
export const MAX_COMMAND_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context management for workflow execution
|
|
3
|
+
*/
|
|
4
|
+
import type {
|
|
5
|
+
ExecutionContext,
|
|
6
|
+
GitHubContext,
|
|
7
|
+
RunnerContext,
|
|
8
|
+
InputsContext,
|
|
9
|
+
} from './workflow-models.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Base context
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Context builder options
|
|
17
|
+
*/
|
|
18
|
+
export interface ContextBuilderOptions {
|
|
19
|
+
/** GitHub context overrides */
|
|
20
|
+
github?: Partial<GitHubContext>;
|
|
21
|
+
/** Runner context overrides */
|
|
22
|
+
runner?: Partial<RunnerContext>;
|
|
23
|
+
/** Environment variables */
|
|
24
|
+
env?: Record<string, string>;
|
|
25
|
+
/** Repository variables */
|
|
26
|
+
vars?: Record<string, string>;
|
|
27
|
+
/** Secrets */
|
|
28
|
+
secrets?: Record<string, string>;
|
|
29
|
+
/** Workflow dispatch inputs */
|
|
30
|
+
inputs?: InputsContext;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a base execution context
|
|
35
|
+
*/
|
|
36
|
+
export function createBaseContext(
|
|
37
|
+
options: ContextBuilderOptions = {}
|
|
38
|
+
): ExecutionContext {
|
|
39
|
+
const os = process.platform;
|
|
40
|
+
const arch = process.arch;
|
|
41
|
+
|
|
42
|
+
const github: GitHubContext = {
|
|
43
|
+
event_name: 'push',
|
|
44
|
+
event: {},
|
|
45
|
+
ref: 'refs/heads/main',
|
|
46
|
+
ref_name: 'main',
|
|
47
|
+
sha: '0000000000000000000000000000000000000000',
|
|
48
|
+
repository: 'owner/repo',
|
|
49
|
+
repository_owner: 'owner',
|
|
50
|
+
actor: 'actor',
|
|
51
|
+
workflow: 'workflow',
|
|
52
|
+
job: 'job',
|
|
53
|
+
run_id: '1',
|
|
54
|
+
run_number: 1,
|
|
55
|
+
run_attempt: 1,
|
|
56
|
+
server_url: 'https://github.com',
|
|
57
|
+
api_url: 'https://api.github.com',
|
|
58
|
+
graphql_url: 'https://api.github.com/graphql',
|
|
59
|
+
workspace: '/home/runner/work/repo/repo',
|
|
60
|
+
action: '',
|
|
61
|
+
action_path: '',
|
|
62
|
+
token: '',
|
|
63
|
+
...options.github,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const osName = os === 'win32' ? 'Windows' as const : os === 'darwin' ? 'macOS' as const : 'Linux' as const;
|
|
67
|
+
const archMap: Record<string, 'X64' | 'ARM64' | 'ARM' | 'X86'> = { x64: 'X64', arm64: 'ARM64', arm: 'ARM' };
|
|
68
|
+
const archName = archMap[arch] ?? 'X86';
|
|
69
|
+
|
|
70
|
+
const runner: RunnerContext = {
|
|
71
|
+
name: 'local-runner',
|
|
72
|
+
os: osName,
|
|
73
|
+
arch: archName,
|
|
74
|
+
temp: process.env.RUNNER_TEMP || '/tmp',
|
|
75
|
+
tool_cache: process.env.RUNNER_TOOL_CACHE || '/opt/hostedtoolcache',
|
|
76
|
+
debug: process.env.RUNNER_DEBUG || '',
|
|
77
|
+
...options.runner,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
github,
|
|
82
|
+
env: options.env || {},
|
|
83
|
+
vars: options.vars || {},
|
|
84
|
+
secrets: options.secrets || {},
|
|
85
|
+
runner,
|
|
86
|
+
job: { status: 'success' },
|
|
87
|
+
steps: {},
|
|
88
|
+
needs: {},
|
|
89
|
+
inputs: options.inputs,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Environment variable management
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
const GITHUB_ENV_HEREDOC_PATTERN = /^([a-zA-Z_][a-zA-Z0-9_]*)<<(.+)$/;
|
|
98
|
+
const GITHUB_ENV_SIMPLE_PATTERN = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse GITHUB_ENV file format
|
|
102
|
+
* Format:
|
|
103
|
+
* NAME=value
|
|
104
|
+
* or
|
|
105
|
+
* NAME<<EOF
|
|
106
|
+
* multiline
|
|
107
|
+
* value
|
|
108
|
+
* EOF
|
|
109
|
+
*/
|
|
110
|
+
export function parseGitHubEnvFile(content: string): Record<string, string> {
|
|
111
|
+
const env: Record<string, string> = {};
|
|
112
|
+
const lines = content
|
|
113
|
+
.split('\n')
|
|
114
|
+
.map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line));
|
|
115
|
+
let i = 0;
|
|
116
|
+
|
|
117
|
+
while (i < lines.length) {
|
|
118
|
+
const line = lines[i].trim();
|
|
119
|
+
|
|
120
|
+
if (!line || line.startsWith('#')) {
|
|
121
|
+
i++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check for heredoc format: NAME<<DELIMITER
|
|
126
|
+
const heredocMatch = line.match(GITHUB_ENV_HEREDOC_PATTERN);
|
|
127
|
+
if (heredocMatch) {
|
|
128
|
+
const [, name, delimiter] = heredocMatch;
|
|
129
|
+
const valueLines: string[] = [];
|
|
130
|
+
i++;
|
|
131
|
+
|
|
132
|
+
while (i < lines.length && lines[i] !== delimiter) {
|
|
133
|
+
valueLines.push(lines[i]);
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
env[name] = valueLines.join('\n');
|
|
138
|
+
i++; // Skip delimiter line
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Simple format: NAME=value
|
|
143
|
+
const simpleMatch = line.match(GITHUB_ENV_SIMPLE_PATTERN);
|
|
144
|
+
if (simpleMatch) {
|
|
145
|
+
const [, name, value] = simpleMatch;
|
|
146
|
+
env[name] = value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return env;
|
|
153
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* takos-actions-engine
|
|
3
|
+
* GitHub Actions compatible CI engine
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Public types
|
|
7
|
+
export type {
|
|
8
|
+
// Trigger types
|
|
9
|
+
BranchFilter,
|
|
10
|
+
PullRequestTriggerConfig,
|
|
11
|
+
PullRequestEventType,
|
|
12
|
+
WorkflowDispatchInput,
|
|
13
|
+
WorkflowDispatchConfig,
|
|
14
|
+
ScheduleTriggerConfig,
|
|
15
|
+
RepositoryDispatchConfig,
|
|
16
|
+
WorkflowCallInput,
|
|
17
|
+
WorkflowCallOutput,
|
|
18
|
+
WorkflowCallSecret,
|
|
19
|
+
WorkflowCallConfig,
|
|
20
|
+
WorkflowTrigger,
|
|
21
|
+
// Step / Job / Workflow types
|
|
22
|
+
Step,
|
|
23
|
+
MatrixConfig,
|
|
24
|
+
JobStrategy,
|
|
25
|
+
ContainerConfig,
|
|
26
|
+
JobOutputs,
|
|
27
|
+
PermissionLevel,
|
|
28
|
+
Permissions,
|
|
29
|
+
ConcurrencyConfig,
|
|
30
|
+
JobDefaults,
|
|
31
|
+
Job,
|
|
32
|
+
Workflow,
|
|
33
|
+
// Execution state types
|
|
34
|
+
RunStatus,
|
|
35
|
+
Conclusion,
|
|
36
|
+
StepResult,
|
|
37
|
+
JobResult,
|
|
38
|
+
WorkflowResult,
|
|
39
|
+
// Context types
|
|
40
|
+
GitHubContext,
|
|
41
|
+
RunnerContext,
|
|
42
|
+
JobContext,
|
|
43
|
+
StepsContext,
|
|
44
|
+
NeedsContext,
|
|
45
|
+
StrategyContext,
|
|
46
|
+
MatrixContext,
|
|
47
|
+
InputsContext,
|
|
48
|
+
ExecutionContext,
|
|
49
|
+
// Parser / scheduler types
|
|
50
|
+
ParsedWorkflow,
|
|
51
|
+
DiagnosticSeverity,
|
|
52
|
+
WorkflowDiagnostic,
|
|
53
|
+
ExecutionPlan,
|
|
54
|
+
StepExecutor,
|
|
55
|
+
ActionResolver,
|
|
56
|
+
} from './workflow-models.js';
|
|
57
|
+
|
|
58
|
+
// Parser — public API
|
|
59
|
+
export { parseWorkflow } from './parser/workflow.js';
|
|
60
|
+
export { validateWorkflow, type ValidationResult } from './parser/validator.js';
|
|
61
|
+
|
|
62
|
+
// Scheduler — public API
|
|
63
|
+
export { createExecutionPlan } from './scheduler/job.js';
|
|
64
|
+
|