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.
Files changed (171) hide show
  1. package/coverage/base.css +224 -0
  2. package/coverage/block-navigation.js +87 -0
  3. package/coverage/clover.xml +3477 -0
  4. package/coverage/coverage-final.json +20 -0
  5. package/coverage/favicon.png +0 -0
  6. package/coverage/index.html +176 -0
  7. package/coverage/prettify.css +1 -0
  8. package/coverage/prettify.js +2 -0
  9. package/coverage/sort-arrow-sprite.png +0 -0
  10. package/coverage/sorter.js +210 -0
  11. package/coverage/src/context/base.ts.html +1792 -0
  12. package/coverage/src/context/env.ts.html +1243 -0
  13. package/coverage/src/context/index.html +161 -0
  14. package/coverage/src/context/index.ts.html +229 -0
  15. package/coverage/src/context/secrets.ts.html +1276 -0
  16. package/coverage/src/index.html +131 -0
  17. package/coverage/src/index.ts.html +502 -0
  18. package/coverage/src/parser/expression.ts.html +2854 -0
  19. package/coverage/src/parser/index.html +161 -0
  20. package/coverage/src/parser/index.ts.html +163 -0
  21. package/coverage/src/parser/validator.ts.html +1588 -0
  22. package/coverage/src/parser/workflow.ts.html +616 -0
  23. package/coverage/src/scheduler/dependency.ts.html +1138 -0
  24. package/coverage/src/scheduler/index.html +221 -0
  25. package/coverage/src/scheduler/index.ts.html +214 -0
  26. package/coverage/src/scheduler/job-context.ts.html +265 -0
  27. package/coverage/src/scheduler/job-policy.ts.html +559 -0
  28. package/coverage/src/scheduler/job.ts.html +1816 -0
  29. package/coverage/src/scheduler/listener-registry.ts.html +199 -0
  30. package/coverage/src/scheduler/step.ts.html +2206 -0
  31. package/coverage/src/scheduler/steps-context.ts.html +217 -0
  32. package/coverage/src/types.ts.html +1897 -0
  33. package/coverage/src/utils/index.html +116 -0
  34. package/coverage/src/utils/needs.ts.html +127 -0
  35. package/dist/__tests__/context/env.test.d.ts +2 -0
  36. package/dist/__tests__/context/env.test.d.ts.map +1 -0
  37. package/dist/__tests__/context/env.test.js +28 -0
  38. package/dist/__tests__/context/env.test.js.map +1 -0
  39. package/dist/__tests__/index.test.d.ts +2 -0
  40. package/dist/__tests__/index.test.d.ts.map +1 -0
  41. package/dist/__tests__/index.test.js +50 -0
  42. package/dist/__tests__/index.test.js.map +1 -0
  43. package/dist/__tests__/parser/expression.test.d.ts +2 -0
  44. package/dist/__tests__/parser/expression.test.d.ts.map +1 -0
  45. package/dist/__tests__/parser/expression.test.js +116 -0
  46. package/dist/__tests__/parser/expression.test.js.map +1 -0
  47. package/dist/__tests__/parser/workflow.test.d.ts +2 -0
  48. package/dist/__tests__/parser/workflow.test.d.ts.map +1 -0
  49. package/dist/__tests__/parser/workflow.test.js +134 -0
  50. package/dist/__tests__/parser/workflow.test.js.map +1 -0
  51. package/dist/__tests__/scheduler/dependency.test.d.ts +2 -0
  52. package/dist/__tests__/scheduler/dependency.test.d.ts.map +1 -0
  53. package/dist/__tests__/scheduler/dependency.test.js +41 -0
  54. package/dist/__tests__/scheduler/dependency.test.js.map +1 -0
  55. package/dist/__tests__/scheduler/job-context.test.d.ts +2 -0
  56. package/dist/__tests__/scheduler/job-context.test.d.ts.map +1 -0
  57. package/dist/__tests__/scheduler/job-context.test.js +108 -0
  58. package/dist/__tests__/scheduler/job-context.test.js.map +1 -0
  59. package/dist/__tests__/scheduler/job-policy.test.d.ts +2 -0
  60. package/dist/__tests__/scheduler/job-policy.test.d.ts.map +1 -0
  61. package/dist/__tests__/scheduler/job-policy.test.js +159 -0
  62. package/dist/__tests__/scheduler/job-policy.test.js.map +1 -0
  63. package/dist/__tests__/scheduler/job.test.d.ts +2 -0
  64. package/dist/__tests__/scheduler/job.test.d.ts.map +1 -0
  65. package/dist/__tests__/scheduler/job.test.js +826 -0
  66. package/dist/__tests__/scheduler/job.test.js.map +1 -0
  67. package/dist/__tests__/scheduler/listener-registry.test.d.ts +2 -0
  68. package/dist/__tests__/scheduler/listener-registry.test.d.ts.map +1 -0
  69. package/dist/__tests__/scheduler/listener-registry.test.js +79 -0
  70. package/dist/__tests__/scheduler/listener-registry.test.js.map +1 -0
  71. package/dist/__tests__/scheduler/step.test.d.ts +2 -0
  72. package/dist/__tests__/scheduler/step.test.d.ts.map +1 -0
  73. package/dist/__tests__/scheduler/step.test.js +209 -0
  74. package/dist/__tests__/scheduler/step.test.js.map +1 -0
  75. package/dist/__tests__/scheduler/steps-context.test.d.ts +2 -0
  76. package/dist/__tests__/scheduler/steps-context.test.d.ts.map +1 -0
  77. package/dist/__tests__/scheduler/steps-context.test.js +43 -0
  78. package/dist/__tests__/scheduler/steps-context.test.js.map +1 -0
  79. package/dist/constants.d.ts +47 -0
  80. package/dist/constants.d.ts.map +1 -0
  81. package/dist/constants.js +53 -0
  82. package/dist/constants.js.map +1 -0
  83. package/dist/context.d.ts +37 -0
  84. package/dist/context.d.ts.map +1 -0
  85. package/dist/context.js +105 -0
  86. package/dist/context.js.map +1 -0
  87. package/dist/index.d.ts +9 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +10 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/parser/evaluator-builtins.d.ts +14 -0
  92. package/dist/parser/evaluator-builtins.d.ts.map +1 -0
  93. package/dist/parser/evaluator-builtins.js +258 -0
  94. package/dist/parser/evaluator-builtins.js.map +1 -0
  95. package/dist/parser/evaluator.d.ts +38 -0
  96. package/dist/parser/evaluator.d.ts.map +1 -0
  97. package/dist/parser/evaluator.js +257 -0
  98. package/dist/parser/evaluator.js.map +1 -0
  99. package/dist/parser/expression.d.ts +20 -0
  100. package/dist/parser/expression.d.ts.map +1 -0
  101. package/dist/parser/expression.js +128 -0
  102. package/dist/parser/expression.js.map +1 -0
  103. package/dist/parser/tokenizer.d.ts +26 -0
  104. package/dist/parser/tokenizer.d.ts.map +1 -0
  105. package/dist/parser/tokenizer.js +162 -0
  106. package/dist/parser/tokenizer.js.map +1 -0
  107. package/dist/parser/validator.d.ts +13 -0
  108. package/dist/parser/validator.d.ts.map +1 -0
  109. package/dist/parser/validator.js +383 -0
  110. package/dist/parser/validator.js.map +1 -0
  111. package/dist/parser/workflow.d.ts +30 -0
  112. package/dist/parser/workflow.d.ts.map +1 -0
  113. package/dist/parser/workflow.js +152 -0
  114. package/dist/parser/workflow.js.map +1 -0
  115. package/dist/scheduler/dependency.d.ts +37 -0
  116. package/dist/scheduler/dependency.d.ts.map +1 -0
  117. package/dist/scheduler/dependency.js +133 -0
  118. package/dist/scheduler/dependency.js.map +1 -0
  119. package/dist/scheduler/job-policy.d.ts +23 -0
  120. package/dist/scheduler/job-policy.d.ts.map +1 -0
  121. package/dist/scheduler/job-policy.js +117 -0
  122. package/dist/scheduler/job-policy.js.map +1 -0
  123. package/dist/scheduler/job.d.ts +151 -0
  124. package/dist/scheduler/job.d.ts.map +1 -0
  125. package/dist/scheduler/job.js +348 -0
  126. package/dist/scheduler/job.js.map +1 -0
  127. package/dist/scheduler/step-output-parser.d.ts +14 -0
  128. package/dist/scheduler/step-output-parser.d.ts.map +1 -0
  129. package/dist/scheduler/step-output-parser.js +70 -0
  130. package/dist/scheduler/step-output-parser.js.map +1 -0
  131. package/dist/scheduler/step.d.ts +74 -0
  132. package/dist/scheduler/step.d.ts.map +1 -0
  133. package/dist/scheduler/step.js +387 -0
  134. package/dist/scheduler/step.js.map +1 -0
  135. package/dist/types.d.ts +499 -0
  136. package/dist/types.d.ts.map +1 -0
  137. package/dist/types.js +5 -0
  138. package/dist/types.js.map +1 -0
  139. package/dist/workflow-models.d.ts +504 -0
  140. package/dist/workflow-models.d.ts.map +1 -0
  141. package/dist/workflow-models.js +5 -0
  142. package/dist/workflow-models.js.map +1 -0
  143. package/package.json +29 -0
  144. package/src/__tests__/context/env.test.ts +38 -0
  145. package/src/__tests__/index.test.ts +55 -0
  146. package/src/__tests__/parser/expression.test.ts +151 -0
  147. package/src/__tests__/parser/workflow.test.ts +151 -0
  148. package/src/__tests__/scheduler/dependency.test.ts +51 -0
  149. package/src/__tests__/scheduler/job-context.test.ts +119 -0
  150. package/src/__tests__/scheduler/job-policy.test.ts +195 -0
  151. package/src/__tests__/scheduler/job.test.ts +1014 -0
  152. package/src/__tests__/scheduler/listener-registry.test.ts +95 -0
  153. package/src/__tests__/scheduler/step.test.ts +258 -0
  154. package/src/__tests__/scheduler/steps-context.test.ts +49 -0
  155. package/src/constants.ts +61 -0
  156. package/src/context.ts +153 -0
  157. package/src/index.ts +64 -0
  158. package/src/parser/evaluator-builtins.ts +315 -0
  159. package/src/parser/evaluator.ts +333 -0
  160. package/src/parser/expression.ts +154 -0
  161. package/src/parser/tokenizer.ts +191 -0
  162. package/src/parser/validator.ts +444 -0
  163. package/src/parser/workflow.ts +176 -0
  164. package/src/scheduler/dependency.ts +180 -0
  165. package/src/scheduler/job-policy.ts +198 -0
  166. package/src/scheduler/job.ts +523 -0
  167. package/src/scheduler/step-output-parser.ts +94 -0
  168. package/src/scheduler/step.ts +543 -0
  169. package/src/workflow-models.ts +593 -0
  170. package/tsconfig.json +14 -0
  171. 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
+ });
@@ -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
+