@olivervea/pipelines 2.0.0-beta.2 → 2.0.0-beta.20
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 +125 -62
- package/manifest.schema.json +208 -11
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +121 -51
- package/src/build/cli.ts +29 -22
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +82 -20
- package/src/build/project.ts +95 -28
- package/src/builder/pipeline.ts +344 -88
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +7 -5
- package/src/core/config-refs.ts +103 -0
- package/src/core/config.ts +224 -0
- package/src/core/external.ts +86 -0
- package/src/core/handlers.ts +61 -15
- package/src/core/models.ts +32 -15
- package/src/core/origin.ts +23 -0
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +89 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +84 -31
- package/src/ctx/sh.ts +105 -44
- package/src/ctx/types.ts +12 -4
- package/src/describe/describe.ts +701 -133
- package/src/describe/manifest-schema.ts +258 -61
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +72 -35
- package/src/harness/env.ts +45 -25
- package/src/harness/execute.ts +58 -29
- package/src/harness/index.ts +22 -9
- package/src/harness/main.ts +239 -78
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +77 -15
- package/src/index.ts +124 -11
- package/src/library/checks.ts +28 -0
- package/src/library/compose-deploy.ts +113 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +452 -108
- package/src/library/helm-deploy.ts +449 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +499 -105
- package/src/library/index.ts +48 -12
- package/src/library/models.ts +33 -9
- package/src/library/ssh.ts +67 -0
- package/src/testing/index.ts +74 -36
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +17 -7
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +86 -36
- package/src/library/script.ts +0 -85
package/src/harness/main.ts
CHANGED
|
@@ -2,29 +2,47 @@
|
|
|
2
2
|
// describe [--sentinels] → manifest (canonical JSON) on stdout
|
|
3
3
|
// run <step> → $OLVE_DIR/input.json → handler → $OLVE_DIR/work/output.json (+ result upload)
|
|
4
4
|
// map-trigger <trigger> → $OLVE_DIR/input.json (event) → pipeline input → $OLVE_DIR/work/output.json
|
|
5
|
-
import { mkdirSync, readFileSync } from
|
|
6
|
-
import { join } from
|
|
7
|
-
import { isImplementation } from
|
|
8
|
-
import
|
|
9
|
-
import type {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
5
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { isImplementation } from '../core/handlers.ts';
|
|
8
|
+
import { configRefPath, isConfigRef, unionAt } from '../core/config.ts';
|
|
9
|
+
import type { AnyStep } from '../core/steps.ts';
|
|
10
|
+
import type { WebhookEvent } from '../core/webhooks.ts';
|
|
11
|
+
import { pipelineData, type PipelineData, type StageNode } from '../builder/pipeline.ts';
|
|
12
|
+
import { createCtx, formatLog } from '../ctx/index.ts';
|
|
13
|
+
import { DescribeError, describePipeline, manifestText } from '../describe/describe.ts';
|
|
14
|
+
import type { HarnessInfo } from '../util/handler-hash.ts';
|
|
15
|
+
import { callbackUrlsFromEnv, ENV, paths, runInfoFromEnv } from './env.ts';
|
|
16
|
+
import { EXIT } from './exit.ts';
|
|
17
|
+
import {
|
|
18
|
+
executeStep,
|
|
19
|
+
type Envelope,
|
|
20
|
+
type Execution,
|
|
21
|
+
type StepFunction,
|
|
22
|
+
} from './execute.ts';
|
|
23
|
+
import {
|
|
24
|
+
MANIFEST_BEGIN,
|
|
25
|
+
MANIFEST_END,
|
|
26
|
+
packResult,
|
|
27
|
+
removeStale,
|
|
28
|
+
sentinelBlock,
|
|
29
|
+
uploadFile,
|
|
30
|
+
writeEnvelope,
|
|
31
|
+
} from './result.ts';
|
|
32
|
+
import { formatIssues, parseStrict } from './validate.ts';
|
|
19
33
|
|
|
20
34
|
/** step name → lazy import of `handlers/<name>.ts`. Literal import() paths so Bun bundles them. */
|
|
21
|
-
export type HandlerRegistry = Readonly<
|
|
35
|
+
export type HandlerRegistry = Readonly<
|
|
36
|
+
Record<string, () => Promise<{ default: unknown }>>
|
|
37
|
+
>;
|
|
22
38
|
|
|
23
39
|
export interface HarnessConfig {
|
|
24
40
|
/** Default export of pipeline.ts. */
|
|
25
41
|
pipeline: unknown;
|
|
26
42
|
handlers: HandlerRegistry;
|
|
27
43
|
handlerHashes: Readonly<Record<string, string>>;
|
|
44
|
+
/** Kind → hash of the code of the steps of that kind that carry their implementation. */
|
|
45
|
+
kindHashes?: Readonly<Record<string, string>>;
|
|
28
46
|
harness: HarnessInfo;
|
|
29
47
|
}
|
|
30
48
|
|
|
@@ -38,56 +56,99 @@ export const defaultIO: HarnessIO = {
|
|
|
38
56
|
await Bun.write(Bun.stdout, text);
|
|
39
57
|
},
|
|
40
58
|
stderr: (line) => {
|
|
41
|
-
process.stderr.write(line.endsWith(
|
|
59
|
+
process.stderr.write(line.endsWith('\n') ? line : line + '\n');
|
|
42
60
|
},
|
|
43
61
|
};
|
|
44
62
|
|
|
45
|
-
const USAGE =
|
|
63
|
+
const USAGE =
|
|
64
|
+
'usage: step describe [--sentinels] | step run <step> | step map-trigger <trigger>';
|
|
46
65
|
|
|
47
66
|
export function allSteps(data: PipelineData): AnyStep[] {
|
|
48
67
|
const out: AnyStep[] = [];
|
|
49
|
-
const
|
|
50
|
-
|
|
68
|
+
for (const { step } of stepsWithCases(data)) if (!out.includes(step)) out.push(step);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Every step with the `select` case conditions (`<ref>.<discriminator>=<value>`) it sits in. */
|
|
73
|
+
export function stepsWithCases(data: PipelineData): { step: AnyStep; cases: string[] }[] {
|
|
74
|
+
const out: { step: AnyStep; cases: string[] }[] = [];
|
|
75
|
+
const walk = (nodes: readonly StageNode[], cases: string[]) => {
|
|
76
|
+
for (const n of nodes) {
|
|
77
|
+
if (n.kind === 'stage') out.push(...n.steps.map((s) => ({ step: s.step, cases })));
|
|
78
|
+
else if (n.kind === 'when') walk(n.nodes, cases);
|
|
79
|
+
else {
|
|
80
|
+
const union = isConfigRef(n.on) ? unionAt(n.on) : undefined;
|
|
81
|
+
for (const [value, inner] of Object.entries(n.cases))
|
|
82
|
+
walk(
|
|
83
|
+
inner,
|
|
84
|
+
union
|
|
85
|
+
? [...cases, `${configRefPath(n.on)}.${union.discriminator}=${value}`]
|
|
86
|
+
: cases,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
51
90
|
};
|
|
52
|
-
walk(data.nodes);
|
|
53
|
-
for (const fh of data.failureHandlers)
|
|
91
|
+
walk(data.nodes, []);
|
|
92
|
+
for (const fh of data.failureHandlers) out.push({ step: fh.step, cases: [] });
|
|
54
93
|
return out;
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
/** The step `run <name>` executes: the only one so named, or the one in the cases the service selected (`OLVE_CASES`). */
|
|
97
|
+
export function findStep(
|
|
98
|
+
data: PipelineData,
|
|
99
|
+
name: string,
|
|
100
|
+
selected: readonly string[],
|
|
101
|
+
): AnyStep | string {
|
|
102
|
+
const candidates = stepsWithCases(data).filter((c) => c.step.name === name);
|
|
103
|
+
const distinct = candidates.filter(
|
|
104
|
+
(c, i) => candidates.findIndex((d) => d.step === c.step) === i,
|
|
105
|
+
);
|
|
106
|
+
if (distinct.length === 0) return `unknown step '${name}'`;
|
|
107
|
+
if (distinct.length === 1) return distinct[0]!.step;
|
|
108
|
+
const chosen = distinct.filter((c) => c.cases.every((k) => selected.includes(k)));
|
|
109
|
+
if (chosen.length === 1) return chosen[0]!.step;
|
|
110
|
+
return `step '${name}' is in several select cases; ${ENV_CASES} must name the selected one (got ${JSON.stringify(selected)})`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The select case conditions of the step being run (JSON array), set by the service for steps inside a select. */
|
|
114
|
+
export const ENV_CASES = 'OLVE_CASES';
|
|
115
|
+
|
|
57
116
|
/** Installs SIGTERM/SIGINT handlers that abort the returned signal (PID 1 ignores signals without them). */
|
|
58
117
|
export function cancellation(): { signal: AbortSignal; dispose(): void } {
|
|
59
118
|
const ac = new AbortController();
|
|
60
|
-
const onTerm = () => ac.abort(
|
|
61
|
-
const onInt = () =>
|
|
62
|
-
|
|
63
|
-
process.on(
|
|
119
|
+
const onTerm = () => ac.abort('SIGTERM');
|
|
120
|
+
const onInt = () =>
|
|
121
|
+
ac.signal.aborted ? process.exit(EXIT.sigint) : ac.abort('SIGINT');
|
|
122
|
+
process.on('SIGTERM', onTerm);
|
|
123
|
+
process.on('SIGINT', onInt);
|
|
64
124
|
return {
|
|
65
125
|
signal: ac.signal,
|
|
66
126
|
dispose() {
|
|
67
|
-
process.off(
|
|
68
|
-
process.off(
|
|
127
|
+
process.off('SIGTERM', onTerm);
|
|
128
|
+
process.off('SIGINT', onInt);
|
|
69
129
|
},
|
|
70
130
|
};
|
|
71
131
|
}
|
|
72
132
|
|
|
73
|
-
/** Resolves the function that implements `step` (
|
|
74
|
-
export async function resolveStepFunction(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
133
|
+
/** Resolves the function that implements `step` (carried by the step, or a handler module). */
|
|
134
|
+
export async function resolveStepFunction(
|
|
135
|
+
step: AnyStep,
|
|
136
|
+
handlers: HandlerRegistry,
|
|
137
|
+
): Promise<StepFunction | string> {
|
|
138
|
+
if (isImplementation(step)) return step.handler as unknown as StepFunction;
|
|
79
139
|
const load = handlers[step.name];
|
|
80
|
-
if (!load) return `step '${step.name}' has no handler
|
|
140
|
+
if (!load) return `step '${step.name}' has no handler`;
|
|
81
141
|
let mod: { default: unknown };
|
|
82
142
|
try {
|
|
83
143
|
mod = await load();
|
|
84
144
|
} catch (e) {
|
|
85
|
-
return `
|
|
145
|
+
return `the handler of step '${step.name}' failed to load: ${(e as Error)?.stack ?? String(e)}`;
|
|
86
146
|
}
|
|
87
147
|
const impl = mod.default;
|
|
88
|
-
if (!isImplementation(impl))
|
|
148
|
+
if (!isImplementation(impl))
|
|
149
|
+
return `the handler of step '${step.name}' must default-export implement(<step '${step.name}'>, …)`;
|
|
89
150
|
if (impl.step !== step)
|
|
90
|
-
return `
|
|
151
|
+
return `the handler of step '${step.name}' implements a different step object (it must be implement(<the '${step.name}' step from pipeline.ts>, …))`;
|
|
91
152
|
return impl.handler as unknown as StepFunction;
|
|
92
153
|
}
|
|
93
154
|
|
|
@@ -100,7 +161,7 @@ export async function finish(exec: Execution, io: HarnessIO): Promise<number> {
|
|
|
100
161
|
if (!url) return exec.exitCode;
|
|
101
162
|
try {
|
|
102
163
|
mkdirSync(p.tmp, { recursive: true });
|
|
103
|
-
const archive = join(p.tmp,
|
|
164
|
+
const archive = join(p.tmp, 'result.tar.gz');
|
|
104
165
|
await packResult(p.work, archive);
|
|
105
166
|
await uploadFile(url, archive, io.stderr);
|
|
106
167
|
} catch (e) {
|
|
@@ -110,18 +171,31 @@ export async function finish(exec: Execution, io: HarnessIO): Promise<number> {
|
|
|
110
171
|
return exec.exitCode;
|
|
111
172
|
}
|
|
112
173
|
|
|
113
|
-
function readJson(
|
|
174
|
+
function readJson(
|
|
175
|
+
path: string,
|
|
176
|
+
): { ok: true; value: unknown } | { ok: false; message: string } {
|
|
114
177
|
try {
|
|
115
|
-
return { ok: true, value: JSON.parse(readFileSync(path,
|
|
178
|
+
return { ok: true, value: JSON.parse(readFileSync(path, 'utf8')) };
|
|
116
179
|
} catch (e) {
|
|
117
180
|
return { ok: false, message: `cannot read ${path}: ${(e as Error).message}` };
|
|
118
181
|
}
|
|
119
182
|
}
|
|
120
183
|
|
|
121
|
-
async function describeMode(
|
|
122
|
-
|
|
184
|
+
async function describeMode(
|
|
185
|
+
cfg: HarnessConfig,
|
|
186
|
+
args: string[],
|
|
187
|
+
io: HarnessIO,
|
|
188
|
+
): Promise<number> {
|
|
189
|
+
const sentinels = args.includes('--sentinels');
|
|
123
190
|
try {
|
|
124
|
-
const text = manifestText(
|
|
191
|
+
const text = manifestText(
|
|
192
|
+
describePipeline(cfg.pipeline, {
|
|
193
|
+
handlerNames: Object.keys(cfg.handlers),
|
|
194
|
+
handlerHashes: cfg.handlerHashes,
|
|
195
|
+
kindHashes: cfg.kindHashes,
|
|
196
|
+
harness: cfg.harness,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
125
199
|
await io.stdout(sentinels ? `${MANIFEST_BEGIN}\n${text}${MANIFEST_END}\n` : text);
|
|
126
200
|
return EXIT.ok;
|
|
127
201
|
} catch (e) {
|
|
@@ -133,13 +207,17 @@ async function describeMode(cfg: HarnessConfig, args: string[], io: HarnessIO):
|
|
|
133
207
|
}
|
|
134
208
|
}
|
|
135
209
|
|
|
136
|
-
async function runMode(
|
|
137
|
-
|
|
210
|
+
async function runMode(
|
|
211
|
+
cfg: HarnessConfig,
|
|
212
|
+
name: string | undefined,
|
|
213
|
+
io: HarnessIO,
|
|
214
|
+
): Promise<number> {
|
|
215
|
+
if (!name) return (io.stderr(USAGE), EXIT.usage);
|
|
138
216
|
const data = pipelineData(cfg.pipeline);
|
|
139
|
-
const step =
|
|
140
|
-
if (
|
|
217
|
+
const step = findStep(data, name, selectedCases());
|
|
218
|
+
if (typeof step === 'string') return (io.stderr(step), EXIT.usage);
|
|
141
219
|
const fn = await resolveStepFunction(step, cfg.handlers);
|
|
142
|
-
if (typeof fn ===
|
|
220
|
+
if (typeof fn === 'string') return (io.stderr(fn), EXIT.configInvalid);
|
|
143
221
|
|
|
144
222
|
const p = paths();
|
|
145
223
|
removeStale(p.work);
|
|
@@ -151,11 +229,28 @@ async function runMode(cfg: HarnessConfig, name: string | undefined, io: Harness
|
|
|
151
229
|
let exec: Execution;
|
|
152
230
|
if (!raw.ok) {
|
|
153
231
|
io.stderr(raw.message);
|
|
154
|
-
exec = {
|
|
232
|
+
exec = {
|
|
233
|
+
envelope: { status: 'failed', error: { kind: 'input', message: raw.message } },
|
|
234
|
+
exitCode: EXIT.inputInvalid,
|
|
235
|
+
};
|
|
155
236
|
} else {
|
|
156
237
|
const run = runInfoFromEnv(step.name);
|
|
157
|
-
const ctx = createCtx({
|
|
158
|
-
|
|
238
|
+
const ctx = createCtx({
|
|
239
|
+
run,
|
|
240
|
+
secrets: step.secrets,
|
|
241
|
+
paths: { root: p.root, workFiles: p.files, tmp: p.tmp },
|
|
242
|
+
signal: cancel.signal,
|
|
243
|
+
logSink: io.stderr,
|
|
244
|
+
callbackUrls: callbackUrlsFromEnv(),
|
|
245
|
+
});
|
|
246
|
+
exec = await executeStep({
|
|
247
|
+
step,
|
|
248
|
+
fn,
|
|
249
|
+
rawInput: raw.value,
|
|
250
|
+
ctx,
|
|
251
|
+
workFiles: p.files,
|
|
252
|
+
log: io.stderr,
|
|
253
|
+
});
|
|
159
254
|
}
|
|
160
255
|
return await finish(exec, io);
|
|
161
256
|
} finally {
|
|
@@ -164,64 +259,121 @@ async function runMode(cfg: HarnessConfig, name: string | undefined, io: Harness
|
|
|
164
259
|
}
|
|
165
260
|
|
|
166
261
|
function isWebhookEvent(v: unknown): boolean {
|
|
167
|
-
if (typeof v !==
|
|
262
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v) || !('body' in v))
|
|
263
|
+
return false;
|
|
168
264
|
const headers = (v as { headers?: unknown }).headers;
|
|
169
|
-
return typeof headers ===
|
|
265
|
+
return typeof headers === 'object' && headers !== null && !Array.isArray(headers);
|
|
170
266
|
}
|
|
171
267
|
|
|
172
|
-
async function mapTriggerMode(
|
|
173
|
-
|
|
268
|
+
async function mapTriggerMode(
|
|
269
|
+
cfg: HarnessConfig,
|
|
270
|
+
name: string | undefined,
|
|
271
|
+
io: HarnessIO,
|
|
272
|
+
): Promise<number> {
|
|
273
|
+
if (!name) return (io.stderr(USAGE), EXIT.usage);
|
|
174
274
|
const data = pipelineData(cfg.pipeline);
|
|
175
275
|
const trigger = data.triggers.find((t) => t.name === name);
|
|
176
|
-
if (!trigger)
|
|
276
|
+
if (!trigger)
|
|
277
|
+
return (
|
|
278
|
+
io.stderr(
|
|
279
|
+
`unknown trigger '${name}' (known: ${data.triggers.map((t) => t.name).join(', ') || 'none'})`,
|
|
280
|
+
),
|
|
281
|
+
EXIT.usage
|
|
282
|
+
);
|
|
177
283
|
const p = paths();
|
|
178
284
|
removeStale(p.work);
|
|
179
285
|
mkdirSync(p.work, { recursive: true });
|
|
180
286
|
const raw = readJson(p.input);
|
|
181
|
-
const fail = (
|
|
287
|
+
const fail = (
|
|
288
|
+
kind: 'input' | 'output' | 'handler',
|
|
289
|
+
message: string,
|
|
290
|
+
exitCode: number,
|
|
291
|
+
): Execution => {
|
|
182
292
|
io.stderr(message);
|
|
183
|
-
return { envelope: { status:
|
|
293
|
+
return { envelope: { status: 'failed', error: { kind, message } }, exitCode };
|
|
184
294
|
};
|
|
185
295
|
let exec: Execution;
|
|
186
|
-
if (!raw.ok) exec = fail(
|
|
187
|
-
else if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
296
|
+
if (!raw.ok) exec = fail('input', raw.message, EXIT.inputInvalid);
|
|
297
|
+
else if (
|
|
298
|
+
trigger.kind === 'poll' &&
|
|
299
|
+
(typeof raw.value !== 'object' ||
|
|
300
|
+
raw.value === null ||
|
|
301
|
+
typeof (raw.value as { value?: unknown }).value !== 'string')
|
|
302
|
+
)
|
|
303
|
+
exec = fail(
|
|
304
|
+
'input',
|
|
305
|
+
`poll trigger '${name}': the event must be {"value": "<string>"}`,
|
|
306
|
+
EXIT.inputInvalid,
|
|
307
|
+
);
|
|
308
|
+
else if (
|
|
309
|
+
trigger.kind === 'push' &&
|
|
310
|
+
(typeof raw.value !== 'object' || raw.value === null || Array.isArray(raw.value))
|
|
311
|
+
)
|
|
312
|
+
exec = fail(
|
|
313
|
+
'input',
|
|
314
|
+
`push trigger: the event must be the webhook payload object`,
|
|
315
|
+
EXIT.inputInvalid,
|
|
316
|
+
);
|
|
317
|
+
else if (trigger.kind === 'webhook' && !isWebhookEvent(raw.value))
|
|
318
|
+
exec = fail(
|
|
319
|
+
'input',
|
|
320
|
+
`webhook trigger '${name}': the event must be {"headers": {...}, "body": ...}`,
|
|
321
|
+
EXIT.inputInvalid,
|
|
322
|
+
);
|
|
193
323
|
else {
|
|
194
324
|
let mapped: unknown;
|
|
195
325
|
let threw: unknown = undefined;
|
|
196
326
|
try {
|
|
197
327
|
mapped =
|
|
198
|
-
trigger.kind ===
|
|
199
|
-
|
|
200
|
-
|
|
328
|
+
trigger.kind === 'poll'
|
|
329
|
+
? await trigger.map((raw.value as { value: string }).value)
|
|
330
|
+
: trigger.kind === 'webhook'
|
|
331
|
+
? await trigger.map(raw.value as WebhookEvent)
|
|
332
|
+
: await trigger.map(raw.value);
|
|
201
333
|
} catch (e) {
|
|
202
|
-
threw = e ?? new Error(
|
|
334
|
+
threw = e ?? new Error('undefined');
|
|
203
335
|
}
|
|
204
|
-
if (threw !== undefined)
|
|
205
|
-
|
|
336
|
+
if (threw !== undefined)
|
|
337
|
+
exec = fail(
|
|
338
|
+
'handler',
|
|
339
|
+
`trigger '${name}' mapper threw: ${(threw as Error)?.message ?? String(threw)}`,
|
|
340
|
+
EXIT.handlerFailed,
|
|
341
|
+
);
|
|
342
|
+
else if (mapped === null && trigger.kind === 'webhook')
|
|
343
|
+
exec = {
|
|
344
|
+
envelope: { status: 'skipped', reason: `trigger '${name}' ignored the delivery` },
|
|
345
|
+
exitCode: EXIT.ok,
|
|
346
|
+
};
|
|
206
347
|
else {
|
|
207
|
-
const r = parseStrict(data.input.schema, mapped,
|
|
348
|
+
const r = parseStrict(data.input.schema, mapped, 'input');
|
|
208
349
|
exec = r.ok
|
|
209
|
-
? {
|
|
210
|
-
|
|
350
|
+
? {
|
|
351
|
+
envelope: { status: 'succeeded', output: r.value } satisfies Envelope,
|
|
352
|
+
exitCode: EXIT.ok,
|
|
353
|
+
}
|
|
354
|
+
: fail(
|
|
355
|
+
'output',
|
|
356
|
+
`trigger '${name}': mapped input does not match ${data.input.name}:\n ${r.issues.join('\n ')}`,
|
|
357
|
+
EXIT.outputInvalid,
|
|
358
|
+
);
|
|
211
359
|
}
|
|
212
360
|
}
|
|
213
361
|
return finish(exec, io);
|
|
214
362
|
}
|
|
215
363
|
|
|
216
364
|
/** Entry point of the compiled binary (and the dev runner). Returns the exit code. */
|
|
217
|
-
export async function runHarness(
|
|
365
|
+
export async function runHarness(
|
|
366
|
+
cfg: HarnessConfig,
|
|
367
|
+
argv: readonly string[],
|
|
368
|
+
io: HarnessIO = defaultIO,
|
|
369
|
+
): Promise<number> {
|
|
218
370
|
const [mode, ...rest] = argv;
|
|
219
371
|
switch (mode) {
|
|
220
|
-
case
|
|
372
|
+
case 'describe':
|
|
221
373
|
return describeMode(cfg, rest, io);
|
|
222
|
-
case
|
|
374
|
+
case 'run':
|
|
223
375
|
return runMode(cfg, rest[0], io);
|
|
224
|
-
case
|
|
376
|
+
case 'map-trigger':
|
|
225
377
|
return mapTriggerMode(cfg, rest[0], io);
|
|
226
378
|
default:
|
|
227
379
|
io.stderr(USAGE);
|
|
@@ -230,3 +382,12 @@ export async function runHarness(cfg: HarnessConfig, argv: readonly string[], io
|
|
|
230
382
|
}
|
|
231
383
|
|
|
232
384
|
export { formatIssues, formatLog };
|
|
385
|
+
|
|
386
|
+
function selectedCases(): string[] {
|
|
387
|
+
try {
|
|
388
|
+
const v = JSON.parse(process.env[ENV_CASES] ?? '[]') as unknown;
|
|
389
|
+
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];
|
|
390
|
+
} catch {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
}
|
package/src/harness/result.ts
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
// Writing, printing and uploading the result of `run` / `map-trigger`.
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { canonicalJson } from '../util/canonical.ts';
|
|
12
|
+
import { dirEntries, writeTarGz, type TarEntry } from '../util/tar.ts';
|
|
13
|
+
import { redactUrl } from '../ctx/files.ts';
|
|
14
|
+
import type { Envelope } from './execute.ts';
|
|
8
15
|
|
|
9
|
-
export const RESULT_BEGIN =
|
|
10
|
-
export const RESULT_END =
|
|
11
|
-
export const MANIFEST_BEGIN =
|
|
12
|
-
export const MANIFEST_END =
|
|
16
|
+
export const RESULT_BEGIN = '---OLVE-RESULT-BEGIN---';
|
|
17
|
+
export const RESULT_END = '---OLVE-RESULT-END---';
|
|
18
|
+
export const MANIFEST_BEGIN = '---OLVE-MANIFEST-BEGIN---';
|
|
19
|
+
export const MANIFEST_END = '---OLVE-MANIFEST-END---';
|
|
13
20
|
|
|
14
21
|
/** Atomically writes the envelope to `<work>/output.json`. */
|
|
15
22
|
export function writeEnvelope(work: string, envelope: Envelope): string {
|
|
16
23
|
mkdirSync(work, { recursive: true });
|
|
17
|
-
const path = join(work,
|
|
18
|
-
writeFileSync(path +
|
|
19
|
-
renameSync(path +
|
|
24
|
+
const path = join(work, 'output.json');
|
|
25
|
+
writeFileSync(path + '.partial', canonicalJson(envelope));
|
|
26
|
+
renameSync(path + '.partial', path);
|
|
20
27
|
return path;
|
|
21
28
|
}
|
|
22
29
|
|
|
@@ -27,9 +34,15 @@ export function sentinelBlock(envelope: Envelope): string {
|
|
|
27
34
|
|
|
28
35
|
/** Packs output.json + files/ into result.tar.gz (deterministic). */
|
|
29
36
|
export async function packResult(work: string, out: string): Promise<void> {
|
|
30
|
-
const entries: TarEntry[] = [
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const entries: TarEntry[] = [
|
|
38
|
+
{ path: 'output.json', type: 'file', mode: 0o644, source: join(work, 'output.json') },
|
|
39
|
+
];
|
|
40
|
+
const files = join(work, 'files');
|
|
41
|
+
if (existsSync(files))
|
|
42
|
+
entries.push(
|
|
43
|
+
{ path: 'files', type: 'dir', mode: 0o755 },
|
|
44
|
+
...dirEntries(files, 'files'),
|
|
45
|
+
);
|
|
33
46
|
await writeTarGz(out, entries);
|
|
34
47
|
}
|
|
35
48
|
|
|
@@ -48,7 +61,7 @@ export async function uploadFile(
|
|
|
48
61
|
let status = 0;
|
|
49
62
|
let detail: string;
|
|
50
63
|
try {
|
|
51
|
-
const res = await fetch(url, { method:
|
|
64
|
+
const res = await fetch(url, { method: 'PUT', body: Bun.file(file) });
|
|
52
65
|
status = res.status;
|
|
53
66
|
if (res.ok) return;
|
|
54
67
|
detail = `HTTP ${res.status} ${res.statusText}`;
|
|
@@ -57,7 +70,8 @@ export async function uploadFile(
|
|
|
57
70
|
detail = (e as Error).message;
|
|
58
71
|
}
|
|
59
72
|
const transient = status === 0 || TRANSIENT(status);
|
|
60
|
-
if (!transient || i >= attempts)
|
|
73
|
+
if (!transient || i >= attempts)
|
|
74
|
+
throw new Error(`upload to ${redactUrl(url)} failed: ${detail}`);
|
|
61
75
|
log(`upload to ${redactUrl(url)} failed (${detail}); retry ${i}/${attempts - 1}`);
|
|
62
76
|
await Bun.sleep(base * 2 ** (i - 1));
|
|
63
77
|
}
|
|
@@ -68,8 +82,10 @@ export async function uploadFile(
|
|
|
68
82
|
* *contents* of files/. The directories themselves are kept (olve-init creates them 1777).
|
|
69
83
|
*/
|
|
70
84
|
export function removeStale(work: string): void {
|
|
71
|
-
rmSync(join(work,
|
|
72
|
-
rmSync(join(work,
|
|
73
|
-
const files = join(work,
|
|
74
|
-
if (existsSync(files))
|
|
85
|
+
rmSync(join(work, 'output.json'), { force: true });
|
|
86
|
+
rmSync(join(work, 'output.json.partial'), { force: true });
|
|
87
|
+
const files = join(work, 'files');
|
|
88
|
+
if (existsSync(files))
|
|
89
|
+
for (const e of readdirSync(files))
|
|
90
|
+
rmSync(join(files, e), { recursive: true, force: true });
|
|
75
91
|
}
|