@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.
- package/README.md +165 -78
- package/manifest.schema.json +300 -16
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +98 -64
- package/src/build/cli.ts +33 -23
- package/src/build/elf.ts +16 -7
- package/src/build/hashes.ts +40 -51
- package/src/build/project.ts +55 -52
- package/src/builder/pipeline.ts +348 -89
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +6 -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 +62 -15
- package/src/core/matrix.ts +150 -0
- package/src/core/models.ts +37 -15
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +113 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +119 -42
- package/src/ctx/index.ts +110 -39
- package/src/ctx/links.ts +30 -0
- package/src/ctx/sh.ts +106 -44
- package/src/ctx/types.ts +24 -4
- package/src/describe/config-schema.ts +98 -0
- package/src/describe/context.ts +187 -0
- package/src/describe/describe.ts +63 -355
- package/src/describe/failure-handlers.ts +34 -0
- package/src/describe/manifest-schema.ts +304 -61
- package/src/describe/mapper-source.ts +252 -105
- package/src/describe/record.ts +67 -37
- package/src/describe/schemas.ts +71 -0
- package/src/describe/stages.ts +166 -0
- package/src/describe/step.ts +544 -0
- package/src/describe/triggers.ts +72 -0
- package/src/dev/cli.ts +62 -35
- package/src/harness/env.ts +47 -25
- package/src/harness/execute.ts +74 -30
- package/src/harness/exit.ts +1 -1
- package/src/harness/index.ts +19 -8
- package/src/harness/main.ts +245 -99
- package/src/harness/mask.ts +98 -0
- package/src/harness/result.ts +45 -24
- package/src/harness/validate.ts +82 -15
- package/src/index.ts +139 -10
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +115 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +426 -112
- package/src/library/helm-deploy.ts +529 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +510 -162
- package/src/library/index.ts +48 -14
- package/src/library/models.ts +35 -14
- package/src/library/ssh.ts +44 -18
- package/src/testing/index.ts +100 -35
- package/src/util/canonical.ts +7 -4
- package/src/util/code-hash.ts +17 -0
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +83 -38
- package/src/library/script.ts +0 -85
- 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
|
+
}
|
package/src/core/models.ts
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import type { ModelBrand } from
|
|
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:
|
|
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[
|
|
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[
|
|
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[
|
|
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>(
|
|
23
|
-
|
|
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
|
|
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().
|
|
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<
|
|
49
|
-
.meta({
|
|
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:
|
|
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:
|
|
82
|
+
return Object.freeze({ kind: 'optional' as const, model });
|
|
66
83
|
}
|
|
67
84
|
export function isOptional(v: unknown): v is Optional {
|
|
68
|
-
return
|
|
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
|
}
|
package/src/core/secrets.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
2
|
-
import { STEP, type StepBrand } from
|
|
3
|
-
import type { AnyModel, Infer, Optional } from
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
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
|
-
/**
|
|
8
|
-
|
|
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>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
/**
|
|
20
|
-
|
|
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
|
-
/**
|
|
39
|
-
readonly
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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 &
|
|
89
|
+
export type LiteralName<N extends string> = N &
|
|
90
|
+
(string extends N ? '❌ step name must be a string literal' : unknown);
|
|
58
91
|
|
|
59
|
-
|
|
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[] = [
|
|
94
|
+
export const RESERVED_STEP_NAMES: readonly string[] = ['input'];
|
|
63
95
|
|
|
64
|
-
export interface StepDef<
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|
161
|
+
return (
|
|
162
|
+
typeof v === 'object' && v !== null && (v as Record<symbol, unknown>)[STEP] === true
|
|
163
|
+
);
|
|
97
164
|
}
|
|
98
165
|
|
|
99
|
-
/**
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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;
|
package/src/core/webhooks.ts
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
|
-
import type { Secret } from
|
|
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
|
-
| {
|
|
6
|
-
|
|
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: {
|
|
10
|
-
|
|
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:
|
|
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:
|
|
34
|
+
return Object.freeze({ scheme: 'bearer', secret });
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
/**
|