@olivervea/pipelines 2.0.0-beta.2 → 2.0.0-beta.6
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 +89 -36
- package/manifest.schema.json +88 -10
- package/package.json +1 -1
- package/src/build/build.ts +3 -3
- package/src/build/cli.ts +1 -1
- package/src/build/hashes.ts +1 -1
- package/src/build/project.ts +26 -5
- package/src/builder/pipeline.ts +77 -9
- package/src/core/config-refs.ts +77 -0
- package/src/core/config.ts +147 -0
- package/src/core/handlers.ts +3 -3
- package/src/core/steps.ts +29 -6
- package/src/ctx/index.ts +3 -0
- package/src/ctx/sh.ts +2 -1
- package/src/ctx/types.ts +4 -1
- package/src/describe/describe.ts +187 -13
- package/src/describe/manifest-schema.ts +37 -4
- package/src/dev/cli.ts +11 -4
- package/src/harness/execute.ts +7 -4
- package/src/harness/index.ts +1 -1
- package/src/harness/main.ts +49 -10
- package/src/harness/validate.ts +19 -1
- package/src/index.ts +4 -3
- package/src/library/checks.ts +23 -0
- package/src/library/compose-deploy.ts +93 -0
- package/src/library/github-source.ts +23 -4
- package/src/library/helm-deploy-ssh.ts +240 -87
- package/src/library/helm-deploy.ts +268 -0
- package/src/library/image-build.ts +313 -33
- package/src/library/index.ts +10 -3
- package/src/library/models.ts +16 -1
- package/src/library/npm.ts +33 -0
- package/src/library/ssh.ts +45 -0
- package/src/testing/index.ts +6 -4
- package/src/util/handler-hash.ts +1 -1
package/src/describe/describe.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// `describe`: evaluates the contracts (models + pipeline, never handlers) into the manifest and runs
|
|
2
2
|
// every describe-time check. All problems are collected and reported together.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { CONFIG_NAME, isConfigEntry, SECRET_REF_KIND } from "../core/config.ts";
|
|
5
|
+
import { CONFIG_CASE_KEY, CONFIG_REF_KEY } from "../core/config-refs.ts";
|
|
6
|
+
import { configRefPath, configRefTarget, isConfigRef, unionAt } from "../core/config.ts";
|
|
4
7
|
import { isModel, isOptional, MODEL_NAME, type AnyModel } from "../core/models.ts";
|
|
5
8
|
import { SECRET_NAME, type Secret } from "../core/secrets.ts";
|
|
6
9
|
import { RESERVED_STEP_NAMES, STEP_NAME, isStep, type AnyStep } from "../core/steps.ts";
|
|
@@ -20,7 +23,7 @@ export class DescribeError extends Error {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface DescribeOptions {
|
|
23
|
-
/** Step names that have a handler
|
|
26
|
+
/** Step names that have a handler (a `handlers/*.ts` default-exporting `implement(step, …)`). */
|
|
24
27
|
handlerNames: Iterable<string>;
|
|
25
28
|
/** Step name → handler hash (from the build; includes the harness identity). */
|
|
26
29
|
handlerHashes: Readonly<Record<string, string>>;
|
|
@@ -127,14 +130,46 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
127
130
|
return s.name;
|
|
128
131
|
};
|
|
129
132
|
|
|
133
|
+
// ── config entries ──
|
|
134
|
+
const configs = new Map<string, { description: string; required: boolean; schema: JsonSchema; key: string; secretRefs: string[] }>();
|
|
135
|
+
const useConfig = (e: unknown, at: string): string | undefined => {
|
|
136
|
+
if (!isConfigEntry(e)) return void problems.push(`${at}: config entries must be config("name", "description", schema)`);
|
|
137
|
+
if (!CONFIG_NAME.test(e.name)) problems.push(`${at}: config name '${e.name}' must match ${CONFIG_NAME}`);
|
|
138
|
+
let schema: JsonSchema;
|
|
139
|
+
try {
|
|
140
|
+
schema = z.toJSONSchema(e.schema, { io: "input", target: "draft-2020-12" }) as JsonSchema;
|
|
141
|
+
delete schema.$schema;
|
|
142
|
+
schema = canonicalize(schema) as JsonSchema;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
problems.push(`${at}: config '${e.name}' is not representable as JSON Schema (${(err as Error).message})`);
|
|
145
|
+
return e.name;
|
|
146
|
+
}
|
|
147
|
+
const key = JSON.stringify([e.description, schema]);
|
|
148
|
+
const prev = configs.get(e.name);
|
|
149
|
+
if (!prev) configs.set(e.name, { description: e.description, required: !e.schema.safeParse(undefined).success, schema, key, secretRefs: secretRefsOf(schema) });
|
|
150
|
+
else if (prev.key !== key) problems.push(`two different config entries are named '${e.name}' (one description and schema per config name)`);
|
|
151
|
+
return e.name;
|
|
152
|
+
};
|
|
153
|
+
|
|
130
154
|
// ── step identity ──
|
|
131
155
|
const steps = new Map<string, AnyStep>();
|
|
132
|
-
const
|
|
156
|
+
const placements = new Map<string, { step: AnyStep; cases: readonly CaseOf[] }[]>();
|
|
157
|
+
// Two places are mutually exclusive when they sit in different cases of the same select.
|
|
158
|
+
const exclusive = (a: readonly CaseOf[], b: readonly CaseOf[]) => a.some((x) => b.some((y) => y.select === x.select && y.value !== x.value));
|
|
159
|
+
const registerStep = (s: AnyStep, where: string, cases: readonly CaseOf[] = []) => {
|
|
133
160
|
if (typeof s.name !== "string" || !STEP_NAME.test(s.name))
|
|
134
161
|
problems.push(`${where}: step name '${String(s.name)}' must be lower-case kebab (${STEP_NAME})`);
|
|
135
162
|
if (RESERVED.has(s.name)) problems.push(`${where}: '${s.name}' is reserved and cannot be a step name`);
|
|
136
|
-
|
|
137
|
-
|
|
163
|
+
const prev = placements.get(s.name) ?? [];
|
|
164
|
+
if (prev.some((p) => !exclusive(p.cases, cases))) problems.push(`duplicate step name '${s.name}'`);
|
|
165
|
+
else if (prev.length) {
|
|
166
|
+
if (prev.some((p) => p.step !== s && p.step.kind === "custom" && s.kind === "custom"))
|
|
167
|
+
problems.push(`step '${s.name}': two different custom steps share the name across select cases; handlers pair by name, so use one step or different names`);
|
|
168
|
+
if (prev.some((p) => (p.step.output?.name ?? null) !== (s.output?.name ?? null)))
|
|
169
|
+
problems.push(`step '${s.name}': its select cases must give it the same output model`);
|
|
170
|
+
}
|
|
171
|
+
placements.set(s.name, [...prev, { step: s, cases }]);
|
|
172
|
+
if (!steps.has(s.name)) steps.set(s.name, s);
|
|
138
173
|
};
|
|
139
174
|
|
|
140
175
|
const inputModel = data.input;
|
|
@@ -144,7 +179,7 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
144
179
|
const outputViewOf = (modelName: string | null): JsonSchema | undefined => (modelName ? models.get(modelName)?.schemas.output : undefined);
|
|
145
180
|
|
|
146
181
|
// ── one step ──
|
|
147
|
-
const describeStep = (node: StageStepNode, avail: Map<string, Avail>, failureHandler: boolean): StepDescriptorT => {
|
|
182
|
+
const describeStep = (node: StageStepNode, avail: Map<string, Avail>, failureHandler: boolean, cases: readonly CaseOf[] = []): StepDescriptorT => {
|
|
148
183
|
const s = node.step;
|
|
149
184
|
const at = `step '${s.name}'`;
|
|
150
185
|
if (typeof s.image !== "string" || s.image.trim() === "") problems.push(`${at}: image must be a non-empty string`);
|
|
@@ -194,11 +229,30 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
194
229
|
}
|
|
195
230
|
}
|
|
196
231
|
|
|
232
|
+
for (const key of Object.keys(s.input as Record<string, unknown>))
|
|
233
|
+
if (key.startsWith("$")) problems.push(`${at}: input '${key}': keys starting with '$' are reserved`);
|
|
197
234
|
const secretNames = s.secrets.map((x) => useSecret(x, at)).sort();
|
|
235
|
+
const configNames = [...new Set((s.config ?? []).map((c) => useConfig(c, at)).filter((n): n is string => n !== undefined))].sort();
|
|
236
|
+
// A library step that references parts of an entry needs the secrets those parts name; any other
|
|
237
|
+
// step receiving an entry needs every secret the entry names.
|
|
238
|
+
let refs: { ref: string; case?: string }[] = [];
|
|
239
|
+
try {
|
|
240
|
+
refs = s.builtin ? recordedConfigRefs(s.builtin.manifestConfig()) : [];
|
|
241
|
+
} catch {
|
|
242
|
+
// reported with the library configuration below
|
|
243
|
+
}
|
|
244
|
+
for (const name of configNames) {
|
|
245
|
+
const into = refs.filter((r) => r.ref.split(".")[0] === name);
|
|
246
|
+
const needed = into.length === 0
|
|
247
|
+
? configs.get(name)?.secretRefs ?? []
|
|
248
|
+
: [...new Set(into.flatMap((r) => secretRefsOf(subSchemas(configs.get(name)?.schema, r.ref.split(".").slice(1), r.case, name))))].sort();
|
|
249
|
+
for (const ref of needed)
|
|
250
|
+
if (!secretNames.includes(ref)) problems.push(`${at}: config '${name}' references secret '${ref}' (secretRef), so the step must declare it in secrets`);
|
|
251
|
+
}
|
|
198
252
|
let handlerHash: string;
|
|
199
253
|
let config: Record<string, unknown> | undefined;
|
|
200
254
|
if (s.kind === "custom") {
|
|
201
|
-
if (!handlerNames.has(s.name)) problems.push(`${at} has no handler (
|
|
255
|
+
if (!handlerNames.has(s.name)) problems.push(`${at} has no handler (add a handlers/*.ts default-exporting implement(${camel(s.name)}, …))`);
|
|
202
256
|
handlerHash = opts.handlerHashes[s.name] ?? "";
|
|
203
257
|
if (handlerNames.has(s.name) && !handlerHash) problems.push(`${at}: no handler hash was computed`);
|
|
204
258
|
} else if (s.builtin) {
|
|
@@ -208,6 +262,13 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
208
262
|
problems.push(`${at}: invalid ${s.kind} configuration (${(e as Error).message})`);
|
|
209
263
|
config = {};
|
|
210
264
|
}
|
|
265
|
+
for (const { ref, case: kase } of recordedConfigRefs(config)) {
|
|
266
|
+
const [entry, ...path] = ref.split(".") as [string, ...string[]];
|
|
267
|
+
if (kase !== undefined && !cases.some((c) => c.cond === kase))
|
|
268
|
+
problems.push(`${at}: config reference '${ref}' is narrowed to the select case '${kase}' and can only be used inside it`);
|
|
269
|
+
if (!configNames.includes(entry)) problems.push(`${at}: config reference '${ref}': the step does not receive config entry '${entry}'`);
|
|
270
|
+
else if (!resolveField(configs.get(entry)?.schema, path)) problems.push(`${at}: config reference '${ref}': config '${entry}' has no field '${path.join(".")}'`);
|
|
271
|
+
}
|
|
211
272
|
handlerHash = libraryHandlerHash(s.kind, config, opts.harness);
|
|
212
273
|
} else {
|
|
213
274
|
problems.push(`${at}: library step of kind '${s.kind}' has no implementation`);
|
|
@@ -230,7 +291,10 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
230
291
|
...(s.timeoutSeconds !== undefined ? { timeoutSeconds: s.timeoutSeconds } : {}),
|
|
231
292
|
...(s.retries !== undefined ? { retries: s.retries } : {}),
|
|
232
293
|
...(config !== undefined ? { config: config as Record<string, z.core.util.JSONType> } : {}),
|
|
294
|
+
...(configNames.length ? { configEntries: configNames } : {}),
|
|
295
|
+
...(s.clusterAccess === true ? { clusterAccess: true as const } : {}),
|
|
233
296
|
};
|
|
297
|
+
if (s.clusterAccess !== undefined && typeof s.clusterAccess !== "boolean") problems.push(`${at}: clusterAccess must be a boolean`);
|
|
234
298
|
if (s.output && !isModel(s.output)) problems.push(`${at}: output must be a model`);
|
|
235
299
|
return out;
|
|
236
300
|
};
|
|
@@ -275,17 +339,22 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
275
339
|
};
|
|
276
340
|
|
|
277
341
|
// ── stages ──
|
|
278
|
-
|
|
342
|
+
let selects = 0;
|
|
343
|
+
const walk = (nodes: readonly StageNode[], avail: Map<string, Avail>, cases: readonly CaseOf[] = []): Stage[] => {
|
|
279
344
|
const out: Stage[] = [];
|
|
280
345
|
for (const n of nodes) {
|
|
346
|
+
if (n.kind === "select") {
|
|
347
|
+
out.push(describeSelect(n, avail, cases, ++selects));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
281
350
|
if (n.kind === "stage") {
|
|
282
351
|
const descs = n.steps.map((ss) => {
|
|
283
352
|
if (!isStep(ss.step)) {
|
|
284
353
|
problems.push("a stage entry is not a step");
|
|
285
354
|
return undefined;
|
|
286
355
|
}
|
|
287
|
-
registerStep(ss.step, "stage");
|
|
288
|
-
return describeStep(ss, avail, false);
|
|
356
|
+
registerStep(ss.step, "stage", cases);
|
|
357
|
+
return describeStep(ss, avail, false, cases);
|
|
289
358
|
}).filter((d): d is StepDescriptorT => d !== undefined);
|
|
290
359
|
// Outputs become available only after the barrier (same-stage siblings can't see each other).
|
|
291
360
|
for (const ss of n.steps) if (isStep(ss.step)) avail.set(ss.step.name, { model: ss.step.output?.name ?? null, maybe: false });
|
|
@@ -305,13 +374,43 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
305
374
|
when[field] = values;
|
|
306
375
|
}
|
|
307
376
|
const inner = new Map(avail);
|
|
308
|
-
const stages = walk(n.nodes, inner);
|
|
377
|
+
const stages = walk(n.nodes, inner, cases);
|
|
309
378
|
for (const [name, a] of inner) if (!avail.has(name)) avail.set(name, { model: a.model, maybe: true });
|
|
310
379
|
out.push({ when, stages });
|
|
311
380
|
}
|
|
312
381
|
}
|
|
313
382
|
return out;
|
|
314
383
|
};
|
|
384
|
+
const describeSelect = (n: Extract<StageNode, { kind: "select" }>, avail: Map<string, Avail>, cases: readonly CaseOf[], id: number): Stage => {
|
|
385
|
+
if (!isConfigRef(n.on)) {
|
|
386
|
+
problems.push("select: the first argument must be a config reference");
|
|
387
|
+
return { select: "", discriminator: "", cases: {} };
|
|
388
|
+
}
|
|
389
|
+
const ref = configRefPath(n.on);
|
|
390
|
+
const { entry, case: kase } = configRefTarget(n.on);
|
|
391
|
+
useConfig(entry, `select '${ref}'`);
|
|
392
|
+
if (kase !== undefined && !cases.some((c) => c.cond === kase))
|
|
393
|
+
problems.push(`select '${ref}': the reference is narrowed to the case '${kase}' and can only be used inside it`);
|
|
394
|
+
const union = unionAt(n.on);
|
|
395
|
+
if (!union) problems.push(`select '${ref}': it must reference a z.discriminatedUnion in config '${entry.name}'`);
|
|
396
|
+
else {
|
|
397
|
+
for (const v of union.values) if (!Object.hasOwn(n.cases, v)) problems.push(`select '${ref}': no case for ${union.discriminator} '${v}'`);
|
|
398
|
+
for (const v of Object.keys(n.cases)) if (!union.values.includes(v)) problems.push(`select '${ref}': '${v}' is not a value of ${union.discriminator} (${union.values.join(", ")})`);
|
|
399
|
+
}
|
|
400
|
+
const discriminator = union?.discriminator ?? "";
|
|
401
|
+
const branches = Object.entries(n.cases).map(([value, nodes]) => {
|
|
402
|
+
const inner = new Map(avail);
|
|
403
|
+
const stages = walk(nodes, inner, [...cases, { select: id, value, cond: `${ref}.${discriminator}=${value}` }]);
|
|
404
|
+
return { value, inner, stages };
|
|
405
|
+
});
|
|
406
|
+
// A step every case has is as available as in its cases; one only some cases have is maybe-available.
|
|
407
|
+
const added = new Set(branches.flatMap((b) => [...b.inner.keys()].filter((k) => !avail.has(k))));
|
|
408
|
+
for (const name of added) {
|
|
409
|
+
const present = branches.map((b) => b.inner.get(name)).filter((a): a is Avail => a !== undefined);
|
|
410
|
+
avail.set(name, { model: present[0]!.model, maybe: present.length < branches.length || present.some((a) => a.maybe) });
|
|
411
|
+
}
|
|
412
|
+
return { select: ref, discriminator, cases: Object.fromEntries(branches.map((b) => [b.value, b.stages])) };
|
|
413
|
+
};
|
|
315
414
|
const stages = walk(data.nodes, new Map([["input", { model: inputModel.name, maybe: false }]]));
|
|
316
415
|
|
|
317
416
|
// ── failure handlers ──
|
|
@@ -329,11 +428,11 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
329
428
|
return { step: describeStep({ step: fh.step }, new Map(), true), steps: scope };
|
|
330
429
|
}).filter((x) => x !== undefined);
|
|
331
430
|
|
|
332
|
-
// ──
|
|
431
|
+
// ── handlers without a step ──
|
|
333
432
|
for (const h of handlerNames) {
|
|
334
433
|
const s = steps.get(h);
|
|
335
|
-
if (!s) problems.push(`
|
|
336
|
-
else if (s.kind !== "custom") problems.push(`
|
|
434
|
+
if (!s) problems.push(`a handler implements step '${h}', which is not in the pipeline`);
|
|
435
|
+
else if (s.kind !== "custom") problems.push(`a handler implements '${h}', a library step (${s.kind}) that carries its own implementation`);
|
|
337
436
|
}
|
|
338
437
|
|
|
339
438
|
// ── triggers ──
|
|
@@ -369,6 +468,13 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
369
468
|
harness: opts.harness,
|
|
370
469
|
models: Object.fromEntries([...models].map(([k, v]) => [k, v.schemas])),
|
|
371
470
|
secrets: [...secrets].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([name, description]) => ({ name, description })),
|
|
471
|
+
...(configs.size
|
|
472
|
+
? {
|
|
473
|
+
config: [...configs]
|
|
474
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
475
|
+
.map(([name, c]) => ({ name, description: c.description, required: c.required, schema: c.schema })),
|
|
476
|
+
}
|
|
477
|
+
: {}),
|
|
372
478
|
triggers,
|
|
373
479
|
stages,
|
|
374
480
|
failureHandlers,
|
|
@@ -381,6 +487,74 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
381
487
|
/** Canonical manifest text (what `describe` prints and the service hashes). */
|
|
382
488
|
export const manifestText = (m: ManifestT): string => canonicalJson(m);
|
|
383
489
|
|
|
490
|
+
/**
|
|
491
|
+
* The schemas at `path` inside an entry's schema: every union branch that has the field, except where
|
|
492
|
+
* `kase` (`<entry>.<path>.<discriminator>=<value>`) narrows a union to one branch.
|
|
493
|
+
*/
|
|
494
|
+
export function subSchemas(schema: unknown, path: readonly string[], kase: string | undefined, entry: string): unknown[] {
|
|
495
|
+
let narrow: { at: string; discriminator: string; value: string } | undefined;
|
|
496
|
+
if (kase) {
|
|
497
|
+
const eq = kase.lastIndexOf("=");
|
|
498
|
+
const left = kase.slice(0, eq);
|
|
499
|
+
const dot = left.lastIndexOf(".");
|
|
500
|
+
narrow = { at: left.slice(0, dot), discriminator: left.slice(dot + 1), value: kase.slice(eq + 1) };
|
|
501
|
+
}
|
|
502
|
+
const walk = (node: unknown, rest: readonly string[], here: string): unknown[] => {
|
|
503
|
+
if (node === null || typeof node !== "object") return [];
|
|
504
|
+
const o = node as JsonSchema;
|
|
505
|
+
const branches = (o.oneOf ?? o.anyOf) as JsonSchema[] | undefined;
|
|
506
|
+
if (Array.isArray(branches)) {
|
|
507
|
+
const chosen = narrow && narrow.at === here
|
|
508
|
+
? branches.filter((b) => ((b.properties as Record<string, JsonSchema> | undefined)?.[narrow!.discriminator] as JsonSchema | undefined)?.const === narrow!.value)
|
|
509
|
+
: branches;
|
|
510
|
+
return chosen.flatMap((b) => walk(b, rest, here));
|
|
511
|
+
}
|
|
512
|
+
if (rest.length === 0) return [node];
|
|
513
|
+
const props = o.properties as Record<string, JsonSchema> | undefined;
|
|
514
|
+
if (props && Object.hasOwn(props, rest[0]!)) return walk(props[rest[0]!], rest.slice(1), `${here}.${rest[0]}`);
|
|
515
|
+
return [];
|
|
516
|
+
};
|
|
517
|
+
return walk(schema, path, entry);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Secret names of every secretRef inside a config entry's JSON Schema. */
|
|
521
|
+
export function secretRefsOf(schema: unknown): string[] {
|
|
522
|
+
const out = new Set<string>();
|
|
523
|
+
const walk = (v: unknown) => {
|
|
524
|
+
if (Array.isArray(v)) return v.forEach(walk);
|
|
525
|
+
if (v === null || typeof v !== "object") return;
|
|
526
|
+
const o = v as JsonSchema;
|
|
527
|
+
if (o["x-olve-kind"] === SECRET_REF_KIND) {
|
|
528
|
+
const name = (o.properties as { secret?: { const?: unknown } } | undefined)?.secret?.const;
|
|
529
|
+
if (typeof name === "string") out.add(name);
|
|
530
|
+
}
|
|
531
|
+
Object.values(o).forEach(walk);
|
|
532
|
+
};
|
|
533
|
+
walk(schema);
|
|
534
|
+
return [...out].sort();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** The `{"$config": "<entry>.<path>"[, "$case": …]}` references in a library step's recorded configuration. */
|
|
538
|
+
export function recordedConfigRefs(v: unknown, out: { ref: string; case?: string }[] = []): { ref: string; case?: string }[] {
|
|
539
|
+
if (Array.isArray(v)) v.forEach((x) => recordedConfigRefs(x, out));
|
|
540
|
+
else if (v !== null && typeof v === "object") {
|
|
541
|
+
const o = v as Record<string, unknown>;
|
|
542
|
+
const keys = Object.keys(o).sort();
|
|
543
|
+
const isRef = typeof o[CONFIG_REF_KEY] === "string"
|
|
544
|
+
&& (keys.length === 1 ? keys[0] === CONFIG_REF_KEY : keys.length === 2 && keys[0] === CONFIG_CASE_KEY && keys[1] === CONFIG_REF_KEY && typeof o[CONFIG_CASE_KEY] === "string");
|
|
545
|
+
if (isRef) out.push({ ref: o[CONFIG_REF_KEY] as string, ...(keys.length === 2 ? { case: o[CONFIG_CASE_KEY] as string } : {}) });
|
|
546
|
+
else Object.values(o).forEach((x) => recordedConfigRefs(x, out));
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** A select case a step sits in: which select (numbered in describe order), which value, and its condition text. */
|
|
552
|
+
interface CaseOf {
|
|
553
|
+
select: number;
|
|
554
|
+
value: string;
|
|
555
|
+
cond: string;
|
|
556
|
+
}
|
|
557
|
+
|
|
384
558
|
function camel(name: string): string {
|
|
385
559
|
return name.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
|
386
560
|
}
|
|
@@ -7,6 +7,7 @@ export const API_VERSION = "2.0";
|
|
|
7
7
|
const stepName = z.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/).describe("Step name: lower-case kebab, 1-63 chars. The step's identity.");
|
|
8
8
|
const modelName = z.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/).describe("Model name (key of `models`).");
|
|
9
9
|
const secretName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/).describe("Secret name; also the env var name in the step container.");
|
|
10
|
+
const configName = z.string().regex(/^[a-z][a-zA-Z0-9]*$/).describe("Config entry name; a key of the pipeline's config document.");
|
|
10
11
|
const hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
11
12
|
const Json = z.json().meta({ id: "Json", description: "Any JSON value." });
|
|
12
13
|
const jsonSchema = z.record(z.string(), z.unknown()).describe("A JSON Schema (draft 2020-12) object, canonical (sorted keys, no $schema).");
|
|
@@ -50,7 +51,7 @@ export const StepDescriptor = z
|
|
|
50
51
|
.strictObject({
|
|
51
52
|
name: stepName,
|
|
52
53
|
description: z.string().describe("What the step does, shown in the UI (may be empty)."),
|
|
53
|
-
kind: z.
|
|
54
|
+
kind: z.string().min(1).describe("'custom' = handlers/<name>.ts; others are SDK library steps. A label: the service accepts any kind."),
|
|
54
55
|
image: z.string().min(1).describe("Container image the step runs in."),
|
|
55
56
|
inputTypes: z.record(z.string(), InputType).describe("Declared input entries, keyed like input.json."),
|
|
56
57
|
input: z.record(z.string(), Mapping).describe("How the service assembles input.json: one mapping per input key (same keys as inputTypes)."),
|
|
@@ -59,11 +60,25 @@ export const StepDescriptor = z
|
|
|
59
60
|
handlerHash: hash.describe("Implementation identity: handler bundle (contract files excluded) + harness (bun + sdkHash); for library steps kind + config + harness."),
|
|
60
61
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
61
62
|
retries: z.number().int().nonnegative().optional(),
|
|
62
|
-
config: z
|
|
63
|
+
config: z
|
|
64
|
+
.record(z.string(), Json)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Library-step configuration (display only; already covered by handlerHash). A config reference is recorded as {\"$config\": \"<entry>.<field>…\"}; the entry is in configEntries."),
|
|
67
|
+
configEntries: z
|
|
68
|
+
.array(configName)
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Config entries the step receives (sorted; omitted when none). The service delivers them in input.json under '$config'."),
|
|
71
|
+
clusterAccess: z
|
|
72
|
+
.literal(true)
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("The step pod gets the instance's step ServiceAccount token (Kubernetes:StepServiceAccount), if the instance grants one. Omitted = no token."),
|
|
63
75
|
})
|
|
64
76
|
.meta({ id: "Step" });
|
|
65
77
|
|
|
66
|
-
export type Stage =
|
|
78
|
+
export type Stage =
|
|
79
|
+
| { steps: z.infer<typeof StepDescriptor>[] }
|
|
80
|
+
| { when: Record<string, (string | number | boolean)[]>; stages: Stage[] }
|
|
81
|
+
| { select: string; discriminator: string; cases: Record<string, Stage[]> };
|
|
67
82
|
export const Stage: z.ZodType<Stage> = z
|
|
68
83
|
.union([
|
|
69
84
|
z.strictObject({ steps: z.array(StepDescriptor).min(1).describe("Steps run in parallel; the stage is a barrier.") }),
|
|
@@ -73,6 +88,13 @@ export const Stage: z.ZodType<Stage> = z
|
|
|
73
88
|
),
|
|
74
89
|
stages: z.array(z.lazy(() => Stage)),
|
|
75
90
|
}),
|
|
91
|
+
z.strictObject({
|
|
92
|
+
select: z.string().min(1).describe("Config reference (`<entry>.<field>…`) to a discriminated union; the run's config snapshot picks the case."),
|
|
93
|
+
discriminator: z.string().min(1).describe("The union's literal field whose value names the case."),
|
|
94
|
+
cases: z
|
|
95
|
+
.record(z.string(), z.array(z.lazy(() => Stage)))
|
|
96
|
+
.describe("Discriminator value → stages. Exactly one case runs; a step name may repeat across cases, nowhere else."),
|
|
97
|
+
}),
|
|
76
98
|
])
|
|
77
99
|
.meta({ id: "Stage" });
|
|
78
100
|
|
|
@@ -117,12 +139,23 @@ export const Manifest = z
|
|
|
117
139
|
harness: z
|
|
118
140
|
.strictObject({
|
|
119
141
|
bun: z.string().describe("Bun runtime version compiled into the binary."),
|
|
120
|
-
sdk: z.string().describe("@
|
|
142
|
+
sdk: z.string().describe("@olivervea/pipelines package version."),
|
|
121
143
|
sdkHash: hash.describe("Hash of the SDK runtime sources + zod version."),
|
|
122
144
|
})
|
|
123
145
|
.describe("Harness identity. Already folded into every handlerHash."),
|
|
124
146
|
models: z.record(modelName, ModelSchemas).describe("Every model referenced by the pipeline (input, step inputs/outputs)."),
|
|
125
147
|
secrets: z.array(z.strictObject({ name: secretName, description: z.string() })).describe("All declared secrets (steps + poll triggers), sorted by name."),
|
|
148
|
+
config: z
|
|
149
|
+
.array(
|
|
150
|
+
z.strictObject({
|
|
151
|
+
name: configName,
|
|
152
|
+
description: z.string(),
|
|
153
|
+
required: z.boolean().describe("False when the schema accepts a missing value (optional, or a default)."),
|
|
154
|
+
schema: jsonSchema.describe("z.input view: validates the config document's value for this entry (defaults listed)."),
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Config entries used by steps, sorted by name (omitted when none). Fields of kind secretRef ({secret: NAME}) name a secret."),
|
|
126
159
|
triggers: z.array(Trigger),
|
|
127
160
|
stages: z.array(Stage),
|
|
128
161
|
failureHandlers: z.array(
|
package/src/dev/cli.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// olve-dev — run a pipeline from source through the same harness path as the compiled binary.
|
|
3
3
|
// olve-dev describe <dir> [--sentinels]
|
|
4
|
-
// olve-dev run <dir> <step> [--input-file f] [--olve-dir d] [--secret NAME=value]...
|
|
4
|
+
// olve-dev run <dir> <step> [--input-file f] [--config-file f] [--olve-dir d] [--secret NAME=value]...
|
|
5
5
|
// olve-dev map-trigger <dir> <trigger> --event-file f [--olve-dir d]
|
|
6
6
|
// $OLVE_DIR defaults to <dir>/.olve; outputs land in <dir>/.olve/work/.
|
|
7
|
-
import { copyFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import { runHarness, type HarnessConfig } from "../harness/main.ts";
|
|
@@ -13,11 +13,11 @@ import { handlerHashes } from "../build/hashes.ts";
|
|
|
13
13
|
import { harnessInfo, loadProject, ProjectError } from "../build/project.ts";
|
|
14
14
|
|
|
15
15
|
const USAGE = `usage: olve-dev describe <dir> [--sentinels]
|
|
16
|
-
olve-dev run <dir> <step> [--input-file f] [--olve-dir d] [--secret NAME=value]...
|
|
16
|
+
olve-dev run <dir> <step> [--input-file f] [--config-file f] [--olve-dir d] [--secret NAME=value]...
|
|
17
17
|
olve-dev map-trigger <dir> <trigger> --event-file f [--olve-dir d]`;
|
|
18
18
|
|
|
19
19
|
export async function devConfig(dir: string, withHashes: boolean): Promise<HarnessConfig> {
|
|
20
|
-
const project = loadProject(dir);
|
|
20
|
+
const project = await loadProject(dir);
|
|
21
21
|
const mod = (await import(project.pipelineFile)) as { default?: unknown };
|
|
22
22
|
if (!mod.default) throw new ProjectError(`${project.pipelineFile} has no default export (export default pipeline({...})...)`);
|
|
23
23
|
const harness = harnessInfo();
|
|
@@ -38,6 +38,7 @@ export async function devMain(argv: string[]): Promise<number> {
|
|
|
38
38
|
allowPositionals: true,
|
|
39
39
|
options: {
|
|
40
40
|
"input-file": { type: "string" },
|
|
41
|
+
"config-file": { type: "string" },
|
|
41
42
|
"event-file": { type: "string" },
|
|
42
43
|
"olve-dir": { type: "string" },
|
|
43
44
|
secret: { type: "string", multiple: true },
|
|
@@ -68,6 +69,12 @@ export async function devMain(argv: string[]): Promise<number> {
|
|
|
68
69
|
}
|
|
69
70
|
const file = mode === "run" ? values["input-file"] : values["event-file"];
|
|
70
71
|
if (file) copyFileSync(resolve(file), join(olveDir, "input.json"));
|
|
72
|
+
if (mode === "run" && values["config-file"]) {
|
|
73
|
+
const inputFile = join(olveDir, "input.json");
|
|
74
|
+
const input = (existsSync(inputFile) ? JSON.parse(readFileSync(inputFile, "utf8")) : {}) as Record<string, unknown>;
|
|
75
|
+
input.$config = JSON.parse(readFileSync(resolve(values["config-file"]), "utf8"));
|
|
76
|
+
writeFileSync(inputFile, JSON.stringify(input));
|
|
77
|
+
}
|
|
71
78
|
else if (mode === "map-trigger") return console.error("--event-file is required"), EXIT.usage;
|
|
72
79
|
return await runHarness(await devConfig(dir, false), [mode, name]);
|
|
73
80
|
} catch (e) {
|
package/src/harness/execute.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Ctx, RunInfo } from "../ctx/types.ts";
|
|
|
8
8
|
import { objectPath } from "../ctx/files.ts";
|
|
9
9
|
import { sha256File } from "../util/hash.ts";
|
|
10
10
|
import { EXIT } from "./exit.ts";
|
|
11
|
-
import { validateInput, validateOutput } from "./validate.ts";
|
|
11
|
+
import { validateConfig, validateInput, validateOutput } from "./validate.ts";
|
|
12
12
|
|
|
13
13
|
export type FailureKind = "handler" | "input" | "output";
|
|
14
14
|
export type Envelope =
|
|
@@ -58,13 +58,16 @@ export async function executeStep(args: {
|
|
|
58
58
|
workFiles: string;
|
|
59
59
|
log: (line: string) => void;
|
|
60
60
|
}): Promise<Execution> {
|
|
61
|
-
const { step,
|
|
61
|
+
const { step, log } = args;
|
|
62
62
|
const input = validateInput(step, args.rawInput);
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const config = validateConfig(step, args.rawInput);
|
|
64
|
+
if (!input.ok || !config.ok) {
|
|
65
|
+
const issues = [...(input.ok ? [] : input.issues), ...(config.ok ? [] : config.issues)];
|
|
66
|
+
const message = `step '${step.name}': input does not match the step's inputs:\n ${issues.join("\n ")}`;
|
|
65
67
|
log(message);
|
|
66
68
|
return { envelope: { status: "failed", error: { kind: "input", message } }, exitCode: EXIT.inputInvalid };
|
|
67
69
|
}
|
|
70
|
+
const ctx: Ctx = { ...args.ctx, config: Object.freeze(config.value) as Ctx["config"] };
|
|
68
71
|
|
|
69
72
|
let value: unknown;
|
|
70
73
|
try {
|
package/src/harness/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @
|
|
1
|
+
// @olivervea/pipelines/harness — the runtime the build tool compiles into the step binary.
|
|
2
2
|
export { runHarness, defaultIO, allSteps, resolveStepFunction, type HarnessConfig, type HandlerRegistry, type HarnessIO } from "./main.ts";
|
|
3
3
|
export { EXIT } from "./exit.ts";
|
|
4
4
|
export { ENV, paths, runInfoFromEnv } from "./env.ts";
|
package/src/harness/main.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { isImplementation } from "../core/handlers.ts";
|
|
8
|
+
import { configRefPath, isConfigRef, unionAt } from "../core/config.ts";
|
|
8
9
|
import type { AnyStep } from "../core/steps.ts";
|
|
9
10
|
import type { WebhookEvent } from "../core/webhooks.ts";
|
|
10
11
|
import { pipelineData, type PipelineData, type StageNode } from "../builder/pipeline.ts";
|
|
@@ -46,14 +47,43 @@ const USAGE = "usage: step describe [--sentinels] | step run <step> | step map-t
|
|
|
46
47
|
|
|
47
48
|
export function allSteps(data: PipelineData): AnyStep[] {
|
|
48
49
|
const out: AnyStep[] = [];
|
|
49
|
-
const
|
|
50
|
-
|
|
50
|
+
for (const { step } of stepsWithCases(data)) if (!out.includes(step)) out.push(step);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Every step with the `select` case conditions (`<ref>.<discriminator>=<value>`) it sits in. */
|
|
55
|
+
export function stepsWithCases(data: PipelineData): { step: AnyStep; cases: string[] }[] {
|
|
56
|
+
const out: { step: AnyStep; cases: string[] }[] = [];
|
|
57
|
+
const walk = (nodes: readonly StageNode[], cases: string[]) => {
|
|
58
|
+
for (const n of nodes) {
|
|
59
|
+
if (n.kind === "stage") out.push(...n.steps.map((s) => ({ step: s.step, cases })));
|
|
60
|
+
else if (n.kind === "when") walk(n.nodes, cases);
|
|
61
|
+
else {
|
|
62
|
+
const union = isConfigRef(n.on) ? unionAt(n.on) : undefined;
|
|
63
|
+
for (const [value, inner] of Object.entries(n.cases))
|
|
64
|
+
walk(inner, union ? [...cases, `${configRefPath(n.on)}.${union.discriminator}=${value}`] : cases);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
51
67
|
};
|
|
52
|
-
walk(data.nodes);
|
|
53
|
-
for (const fh of data.failureHandlers)
|
|
68
|
+
walk(data.nodes, []);
|
|
69
|
+
for (const fh of data.failureHandlers) out.push({ step: fh.step, cases: [] });
|
|
54
70
|
return out;
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
/** The step `run <name>` executes: the only one so named, or the one in the cases the service selected (`OLVE_CASES`). */
|
|
74
|
+
export function findStep(data: PipelineData, name: string, selected: readonly string[]): AnyStep | string {
|
|
75
|
+
const candidates = stepsWithCases(data).filter((c) => c.step.name === name);
|
|
76
|
+
const distinct = candidates.filter((c, i) => candidates.findIndex((d) => d.step === c.step) === i);
|
|
77
|
+
if (distinct.length === 0) return `unknown step '${name}'`;
|
|
78
|
+
if (distinct.length === 1) return distinct[0]!.step;
|
|
79
|
+
const chosen = distinct.filter((c) => c.cases.every((k) => selected.includes(k)));
|
|
80
|
+
if (chosen.length === 1) return chosen[0]!.step;
|
|
81
|
+
return `step '${name}' is in several select cases; ${ENV_CASES} must name the selected one (got ${JSON.stringify(selected)})`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The select case conditions of the step being run (JSON array), set by the service for steps inside a select. */
|
|
85
|
+
export const ENV_CASES = "OLVE_CASES";
|
|
86
|
+
|
|
57
87
|
/** Installs SIGTERM/SIGINT handlers that abort the returned signal (PID 1 ignores signals without them). */
|
|
58
88
|
export function cancellation(): { signal: AbortSignal; dispose(): void } {
|
|
59
89
|
const ac = new AbortController();
|
|
@@ -77,17 +107,17 @@ export async function resolveStepFunction(step: AnyStep, handlers: HandlerRegist
|
|
|
77
107
|
return step.builtin.run as StepFunction;
|
|
78
108
|
}
|
|
79
109
|
const load = handlers[step.name];
|
|
80
|
-
if (!load) return `step '${step.name}' has no handler
|
|
110
|
+
if (!load) return `step '${step.name}' has no handler`;
|
|
81
111
|
let mod: { default: unknown };
|
|
82
112
|
try {
|
|
83
113
|
mod = await load();
|
|
84
114
|
} catch (e) {
|
|
85
|
-
return `
|
|
115
|
+
return `the handler of step '${step.name}' failed to load: ${(e as Error)?.stack ?? String(e)}`;
|
|
86
116
|
}
|
|
87
117
|
const impl = mod.default;
|
|
88
|
-
if (!isImplementation(impl)) return `
|
|
118
|
+
if (!isImplementation(impl)) return `the handler of step '${step.name}' must default-export implement(<step '${step.name}'>, …)`;
|
|
89
119
|
if (impl.step !== step)
|
|
90
|
-
return `
|
|
120
|
+
return `the handler of step '${step.name}' implements a different step object (it must be implement(<the '${step.name}' step from pipeline.ts>, …))`;
|
|
91
121
|
return impl.handler as unknown as StepFunction;
|
|
92
122
|
}
|
|
93
123
|
|
|
@@ -136,8 +166,8 @@ async function describeMode(cfg: HarnessConfig, args: string[], io: HarnessIO):
|
|
|
136
166
|
async function runMode(cfg: HarnessConfig, name: string | undefined, io: HarnessIO): Promise<number> {
|
|
137
167
|
if (!name) return io.stderr(USAGE), EXIT.usage;
|
|
138
168
|
const data = pipelineData(cfg.pipeline);
|
|
139
|
-
const step =
|
|
140
|
-
if (
|
|
169
|
+
const step = findStep(data, name, selectedCases());
|
|
170
|
+
if (typeof step === "string") return io.stderr(step), EXIT.usage;
|
|
141
171
|
const fn = await resolveStepFunction(step, cfg.handlers);
|
|
142
172
|
if (typeof fn === "string") return io.stderr(fn), EXIT.configInvalid;
|
|
143
173
|
|
|
@@ -230,3 +260,12 @@ export async function runHarness(cfg: HarnessConfig, argv: readonly string[], io
|
|
|
230
260
|
}
|
|
231
261
|
|
|
232
262
|
export { formatIssues, formatLog };
|
|
263
|
+
|
|
264
|
+
function selectedCases(): string[] {
|
|
265
|
+
try {
|
|
266
|
+
const v = JSON.parse(process.env[ENV_CASES] ?? "[]") as unknown;
|
|
267
|
+
return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
package/src/harness/validate.ts
CHANGED
|
@@ -44,13 +44,31 @@ function entrySchema(e: unknown): z.ZodType {
|
|
|
44
44
|
return e as z.ZodType;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Reserved input.json key holding the step's config entries (written by the service). */
|
|
48
|
+
export const CONFIG_KEY = "$config";
|
|
49
|
+
|
|
50
|
+
/** Parses `$config` with each declared entry's schema (defaults applied). Undeclared entries are ignored. */
|
|
51
|
+
export function validateConfig(step: AnyStep, raw: unknown): Validation<Record<string, unknown>> {
|
|
52
|
+
const given = isPlainObject(raw) ? raw[CONFIG_KEY] : undefined;
|
|
53
|
+
if (given !== undefined && !isPlainObject(given)) return { ok: false, issues: [`input.${CONFIG_KEY} must be an object keyed by config entry names`] };
|
|
54
|
+
const issues: string[] = [];
|
|
55
|
+
const out: Record<string, unknown> = {};
|
|
56
|
+
for (const e of step.config ?? []) {
|
|
57
|
+
const value = given?.[e.name];
|
|
58
|
+
const r = e.schema.safeParse(value);
|
|
59
|
+
if (!r.success) issues.push(...formatIssues(r.error, `config.${e.name}`).map((i) => (value === undefined ? `${i} (config entry '${e.name}' is not set)` : i)));
|
|
60
|
+
else if (r.data !== undefined) out[e.name] = r.data;
|
|
61
|
+
}
|
|
62
|
+
return issues.length ? { ok: false, issues } : { ok: true, value: out };
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/** Validates input.json against the step's input entries. Unknown top-level keys are rejected; nested values are parsed (unknown nested keys stripped). */
|
|
48
66
|
export function validateInput(step: AnyStep, raw: unknown): Validation<Record<string, unknown>> {
|
|
49
67
|
if (!isPlainObject(raw)) return { ok: false, issues: ["input.json must be a JSON object keyed by the step's input names"] };
|
|
50
68
|
const issues: string[] = [];
|
|
51
69
|
const out: Record<string, unknown> = {};
|
|
52
70
|
const declared = step.input as Record<string, unknown>;
|
|
53
|
-
for (const k of Object.keys(raw)) if (!Object.hasOwn(declared, k)) issues.push(`input.${k}: not an input of step '${step.name}'`);
|
|
71
|
+
for (const k of Object.keys(raw)) if (k !== CONFIG_KEY && !Object.hasOwn(declared, k)) issues.push(`input.${k}: not an input of step '${step.name}'`);
|
|
54
72
|
for (const [k, e] of Object.entries(declared)) {
|
|
55
73
|
if (!Object.hasOwn(raw, k)) {
|
|
56
74
|
if (isOptional(e)) continue;
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/// <reference path="./assets.d.ts" />
|
|
2
|
-
// @
|
|
2
|
+
// @olivervea/pipelines — authoring API (contracts, handlers, pipeline builder).
|
|
3
3
|
export { z } from "zod";
|
|
4
4
|
export { model, fileRef, optional, type Model, type AnyModel, type Infer, type InferInput, type Plain, type FileRef, type Optional } from "./core/models.ts";
|
|
5
5
|
export { secret, type Secret, type DeclaredSecrets } from "./core/secrets.ts";
|
|
6
|
+
export { config, secretRef, type ConfigEntry, type AnyConfigEntry, type ConfigValues, type ConfigRef, type ConfigNode, type Configurable, type DiscriminatorOf, type VariantOf } from "./core/config.ts";
|
|
6
7
|
export { hmacSha256, githubSignature, bearer, type WebhookVerify, type WebhookEvent } from "./core/webhooks.ts";
|
|
7
|
-
export { step, type Step, type AnyStep, type StepDef, type StepInput, type StepKind, type InputEntry, type InputSpec, type LibraryStep, type OutputValue } from "./core/steps.ts";
|
|
8
|
+
export { step, type Step, type AnyStep, type StepDef, type StepInput, type StepKind, type ConfigOf, type InputEntry, type InputSpec, type LibraryStep, type OutputValue } from "./core/steps.ts";
|
|
8
9
|
export { implement, type Implementation, type Handler, type HandlerResult, type Skipped } from "./core/handlers.ts";
|
|
9
10
|
export type { Ctx, RunInfo, Sh, ShOptions, ExecOptions, Http, HttpInit, RetryOptions, OutputFileOptions } from "./ctx/types.ts";
|
|
10
11
|
export type { Ref, Mappable, MapperResult } from "./builder/refs.ts";
|
|
11
12
|
export { pipeline, Pipeline, type PipelineDef, type InRefs, type InitialAvail, type WhenCond, type PushEvent, type StageEntry, type CheckStage, type MapperFn } from "./builder/pipeline.ts";
|
|
12
|
-
export { steps, Source, BuiltImage, HelmRelease, FailureContext, type RunValue, type ScriptDef, type GitHubSourceDef, type ImageBuildDef, type HelmDeployOverSshDef, httpHealthCheck, type HttpHealthCheckOptions } from "./library/index.ts";
|
|
13
|
+
export { steps, Source, BuiltImage, PushedImage, HelmRelease, FailureContext, type RunValue, type ScriptDef, type GitHubSourceDef, type ImageBuildDef, type ImageBuildTarballDef, type ImageBuildPushDef, type RegistryAuth, type HelmDeployOverSshDef, helmTarget, deployTarget, type ComposeTargetValue, type HelmDeployInClusterDef, type HelmDeploySshTargetDef, type HelmTargetValue, type InClusterTargetValue, type SshTargetValue, registryTag, registryDigest, type RegistryAuthConfig, type RegistryAccess, npmDistTag, npmHasVersion, httpHealthCheck, type HttpHealthCheckOptions, sshSession, shq, type SshSession, serviceUrl, checkSpa } from "./library/index.ts";
|