@olivervea/pipelines 2.0.0-beta.3 → 2.0.0-beta.31

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 (65) hide show
  1. package/README.md +165 -78
  2. package/manifest.schema.json +300 -16
  3. package/package.json +6 -4
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +98 -64
  6. package/src/build/cli.ts +33 -23
  7. package/src/build/elf.ts +16 -7
  8. package/src/build/hashes.ts +40 -51
  9. package/src/build/project.ts +55 -52
  10. package/src/builder/pipeline.ts +348 -89
  11. package/src/builder/refs.ts +15 -5
  12. package/src/core/brands.ts +6 -5
  13. package/src/core/config-refs.ts +103 -0
  14. package/src/core/config.ts +224 -0
  15. package/src/core/external.ts +86 -0
  16. package/src/core/handlers.ts +62 -15
  17. package/src/core/matrix.ts +150 -0
  18. package/src/core/models.ts +37 -15
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +113 -38
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +119 -42
  24. package/src/ctx/index.ts +110 -39
  25. package/src/ctx/links.ts +30 -0
  26. package/src/ctx/sh.ts +106 -44
  27. package/src/ctx/types.ts +24 -4
  28. package/src/describe/config-schema.ts +98 -0
  29. package/src/describe/context.ts +187 -0
  30. package/src/describe/describe.ts +63 -355
  31. package/src/describe/failure-handlers.ts +34 -0
  32. package/src/describe/manifest-schema.ts +304 -61
  33. package/src/describe/mapper-source.ts +252 -105
  34. package/src/describe/record.ts +67 -37
  35. package/src/describe/schemas.ts +71 -0
  36. package/src/describe/stages.ts +166 -0
  37. package/src/describe/step.ts +544 -0
  38. package/src/describe/triggers.ts +72 -0
  39. package/src/dev/cli.ts +62 -35
  40. package/src/harness/env.ts +47 -25
  41. package/src/harness/execute.ts +74 -30
  42. package/src/harness/exit.ts +1 -1
  43. package/src/harness/index.ts +19 -8
  44. package/src/harness/main.ts +245 -99
  45. package/src/harness/mask.ts +98 -0
  46. package/src/harness/result.ts +45 -24
  47. package/src/harness/validate.ts +82 -15
  48. package/src/index.ts +139 -10
  49. package/src/library/checks.ts +11 -6
  50. package/src/library/compose-deploy.ts +115 -0
  51. package/src/library/github-source.ts +100 -47
  52. package/src/library/helm-deploy-ssh.ts +426 -112
  53. package/src/library/helm-deploy.ts +529 -0
  54. package/src/library/http-health.ts +7 -5
  55. package/src/library/image-build.ts +510 -162
  56. package/src/library/index.ts +48 -14
  57. package/src/library/models.ts +35 -14
  58. package/src/library/ssh.ts +44 -18
  59. package/src/testing/index.ts +100 -35
  60. package/src/util/canonical.ts +7 -4
  61. package/src/util/code-hash.ts +17 -0
  62. package/src/util/hash.ts +5 -5
  63. package/src/util/tar.ts +83 -38
  64. package/src/library/script.ts +0 -85
  65. package/src/util/handler-hash.ts +0 -22
@@ -0,0 +1,150 @@
1
+ import { isModel, type AnyModel, type Infer } from './models.ts';
2
+ import type { Simplify } from './types.ts';
3
+
4
+ /** Axis name → its values, in order. Object key order is the axis order. */
5
+ export type Axes = { readonly [axis: string]: readonly string[] };
6
+ /** One combination of axis values. */
7
+ export type Cell = Readonly<Record<string, string>>;
8
+ /** The cell type of a step whose axes are `Ax`. */
9
+ export type CellOfAxes<Ax extends Axes> = Simplify<{
10
+ readonly [K in keyof Ax as string extends K ? never : K]: Ax[K][number];
11
+ }>;
12
+
13
+ /** What the step's other cells do when one fails. */
14
+ export type FailureStrategy =
15
+ { readonly kind: 'keepRunning' } | { readonly kind: 'cascadeFailure' };
16
+
17
+ /** Every cell runs to the end (the default). */
18
+ export function keepRunning(): FailureStrategy {
19
+ return Object.freeze({ kind: 'keepRunning' as const });
20
+ }
21
+ /** The first failed cell cancels the cells still running or waiting. */
22
+ export function cascadeFailure(): FailureStrategy {
23
+ return Object.freeze({ kind: 'cascadeFailure' as const });
24
+ }
25
+
26
+ export interface MatrixDef<Ax extends Axes> {
27
+ /** The cells are the cross product of the axes, first axis outermost. */
28
+ axes: Ax;
29
+ /** Each entry names axes with one of their values; a cell matching all of them is left out. */
30
+ exclude?: readonly NoInfer<Partial<CellOfAxes<Ax>>>[];
31
+ /** Default `keepRunning()`. */
32
+ failure?: FailureStrategy;
33
+ /** At most this many of the step's cells running at once in a run. */
34
+ maxParallel?: number;
35
+ }
36
+
37
+ /** A matrix step's matrix, as the step carries it. */
38
+ export interface Matrix<Ax extends Axes = Axes> {
39
+ readonly axes: Ax;
40
+ readonly exclude: readonly Cell[];
41
+ readonly failure: FailureStrategy;
42
+ readonly maxParallel?: number;
43
+ }
44
+
45
+ export const AXIS_NAME = /^[a-z][a-zA-Z0-9]{0,30}$/;
46
+ export const AXIS_VALUE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
47
+ export const MAX_AXES = 4;
48
+ export const MAX_CELLS = 64;
49
+
50
+ export function makeMatrix(def: MatrixDef<Axes>): Matrix {
51
+ return Object.freeze({
52
+ axes: Object.freeze(
53
+ Object.fromEntries(
54
+ Object.entries(def.axes ?? {}).map(([k, v]) => [
55
+ k,
56
+ Object.freeze(Array.isArray(v) ? [...v] : v),
57
+ ]),
58
+ ),
59
+ ),
60
+ exclude: Object.freeze((def.exclude ?? []).map((e) => Object.freeze({ ...e }))),
61
+ failure: def.failure ?? keepRunning(),
62
+ ...(def.maxParallel !== undefined ? { maxParallel: def.maxParallel } : {}),
63
+ }) as Matrix;
64
+ }
65
+
66
+ const matches = (cell: Cell, x: Cell) =>
67
+ Object.entries(x).every(([k, v]) => cell[k] === v);
68
+
69
+ /** The cells in order: the cross product in axis order (first axis outermost) minus the excludes. */
70
+ export function matrixCells(m: Matrix): Cell[] {
71
+ let cells: Record<string, string>[] = [{}];
72
+ for (const [axis, values] of Object.entries(m.axes))
73
+ cells = cells.flatMap((c) =>
74
+ (Array.isArray(values) ? values : []).map((v) => ({ ...c, [axis]: v })),
75
+ );
76
+ return cells.filter((c) => !m.exclude.some((x) => matches(c, x)));
77
+ }
78
+
79
+ /** `build-cli[os=linux,arch=x64]`: the name a cell's attempts, files and address use. */
80
+ export function cellName(step: string, cell: Cell): string {
81
+ return `${step}[${Object.entries(cell)
82
+ .map(([k, v]) => `${k}=${v}`)
83
+ .join(',')}]`;
84
+ }
85
+
86
+ /** The cell `raw` (JSON text, as in `OLVE_CELL`) names, in axis order, or what is wrong. */
87
+ export function parseCell(
88
+ step: string,
89
+ m: Matrix,
90
+ raw: string | undefined,
91
+ ): { ok: true; cell: Cell } | { ok: false; message: string } {
92
+ const axes = Object.keys(m.axes);
93
+ const example = JSON.stringify(matrixCells(m)[0] ?? {});
94
+ if (raw === undefined || raw === '')
95
+ return {
96
+ ok: false,
97
+ message: `step '${step}' is a matrix step: OLVE_CELL must name the cell to run, e.g. ${example}`,
98
+ };
99
+ let v: unknown;
100
+ try {
101
+ v = JSON.parse(raw);
102
+ } catch (e) {
103
+ return { ok: false, message: `OLVE_CELL is not JSON: ${(e as Error).message}` };
104
+ }
105
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
106
+ return {
107
+ ok: false,
108
+ message: `OLVE_CELL must be a JSON object of axis values, e.g. ${example}`,
109
+ };
110
+ const given = v as Record<string, unknown>;
111
+ const ordered: Record<string, string> = {};
112
+ for (const a of axes) {
113
+ if (typeof given[a] !== 'string') break;
114
+ ordered[a] = given[a] as string;
115
+ }
116
+ const exact =
117
+ Object.keys(given).length === axes.length &&
118
+ Object.keys(ordered).length === axes.length;
119
+ const all = matrixCells(m);
120
+ if (exact && all.some((c) => cellName(step, c) === cellName(step, ordered)))
121
+ return { ok: true, cell: Object.freeze(ordered) };
122
+ return {
123
+ ok: false,
124
+ message: `OLVE_CELL ${raw} is not a cell of step '${step}' (cells: ${all.map((c) => cellName(step, c)).join(', ')})`,
125
+ };
126
+ }
127
+
128
+ // ─── cells(): reading a matrix step ──────────────────────────────────────────────────────────
129
+
130
+ /** A step input that reads every cell of a matrix step whose output is `model`. */
131
+ export interface Cells<M extends AnyModel = AnyModel> {
132
+ readonly kind: 'cells';
133
+ readonly model: M;
134
+ }
135
+ export function cells<M extends AnyModel>(model: M): Cells<M> {
136
+ return Object.freeze({ kind: 'cells' as const, model });
137
+ }
138
+ export function isCells(v: unknown): v is Cells {
139
+ return (
140
+ typeof v === 'object' &&
141
+ v !== null &&
142
+ (v as { kind?: unknown }).kind === 'cells' &&
143
+ isModel((v as { model?: unknown }).model)
144
+ );
145
+ }
146
+ /** One entry of a `cells` input, in cell order. */
147
+ export interface CellOutput<M extends AnyModel, C extends Cell = Cell> {
148
+ readonly cell: C;
149
+ readonly output: Infer<M>;
150
+ }
@@ -1,30 +1,40 @@
1
- import { z } from "zod";
2
- import type { ModelBrand } from "./brands.ts";
1
+ import { z } from 'zod';
2
+ import type { ModelBrand } from './brands.ts';
3
3
 
4
4
  /** A named, nominal zod schema. The name is its identity in the manifest (one schema per name). */
5
5
  export interface Model<N extends string = string, S extends z.ZodType = z.ZodType> {
6
- readonly kind: "model";
6
+ readonly kind: 'model';
7
7
  readonly name: N;
8
8
  readonly schema: S;
9
9
  }
10
10
  export type AnyModel = Model<string, z.ZodType>;
11
11
 
12
12
  /** Output view of a model (`z.output`), nominally branded with the model name. The brand is optional, so plain literals still assign. */
13
- export type Infer<M extends AnyModel> = z.output<M["schema"]> & { readonly [ModelBrand]?: M["name"] };
13
+ export type Infer<M extends AnyModel> = z.output<M['schema']> & {
14
+ readonly [ModelBrand]?: M['name'];
15
+ };
14
16
  /** Input view of a model (`z.input`: fields with defaults are optional). What trigger mappers produce. */
15
- export type InferInput<M extends AnyModel> = z.input<M["schema"]>;
17
+ export type InferInput<M extends AnyModel> = z.input<M['schema']>;
16
18
  /** Output view without the nominal brand. */
17
- export type Plain<M extends AnyModel> = z.output<M["schema"]>;
19
+ export type Plain<M extends AnyModel> = z.output<M['schema']>;
18
20
 
19
21
  /** Model names: identifier-like, so they are safe as JSON keys, C# names and URL segments. */
20
22
  export const MODEL_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
21
23
 
22
- export function model<const N extends string, S extends z.ZodType>(name: N, schema: S): Model<N, S> {
23
- return Object.freeze({ kind: "model" as const, name, schema });
24
+ export function model<const N extends string, S extends z.ZodType>(
25
+ name: N,
26
+ schema: S,
27
+ ): Model<N, S> {
28
+ return Object.freeze({ kind: 'model' as const, name, schema });
24
29
  }
25
30
 
26
31
  export function isModel(v: unknown): v is AnyModel {
27
- return typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "model" && (v as { schema?: unknown }).schema instanceof z.ZodType;
32
+ return (
33
+ typeof v === 'object' &&
34
+ v !== null &&
35
+ (v as { kind?: unknown }).kind === 'model' &&
36
+ (v as { schema?: unknown }).schema instanceof z.ZodType
37
+ );
28
38
  }
29
39
 
30
40
  // ─── File references ─────────────────────────────────────────────────────────────────────────
@@ -36,17 +46,24 @@ export function isModel(v: unknown): v is AnyModel {
36
46
  const SEGMENT = String.raw`(?!\.\.?(?:/|$))[^/\\\x00]+`;
37
47
  export const FILE_PATH_PATTERN = `^(?:${SEGMENT}/)*${SEGMENT}/?$`;
38
48
  export const FILE_PATH = new RegExp(FILE_PATH_PATTERN);
49
+ /** Step names: lower-case kebab, ≤ 63 chars (usable as K8s label values, URL segments and storage paths). */
50
+ export const STEP_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
51
+ const CELL_PART = String.raw`[a-z][a-zA-Z0-9]{0,30}=[A-Za-z0-9][A-Za-z0-9._-]{0,62}`;
52
+ /** A step name, or a matrix cell's name (`build-cli[os=linux,arch=x64]`): what a file reference's `step` holds. */
53
+ export const ATTEMPT_STEP = new RegExp(
54
+ String.raw`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\[${CELL_PART}(?:,${CELL_PART})*\])?$`,
55
+ );
39
56
 
40
57
  const FileRefSchema = z
41
58
  .object({
42
59
  run: z.number().int().nonnegative(),
43
- step: z.string().min(1),
60
+ step: z.string().regex(ATTEMPT_STEP),
44
61
  attempt: z.number().int().positive(),
45
62
  path: z.string().regex(FILE_PATH),
46
63
  sha256: z.string().regex(/^[0-9a-f]{64}$/),
47
64
  })
48
- .brand<"FileRef">()
49
- .meta({ "x-olve-kind": "fileRef" });
65
+ .brand<'FileRef'>()
66
+ .meta({ 'x-olve-kind': 'fileRef' });
50
67
 
51
68
  /** A file or directory in some step attempt's output (`runs/<p>/<run>/<step>/<attempt>/files/<path>`). Branded: only `ctx.output.*` or a mapper ref produce one. */
52
69
  export type FileRef = z.output<typeof FileRefSchema>;
@@ -58,12 +75,17 @@ export const fileRefSchema = FileRefSchema;
58
75
 
59
76
  /** A step input that may be missing because its producer was *maybe skipped* (inside a `when`). There must still be a producer. */
60
77
  export interface Optional<M extends AnyModel = AnyModel> {
61
- readonly kind: "optional";
78
+ readonly kind: 'optional';
62
79
  readonly model: M;
63
80
  }
64
81
  export function optional<M extends AnyModel>(model: M): Optional<M> {
65
- return Object.freeze({ kind: "optional" as const, model });
82
+ return Object.freeze({ kind: 'optional' as const, model });
66
83
  }
67
84
  export function isOptional(v: unknown): v is Optional {
68
- return typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "optional" && isModel((v as { model?: unknown }).model);
85
+ return (
86
+ typeof v === 'object' &&
87
+ v !== null &&
88
+ (v as { kind?: unknown }).kind === 'optional' &&
89
+ isModel((v as { model?: unknown }).model)
90
+ );
69
91
  }
@@ -1,6 +1,6 @@
1
1
  /** A named pipeline secret. Steps (and poll triggers) declare the secrets they use; only those are mounted. */
2
2
  export interface Secret<N extends string = string> {
3
- readonly kind: "secret";
3
+ readonly kind: 'secret';
4
4
  readonly name: N;
5
5
  readonly description: string;
6
6
  }
@@ -8,6 +8,6 @@ export interface Secret<N extends string = string> {
8
8
  export const SECRET_NAME = /^[A-Z_][A-Z0-9_]*$/;
9
9
 
10
10
  export function secret<const N extends string>(name: N, description: string): Secret<N> {
11
- return Object.freeze({ kind: "secret" as const, name, description });
11
+ return Object.freeze({ kind: 'secret' as const, name, description });
12
12
  }
13
13
  export type DeclaredSecrets<N extends string> = { readonly [K in N]: string };
package/src/core/steps.ts CHANGED
@@ -1,29 +1,55 @@
1
- import type { z } from "zod";
2
- import { STEP, type StepBrand } from "./brands.ts";
3
- import type { AnyModel, Infer, Optional } from "./models.ts";
4
- import type { Secret } from "./secrets.ts";
5
- import type { Simplify } from "./types.ts";
1
+ import type { z } from 'zod';
2
+ import { STEP, type StepBrand } from './brands.ts';
3
+ import type { AnyModel, Infer, Optional } from './models.ts';
4
+ import type { External } from './external.ts';
5
+ import type { AnyConfigEntry } from './config.ts';
6
+ import { configRefsIn } from './config-refs.ts';
7
+ import type { Secret } from './secrets.ts';
8
+ import type { Simplify } from './types.ts';
9
+ import {
10
+ makeMatrix,
11
+ type Axes,
12
+ type CellOfAxes,
13
+ type CellOutput,
14
+ type Cells,
15
+ type Matrix,
16
+ type MatrixDef,
17
+ } from './matrix.ts';
6
18
 
7
- /** A step input entry: a model (matchable by model), `optional(model)`, or a plain zod schema (mapper required). */
8
- export type InputEntry = AnyModel | Optional | z.ZodType;
19
+ /**
20
+ * A step input entry: a model (matchable by model), `optional(model)`, `cells(model)` (every cell of a
21
+ * matrix step), a plain zod schema (mapper required), or `external(model, …)`, answered from outside the run.
22
+ */
23
+ export type InputEntry = AnyModel | Optional | Cells | External | z.ZodType;
9
24
  export type InputSpec = { readonly [key: string]: InputEntry };
10
25
 
11
26
  export type InferEntry<E> =
12
- E extends Optional<infer M> ? Infer<M> | undefined
13
- : E extends AnyModel ? Infer<E>
14
- : E extends z.ZodType ? z.output<E>
15
- : never;
27
+ E extends Optional<infer M>
28
+ ? Infer<M> | undefined
29
+ : E extends Cells<infer M>
30
+ ? CellOutput<M>[]
31
+ : E extends External<infer M>
32
+ ? Infer<M>
33
+ : E extends AnyModel
34
+ ? Infer<E>
35
+ : E extends z.ZodType
36
+ ? z.output<E>
37
+ : never;
16
38
  /** What a handler receives: the output view of every input entry. */
17
39
  export type StepInput<I> = Simplify<{ -readonly [K in keyof I]: InferEntry<I[K]> }>;
18
40
 
19
- /** `custom` steps have a handler file; the others are library steps implemented by the SDK. */
20
- export type StepKind = "custom" | "script" | "gitHubSource" | "imageBuild" | "helmDeployOverSsh";
41
+ /**
42
+ * A label for what a step is, shown in the UI. `custom` is the default; a reusable step names its
43
+ * kind, e.g. `gitHubSource`. Packages prefix theirs (`acme/deploy`).
44
+ */
45
+ export type StepKind = string;
21
46
 
22
47
  export interface Step<
23
48
  N extends string = string,
24
49
  I extends InputSpec = InputSpec,
25
50
  O extends AnyModel | null = AnyModel | null,
26
51
  Sec extends string = string,
52
+ Cfg extends AnyConfigEntry = never,
27
53
  > {
28
54
  readonly [StepBrand]: true;
29
55
  readonly kind: StepKind;
@@ -33,35 +59,48 @@ export interface Step<
33
59
  readonly input: I;
34
60
  readonly output: O;
35
61
  readonly secrets: readonly Secret<Sec>[];
62
+ /** Config entries the step receives as `ctx.config`. */
63
+ readonly config: readonly Cfg[];
36
64
  readonly timeoutSeconds?: number;
37
65
  readonly retries?: number;
38
- /** Library steps carry their own implementation and config. */
39
- readonly builtin?: LibraryImplementation;
40
- }
41
- /** Runtime part of a library step (not part of the public contract). */
42
- export interface LibraryImplementation {
43
- /** Config as emitted in the manifest (JSON only; must not contain paths). */
44
- readonly manifestConfig: () => Record<string, unknown>;
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- readonly run: (input: any, ctx: any) => Promise<unknown>;
66
+ /** The step pod gets the instance's step ServiceAccount token (`Kubernetes:StepServiceAccount`). */
67
+ readonly clusterAccess?: boolean;
68
+ /** How the step is set up, as written in the definition (may hold config references). */
69
+ readonly settings?: Readonly<Record<string, unknown>>;
70
+ /** Set on a matrix step: it runs once per cell. */
71
+ readonly matrix?: Matrix;
47
72
  }
48
- export type LibraryStep = { readonly builtin: LibraryImplementation };
49
73
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
- export type AnyStep = Step<string, any, AnyModel | null, any>;
74
+ export type AnyStep = Step<string, any, AnyModel | null, any, any>;
51
75
 
52
76
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
- export type SecretsOf<S> = S extends Step<any, any, any, infer Sec> ? Sec : never;
54
- export type OutputValue<S extends AnyStep> = S["output"] extends AnyModel ? Infer<S["output"]> : void;
77
+ export type SecretsOf<S> = S extends Step<any, any, any, infer Sec, any> ? Sec : never;
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ export type ConfigOf<S> = S extends Step<any, any, any, any, infer C> ? C : never;
80
+ /** The cell type of a matrix step (`never` for any other step). */
81
+ export type CellOf<S> = S extends { readonly matrix: Matrix<infer Ax> }
82
+ ? CellOfAxes<Ax>
83
+ : never;
84
+ export type OutputValue<S extends AnyStep> = S['output'] extends AnyModel
85
+ ? Infer<S['output']>
86
+ : void;
55
87
 
56
88
  /** Step names must be literals (identity, duplicate detection and mapper keys depend on it). */
57
- export type LiteralName<N extends string> = N & (string extends N ? "❌ step name must be a string literal" : unknown);
89
+ export type LiteralName<N extends string> = N &
90
+ (string extends N ? '❌ step name must be a string literal' : unknown);
58
91
 
59
- /** Step names: lower-case kebab, ≤ 63 chars (usable as K8s label values and URL segments). */
60
- export const STEP_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
92
+ export { STEP_NAME } from './models.ts';
61
93
  /** Names that cannot be steps: `input` is the pipeline input's root in mappings. */
62
- export const RESERVED_STEP_NAMES: readonly string[] = ["input"];
94
+ export const RESERVED_STEP_NAMES: readonly string[] = ['input'];
63
95
 
64
- export interface StepDef<N extends string, I extends InputSpec, O extends AnyModel | null, Sec extends readonly Secret[]> {
96
+ export interface StepDef<
97
+ N extends string,
98
+ I extends InputSpec,
99
+ O extends AnyModel | null,
100
+ Sec extends readonly Secret[],
101
+ Cfg extends readonly AnyConfigEntry[] = [],
102
+ Ax extends Axes = never,
103
+ > {
65
104
  name: LiteralName<N>;
66
105
  /** What the step does, shown in the UI. Required; may be empty. */
67
106
  description: string;
@@ -70,38 +109,74 @@ export interface StepDef<N extends string, I extends InputSpec, O extends AnyMod
70
109
  input?: I;
71
110
  output?: O;
72
111
  secrets?: Sec;
112
+ /** Config entries delivered as `ctx.config` (typed to exactly these). */
113
+ config?: Cfg;
73
114
  timeoutSeconds?: number;
74
115
  /** Automatic retries (an idempotency claim). Default 0. */
75
116
  retries?: number;
117
+ /**
118
+ * Ask for a Kubernetes API token in the step pod: the instance's step ServiceAccount
119
+ * (`Kubernetes:StepServiceAccount`), if it grants one. Default false (no token).
120
+ */
121
+ clusterAccess?: boolean;
122
+ /** A label for the step's kind (default `custom`). */
123
+ kind?: StepKind;
124
+ /**
125
+ * How the step is set up, shown in the manifest and part of the step's identity: the parameters of
126
+ * a reusable step. JSON values and config references (`target.prod.host`); every config entry it
127
+ * references is delivered to the step, and `resolveConfigRefs(value, ctx.config)` resolves them.
128
+ */
129
+ settings?: Readonly<Record<string, unknown>>;
130
+ /** Run the step once per cell of these axes; the handler reads its cell from `ctx.cell`. */
131
+ matrix?: MatrixDef<Ax>;
76
132
  }
77
133
 
78
- export function makeStep(kind: StepKind, def: Record<string, unknown>, builtin?: LibraryImplementation): AnyStep {
134
+ function makeStep(def: Record<string, unknown>): AnyStep {
135
+ const config = new Map<string, AnyConfigEntry>();
136
+ for (const e of (def.config as readonly AnyConfigEntry[] | undefined) ?? [])
137
+ if (!config.has(e.name)) config.set(e.name, e);
138
+ configRefsIn(def.settings, config);
79
139
  const s = {
80
140
  [STEP]: true,
81
- kind,
141
+ kind: def.kind ?? 'custom',
82
142
  name: def.name,
83
143
  description: def.description,
84
144
  image: def.image,
85
145
  input: Object.freeze({ ...((def.input as object | undefined) ?? {}) }),
86
146
  output: def.output ?? null,
87
147
  secrets: Object.freeze([...((def.secrets as readonly Secret[] | undefined) ?? [])]),
148
+ config: Object.freeze([...config.values()]),
88
149
  ...(def.timeoutSeconds !== undefined ? { timeoutSeconds: def.timeoutSeconds } : {}),
89
150
  ...(def.retries !== undefined ? { retries: def.retries } : {}),
90
- ...(builtin ? { builtin } : {}),
151
+ ...(def.clusterAccess === true ? { clusterAccess: true } : {}),
152
+ ...(def.settings !== undefined ? { settings: def.settings } : {}),
153
+ ...(def.matrix !== undefined
154
+ ? { matrix: makeMatrix(def.matrix as MatrixDef<Axes>) }
155
+ : {}),
91
156
  };
92
157
  return Object.freeze(s) as unknown as AnyStep;
93
158
  }
94
159
 
95
160
  export function isStep(v: unknown): v is AnyStep {
96
- return typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[STEP] === true;
161
+ return (
162
+ typeof v === 'object' && v !== null && (v as Record<symbol, unknown>)[STEP] === true
163
+ );
97
164
  }
98
165
 
99
- /** Define a custom step: a contract (name, image, inputs, output, secrets). Its handler lives in `handlers/<name>.ts`. */
166
+ /**
167
+ * Define a step: a contract (name, image, inputs, output, secrets). `implement(step, fn)` gives it
168
+ * its handler; that is what a pipeline stages.
169
+ */
100
170
  export function step<
101
171
  const N extends string,
102
172
  const I extends InputSpec = {},
103
173
  O extends AnyModel | null = null,
104
174
  const Sec extends readonly Secret[] = [],
105
- >(def: StepDef<N, I, O, Sec>): Step<N, I, O, Sec[number]["name"]> {
106
- return makeStep("custom", def as never) as never;
175
+ const Cfg extends readonly AnyConfigEntry[] = [],
176
+ const Ax extends Axes = never,
177
+ >(
178
+ def: StepDef<N, I, O, Sec, Cfg, Ax>,
179
+ ): Step<N, I, O, Sec[number]['name'], Cfg[number]> &
180
+ ([Ax] extends [never] ? unknown : { readonly matrix: Matrix<Ax> }) {
181
+ return makeStep(def as never) as never;
107
182
  }
package/src/core/types.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export type Primitive = string | number | boolean | bigint | null | undefined;
2
2
  export type Simplify<T> = { [K in keyof T]: T[K] } & {};
3
3
  export type Awaitable<T> = T | Promise<T>;
4
- export type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
4
+ export type IsUnion<T, U = T> = T extends unknown
5
+ ? [U] extends [T]
6
+ ? false
7
+ : true
8
+ : never;
5
9
  /** `Ok` when there is no error, else the error (a readable string-literal type). */
6
10
  export type OrError<Err, Ok> = [Err] extends [never] ? Ok : Err;
@@ -1,23 +1,37 @@
1
- import type { Secret } from "./secrets.ts";
1
+ import type { Secret } from './secrets.ts';
2
2
 
3
3
  /** How a webhook trigger authenticates deliveries. Build one with `hmacSha256`, `githubSignature` or `bearer`. */
4
4
  export type WebhookVerify =
5
- | { readonly scheme: "hmac-sha256"; readonly secret: Secret; readonly header: string; readonly prefix: string }
6
- | { readonly scheme: "bearer"; readonly secret: Secret };
5
+ | {
6
+ readonly scheme: 'hmac-sha256';
7
+ readonly secret: Secret;
8
+ readonly header: string;
9
+ readonly prefix: string;
10
+ }
11
+ | { readonly scheme: 'bearer'; readonly secret: Secret };
7
12
 
8
13
  /** HMAC-SHA256 of the raw body keyed by `secret`, hex, in `header` after `prefix`. */
9
- export function hmacSha256(o: { secret: Secret; header: string; prefix?: string }): WebhookVerify {
10
- return Object.freeze({ scheme: "hmac-sha256", secret: o.secret, header: o.header, prefix: o.prefix ?? "" });
14
+ export function hmacSha256(o: {
15
+ secret: Secret;
16
+ header: string;
17
+ prefix?: string;
18
+ }): WebhookVerify {
19
+ return Object.freeze({
20
+ scheme: 'hmac-sha256',
21
+ secret: o.secret,
22
+ header: o.header,
23
+ prefix: o.prefix ?? '',
24
+ });
11
25
  }
12
26
 
13
27
  /** GitHub's delivery signature: `X-Hub-Signature-256: sha256=<hex>`. */
14
28
  export function githubSignature(secret: Secret): WebhookVerify {
15
- return hmacSha256({ secret, header: "X-Hub-Signature-256", prefix: "sha256=" });
29
+ return hmacSha256({ secret, header: 'X-Hub-Signature-256', prefix: 'sha256=' });
16
30
  }
17
31
 
18
32
  /** `Authorization: Bearer <secret>`. */
19
33
  export function bearer(secret: Secret): WebhookVerify {
20
- return Object.freeze({ scheme: "bearer", secret });
34
+ return Object.freeze({ scheme: 'bearer', secret });
21
35
  }
22
36
 
23
37
  /**