@olivervea/pipelines 2.0.0-beta.18 → 2.0.0-beta.19
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/package.json +4 -2
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +111 -43
- package/src/build/cli.ts +28 -21
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +61 -28
- package/src/build/project.ts +69 -30
- package/src/builder/pipeline.ts +274 -102
- package/src/builder/refs.ts +12 -6
- package/src/core/brands.ts +6 -6
- package/src/core/config-refs.ts +46 -20
- package/src/core/config.ts +110 -33
- package/src/core/external.ts +30 -12
- package/src/core/handlers.ts +54 -14
- package/src/core/models.ts +29 -14
- package/src/core/origin.ts +7 -4
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +39 -23
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +75 -34
- package/src/ctx/sh.ts +104 -44
- package/src/ctx/types.ts +4 -4
- package/src/describe/describe.ts +550 -196
- package/src/describe/manifest-schema.ts +210 -80
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +64 -34
- package/src/harness/env.ts +33 -27
- package/src/harness/execute.ts +55 -29
- package/src/harness/index.ts +21 -8
- package/src/harness/main.ts +197 -74
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +62 -19
- package/src/index.ts +123 -14
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +40 -23
- package/src/library/github-source.ts +69 -39
- package/src/library/helm-deploy-ssh.ts +213 -76
- package/src/library/helm-deploy.ts +263 -85
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +293 -117
- package/src/library/index.ts +47 -15
- package/src/library/models.ts +17 -9
- package/src/library/ssh.ts +31 -14
- package/src/testing/index.ts +63 -24
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +15 -5
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +74 -37
package/src/builder/pipeline.ts
CHANGED
|
@@ -1,76 +1,113 @@
|
|
|
1
|
-
import { PIPELINE } from
|
|
2
|
-
import type { AnyModel, Infer, InferInput, Plain } from
|
|
3
|
-
import type { Secret } from
|
|
4
|
-
import type { AnyStep, OutputValue, Step } from
|
|
5
|
-
import { isStep } from
|
|
6
|
-
import type { WebhookEvent, WebhookVerify } from
|
|
7
|
-
import type { IsUnion, OrError, Simplify } from
|
|
8
|
-
import type { Optional } from
|
|
9
|
-
import type { External } from
|
|
10
|
-
import type { FailureContext } from
|
|
11
|
-
import type { MapperResult, Ref } from
|
|
12
|
-
import {
|
|
1
|
+
import { PIPELINE } from '../core/brands.ts';
|
|
2
|
+
import type { AnyModel, Infer, InferInput, Plain } from '../core/models.ts';
|
|
3
|
+
import type { Secret } from '../core/secrets.ts';
|
|
4
|
+
import type { AnyStep, OutputValue, Step } from '../core/steps.ts';
|
|
5
|
+
import { isStep } from '../core/steps.ts';
|
|
6
|
+
import type { WebhookEvent, WebhookVerify } from '../core/webhooks.ts';
|
|
7
|
+
import type { IsUnion, OrError, Simplify } from '../core/types.ts';
|
|
8
|
+
import type { Optional } from '../core/models.ts';
|
|
9
|
+
import type { External } from '../core/external.ts';
|
|
10
|
+
import type { FailureContext } from '../library/models.ts';
|
|
11
|
+
import type { MapperResult, Ref } from './refs.ts';
|
|
12
|
+
import {
|
|
13
|
+
isConfigRef,
|
|
14
|
+
narrowConfigRef,
|
|
15
|
+
unionAt,
|
|
16
|
+
type ConfigNode,
|
|
17
|
+
type ConfigRef,
|
|
18
|
+
type DiscriminatorOf,
|
|
19
|
+
type VariantOf,
|
|
20
|
+
} from '../core/config.ts';
|
|
13
21
|
|
|
14
22
|
// ─── Availability (builder state, type level) ────────────────────────────────────────────────
|
|
15
23
|
|
|
16
|
-
interface Slot<
|
|
24
|
+
interface Slot<
|
|
25
|
+
M extends string | null = string | null,
|
|
26
|
+
V = unknown,
|
|
27
|
+
Maybe extends boolean = boolean,
|
|
28
|
+
> {
|
|
17
29
|
model: M;
|
|
18
30
|
value: V;
|
|
19
31
|
maybe: Maybe;
|
|
20
32
|
}
|
|
21
33
|
type AvailMap = { [step: string]: Slot };
|
|
22
|
-
export type InitialAvail<In extends AnyModel> = {
|
|
34
|
+
export type InitialAvail<In extends AnyModel> = {
|
|
35
|
+
input: Slot<In['name'], Infer<In>, false>;
|
|
36
|
+
};
|
|
23
37
|
|
|
24
38
|
/** The mapper argument: keyed by step name + `input`; maybe-available outputs are `Ref<T | undefined>`. */
|
|
25
39
|
export type InRefs<A extends AvailMap> = {
|
|
26
|
-
readonly [K in keyof A as A[K][
|
|
27
|
-
A[K][
|
|
40
|
+
readonly [K in keyof A as A[K]['model'] extends null ? never : K]: Ref<
|
|
41
|
+
A[K]['maybe'] extends true ? A[K]['value'] | undefined : A[K]['value']
|
|
28
42
|
>;
|
|
29
43
|
};
|
|
30
44
|
|
|
31
|
-
type ProducersOf<A extends AvailMap, MN> = {
|
|
45
|
+
type ProducersOf<A extends AvailMap, MN> = {
|
|
46
|
+
[K in keyof A]: A[K]['model'] extends MN ? K : never;
|
|
47
|
+
}[keyof A];
|
|
32
48
|
type DefiniteProducersOf<A extends AvailMap, MN> = {
|
|
33
|
-
[K in keyof A]: A[K][
|
|
49
|
+
[K in keyof A]: A[K]['model'] extends MN
|
|
50
|
+
? A[K]['maybe'] extends false
|
|
51
|
+
? K
|
|
52
|
+
: never
|
|
53
|
+
: never;
|
|
34
54
|
}[keyof A];
|
|
35
55
|
|
|
36
|
-
type EntryError<
|
|
37
|
-
|
|
56
|
+
type EntryError<
|
|
57
|
+
SN extends string,
|
|
58
|
+
Key extends string,
|
|
59
|
+
E,
|
|
60
|
+
A extends AvailMap,
|
|
61
|
+
> = E extends External
|
|
62
|
+
? never
|
|
38
63
|
: E extends Optional<infer M>
|
|
39
|
-
? [ProducersOf<A, M[
|
|
40
|
-
? `❌ ${M[
|
|
41
|
-
: IsUnion<ProducersOf<A, M[
|
|
42
|
-
? `❌ ${M[
|
|
64
|
+
? [ProducersOf<A, M['name']>] extends [never]
|
|
65
|
+
? `❌ ${M['name']} (input '${Key}' of step '${SN}') is not available at this stage`
|
|
66
|
+
: IsUnion<ProducersOf<A, M['name']>> extends true
|
|
67
|
+
? `❌ ${M['name']} (input '${Key}' of step '${SN}') has several producers; use a mapper to pick one`
|
|
43
68
|
: never
|
|
44
69
|
: E extends AnyModel
|
|
45
|
-
? [ProducersOf<A, E[
|
|
46
|
-
? `❌ ${E[
|
|
47
|
-
: [DefiniteProducersOf<A, E[
|
|
48
|
-
? `❌ ${E[
|
|
49
|
-
: IsUnion<ProducersOf<A, E[
|
|
50
|
-
? `❌ ${E[
|
|
70
|
+
? [ProducersOf<A, E['name']>] extends [never]
|
|
71
|
+
? `❌ ${E['name']} (input '${Key}' of step '${SN}') is not available at this stage`
|
|
72
|
+
: [DefiniteProducersOf<A, E['name']>] extends [never]
|
|
73
|
+
? `❌ ${E['name']} (input '${Key}' of step '${SN}') is only maybe-available (produced inside a when block); declare it optional(${E['name']})`
|
|
74
|
+
: IsUnion<ProducersOf<A, E['name']>> extends true
|
|
75
|
+
? `❌ ${E['name']} (input '${Key}' of step '${SN}') has several producers; use a mapper to pick one`
|
|
51
76
|
: never
|
|
52
77
|
: `❌ input '${Key}' of step '${SN}' is a plain schema, not a model, so it cannot be matched by model; add a mapper`;
|
|
53
78
|
|
|
54
79
|
type DefaultMatchError<S extends AnyStep, A extends AvailMap> = {
|
|
55
|
-
[K in keyof S[
|
|
56
|
-
}[keyof S[
|
|
80
|
+
[K in keyof S['input'] & string]: EntryError<S['name'], K, S['input'][K], A>;
|
|
81
|
+
}[keyof S['input'] & string] extends infer X extends string
|
|
82
|
+
? X
|
|
83
|
+
: never; // `infer` forces display of the literal
|
|
57
84
|
|
|
58
85
|
type NameError<S extends AnyStep, A extends AvailMap, SiblingNames> =
|
|
59
86
|
// NB: no `string extends S["name"]` test here — it poisons inference of mixed stages;
|
|
60
87
|
// non-literal names are rejected in step() instead.
|
|
61
|
-
S[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
S['name'] extends 'input'
|
|
89
|
+
? "❌ 'input' is reserved for the pipeline input and cannot be a step name"
|
|
90
|
+
: S['name'] extends keyof A
|
|
91
|
+
? `❌ duplicate step name '${S['name']}' (already used in an earlier stage)`
|
|
92
|
+
: S['name'] extends SiblingNames
|
|
93
|
+
? `❌ duplicate step name '${S['name']}' (used twice in this stage)`
|
|
94
|
+
: never;
|
|
65
95
|
|
|
66
96
|
// ─── Stage entries ───────────────────────────────────────────────────────────────────────────
|
|
67
97
|
|
|
68
|
-
export type MapperFn<S extends AnyStep, A extends AvailMap> = (
|
|
69
|
-
|
|
98
|
+
export type MapperFn<S extends AnyStep, A extends AvailMap> = (
|
|
99
|
+
from: InRefs<A>,
|
|
100
|
+
) => MapperResult<S>;
|
|
101
|
+
export type StageEntry<A extends AvailMap> =
|
|
102
|
+
AnyStep | readonly [AnyStep, (from: InRefs<A>) => unknown];
|
|
70
103
|
|
|
71
|
-
type StepOfEntry<X> = X extends readonly [infer S extends AnyStep, unknown]
|
|
104
|
+
type StepOfEntry<X> = X extends readonly [infer S extends AnyStep, unknown]
|
|
105
|
+
? S
|
|
106
|
+
: X extends AnyStep
|
|
107
|
+
? X
|
|
108
|
+
: never;
|
|
72
109
|
type SiblingNames<E extends readonly unknown[], K> = {
|
|
73
|
-
[J in keyof E]: J extends K ? never : StepOfEntry<E[J]>[
|
|
110
|
+
[J in keyof E]: J extends K ? never : StepOfEntry<E[J]>['name'];
|
|
74
111
|
}[number];
|
|
75
112
|
|
|
76
113
|
/**
|
|
@@ -80,52 +117,94 @@ type SiblingNames<E extends readonly unknown[], K> = {
|
|
|
80
117
|
* so name errors of tuple entries are reported on the mapper's return type instead;
|
|
81
118
|
* - nothing may look at *neighbouring* entries through E except via NoInfer (sibling names only).
|
|
82
119
|
*/
|
|
83
|
-
export type CheckEntry<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
export type CheckEntry<
|
|
121
|
+
E extends readonly unknown[],
|
|
122
|
+
K,
|
|
123
|
+
X,
|
|
124
|
+
A extends AvailMap,
|
|
125
|
+
> = X extends readonly [infer S extends AnyStep, unknown]
|
|
126
|
+
? readonly [
|
|
127
|
+
S,
|
|
128
|
+
(from: InRefs<A>) => OrError<NameError<S, A, SiblingNames<E, K>>, MapperResult<S>>,
|
|
129
|
+
]
|
|
130
|
+
: X extends AnyStep
|
|
131
|
+
? string extends X['name']
|
|
132
|
+
? X
|
|
133
|
+
: OrError<NameError<X, A, SiblingNames<E, K>> | DefaultMatchError<X, A>, X>
|
|
134
|
+
: '❌ a stage entry must be a step or a [step, mapper] pair';
|
|
89
135
|
|
|
90
136
|
// NB: the template must mention `E[K]` literally, or reverse-mapped inference of E fails.
|
|
91
|
-
export type CheckStage<E extends readonly unknown[], A extends AvailMap> = {
|
|
137
|
+
export type CheckStage<E extends readonly unknown[], A extends AvailMap> = {
|
|
138
|
+
[K in keyof E]: CheckEntry<NoInfer<E>, K, E[K], A>;
|
|
139
|
+
};
|
|
92
140
|
|
|
93
141
|
type OutputsOf<E extends readonly unknown[], Maybe extends boolean> = {
|
|
94
|
-
[S in StepOfEntry<E[number]> as S[
|
|
95
|
-
S[
|
|
142
|
+
[S in StepOfEntry<E[number]> as S['name']]: Slot<
|
|
143
|
+
S['output'] extends AnyModel ? S['output']['name'] : null,
|
|
96
144
|
OutputValue<S>,
|
|
97
145
|
Maybe
|
|
98
146
|
>;
|
|
99
147
|
};
|
|
100
|
-
type MakeMaybe<X extends AvailMap> = {
|
|
148
|
+
type MakeMaybe<X extends AvailMap> = {
|
|
149
|
+
[K in keyof X]: Slot<X[K]['model'], X[K]['value'], true>;
|
|
150
|
+
};
|
|
101
151
|
|
|
102
152
|
// ─── select (branching on config) ────────────────────────────────────────────────────────────
|
|
103
153
|
|
|
104
154
|
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
|
|
105
|
-
type SlotIn<B, K extends PropertyKey> = B extends { [P in K]: infer X extends Slot }
|
|
155
|
+
type SlotIn<B, K extends PropertyKey> = B extends { [P in K]: infer X extends Slot }
|
|
156
|
+
? X
|
|
157
|
+
: never;
|
|
106
158
|
/** A case function: the pipeline so far, and the select's reference narrowed to the case's variant. */
|
|
107
|
-
export type SelectCase<In extends AnyModel, A extends AvailMap, R> = (
|
|
159
|
+
export type SelectCase<In extends AnyModel, A extends AvailMap, R> = (
|
|
160
|
+
p: Pipeline<In, A>,
|
|
161
|
+
ref: R,
|
|
162
|
+
) => Pipeline<In, AvailMap>;
|
|
108
163
|
export type SelectCases<In extends AnyModel, A extends AvailMap, U, D extends string> = {
|
|
109
|
-
readonly [V in Extract<U[D & keyof U], string>]: SelectCase<
|
|
164
|
+
readonly [V in Extract<U[D & keyof U], string>]: SelectCase<
|
|
165
|
+
In,
|
|
166
|
+
A,
|
|
167
|
+
ConfigNode<VariantOf<U, D, V>>
|
|
168
|
+
>;
|
|
110
169
|
};
|
|
111
|
-
type CaseAvail<C> = {
|
|
170
|
+
type CaseAvail<C> = {
|
|
171
|
+
[K in keyof C]: C[K] extends (...args: never[]) => Pipeline<AnyModel, infer B>
|
|
172
|
+
? B
|
|
173
|
+
: never;
|
|
174
|
+
}[keyof C];
|
|
112
175
|
/**
|
|
113
176
|
* After a select: a step every case has is as available as in its cases (one case always runs);
|
|
114
177
|
* a step only some cases have is maybe-available.
|
|
115
178
|
*/
|
|
116
179
|
export type AfterSelect<A extends AvailMap, B> = Simplify<
|
|
117
|
-
A &
|
|
118
|
-
|
|
119
|
-
|
|
180
|
+
A & {
|
|
181
|
+
[K in Exclude<keyof B, keyof A>]: Slot<
|
|
182
|
+
SlotIn<B, K>['model'],
|
|
183
|
+
SlotIn<B, K>['value'],
|
|
184
|
+
SlotIn<B, K>['maybe']
|
|
185
|
+
>;
|
|
186
|
+
} & {
|
|
187
|
+
[K in Exclude<KeysOfUnion<B>, keyof B | keyof A>]: Slot<
|
|
188
|
+
SlotIn<B, K>['model'],
|
|
189
|
+
SlotIn<B, K>['value'],
|
|
190
|
+
true
|
|
191
|
+
>;
|
|
192
|
+
}
|
|
120
193
|
>;
|
|
121
194
|
type SelectOnError<U> = [DiscriminatorOf<U>] extends [never]
|
|
122
|
-
?
|
|
123
|
-
: IsUnion<DiscriminatorOf<U>> extends true
|
|
195
|
+
? '❌ select needs a reference to a discriminated union in config (z.discriminatedUnion)'
|
|
196
|
+
: IsUnion<DiscriminatorOf<U>> extends true
|
|
197
|
+
? "❌ the union has more than one literal field; select can't tell which is the discriminator"
|
|
198
|
+
: never;
|
|
124
199
|
|
|
125
200
|
// ─── Conditions / triggers ───────────────────────────────────────────────────────────────────
|
|
126
201
|
|
|
127
202
|
type IsLiteralLike<T> = [T] extends [string | number | boolean]
|
|
128
|
-
? string extends T
|
|
203
|
+
? string extends T
|
|
204
|
+
? false
|
|
205
|
+
: number extends T
|
|
206
|
+
? false
|
|
207
|
+
: true
|
|
129
208
|
: false;
|
|
130
209
|
export type WhenCond<In extends AnyModel> = {
|
|
131
210
|
[K in keyof Plain<In> & string]?: Exclude<Plain<In>[K], undefined> extends infer L
|
|
@@ -143,13 +222,19 @@ export interface PushEvent {
|
|
|
143
222
|
}
|
|
144
223
|
|
|
145
224
|
type CheckHeader<V, Allowed extends string> = V extends `$SECRET:${infer X}`
|
|
146
|
-
? X extends Allowed
|
|
225
|
+
? X extends Allowed
|
|
226
|
+
? V
|
|
227
|
+
: `❌ secret '${X}' is not declared in this poll trigger's secrets`
|
|
147
228
|
: V;
|
|
148
229
|
|
|
149
230
|
/** Rejects extra fields in a trigger mapping (arrow-function returns get no excess-property check). */
|
|
150
|
-
type Exact<R, T> = R & {
|
|
231
|
+
type Exact<R, T> = R & {
|
|
232
|
+
[
|
|
233
|
+
K in Exclude<keyof R, keyof T>
|
|
234
|
+
]: `❌ '${K & string}' is not a field of the pipeline input`;
|
|
235
|
+
};
|
|
151
236
|
|
|
152
|
-
type StepNames<A extends AvailMap> = Exclude<keyof A,
|
|
237
|
+
type StepNames<A extends AvailMap> = Exclude<keyof A, 'input'> & string;
|
|
153
238
|
|
|
154
239
|
// ─── Runtime records (read by describe/harness) ──────────────────────────────────────────────
|
|
155
240
|
|
|
@@ -159,14 +244,26 @@ export interface StageStepNode {
|
|
|
159
244
|
readonly mapper?: MapperFunction;
|
|
160
245
|
}
|
|
161
246
|
export type StageNode =
|
|
162
|
-
| { readonly kind:
|
|
163
|
-
| {
|
|
164
|
-
|
|
247
|
+
| { readonly kind: 'stage'; readonly steps: readonly StageStepNode[] }
|
|
248
|
+
| {
|
|
249
|
+
readonly kind: 'when';
|
|
250
|
+
readonly cond: Readonly<Record<string, unknown>>;
|
|
251
|
+
readonly nodes: readonly StageNode[];
|
|
252
|
+
}
|
|
253
|
+
| {
|
|
254
|
+
readonly kind: 'select';
|
|
255
|
+
readonly on: ConfigRef;
|
|
256
|
+
readonly cases: Readonly<Record<string, readonly StageNode[]>>;
|
|
257
|
+
};
|
|
165
258
|
|
|
166
259
|
export type TriggerNode =
|
|
167
|
-
| { readonly kind: "push"; readonly name: string; readonly map: (event: unknown) => unknown }
|
|
168
260
|
| {
|
|
169
|
-
readonly kind:
|
|
261
|
+
readonly kind: 'push';
|
|
262
|
+
readonly name: string;
|
|
263
|
+
readonly map: (event: unknown) => unknown;
|
|
264
|
+
}
|
|
265
|
+
| {
|
|
266
|
+
readonly kind: 'poll';
|
|
170
267
|
readonly name: string;
|
|
171
268
|
readonly url: string;
|
|
172
269
|
readonly headers: Readonly<Record<string, string>>;
|
|
@@ -175,7 +272,12 @@ export type TriggerNode =
|
|
|
175
272
|
readonly intervalSeconds: number;
|
|
176
273
|
readonly map: (value: string) => unknown;
|
|
177
274
|
}
|
|
178
|
-
| {
|
|
275
|
+
| {
|
|
276
|
+
readonly kind: 'webhook';
|
|
277
|
+
readonly name: string;
|
|
278
|
+
readonly verify: WebhookVerify;
|
|
279
|
+
readonly map: (event: WebhookEvent) => unknown;
|
|
280
|
+
};
|
|
179
281
|
|
|
180
282
|
export interface FailureHandlerNode {
|
|
181
283
|
readonly step: AnyStep;
|
|
@@ -206,12 +308,18 @@ export interface PipelineData {
|
|
|
206
308
|
}
|
|
207
309
|
|
|
208
310
|
export function pipelineData(p: unknown): PipelineData {
|
|
209
|
-
const d =
|
|
210
|
-
|
|
311
|
+
const d =
|
|
312
|
+
typeof p === 'object' && p !== null
|
|
313
|
+
? (p as Record<symbol, unknown>)[PIPELINE]
|
|
314
|
+
: undefined;
|
|
315
|
+
if (!d)
|
|
316
|
+
throw new TypeError(
|
|
317
|
+
'not a pipeline (pipeline.ts must default-export pipeline({...})...)',
|
|
318
|
+
);
|
|
211
319
|
return d as PipelineData;
|
|
212
320
|
}
|
|
213
321
|
export function isPipeline(p: unknown): boolean {
|
|
214
|
-
return typeof p ===
|
|
322
|
+
return typeof p === 'object' && p !== null && PIPELINE in p;
|
|
215
323
|
}
|
|
216
324
|
|
|
217
325
|
// ─── Pipeline builder ────────────────────────────────────────────────────────────────────────
|
|
@@ -219,7 +327,8 @@ export function isPipeline(p: unknown): boolean {
|
|
|
219
327
|
// Builder calls record into shared data and hand back a fresh handle; a handle that was already
|
|
220
328
|
// extended no longer matches its type (it would claim fewer stages than the data has), so reuse is a problem.
|
|
221
329
|
const extended = new WeakSet<object>();
|
|
222
|
-
const ReusedProblem =
|
|
330
|
+
const ReusedProblem =
|
|
331
|
+
'a pipeline value was extended twice: chain the calls (pipeline({...}).stage(a).stage(b)) instead of reusing an earlier value';
|
|
223
332
|
|
|
224
333
|
export interface PipelineDef<In extends AnyModel> {
|
|
225
334
|
/** Display name. */
|
|
@@ -236,7 +345,14 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
236
345
|
declare readonly __avail: A;
|
|
237
346
|
readonly [PIPELINE]: PipelineData;
|
|
238
347
|
|
|
239
|
-
constructor(
|
|
348
|
+
constructor(
|
|
349
|
+
def: PipelineDef<In>,
|
|
350
|
+
shared?: {
|
|
351
|
+
triggers: TriggerNode[];
|
|
352
|
+
failureHandlers: FailureHandlerNode[];
|
|
353
|
+
problems: string[];
|
|
354
|
+
},
|
|
355
|
+
) {
|
|
240
356
|
this[PIPELINE] = {
|
|
241
357
|
name: def.name,
|
|
242
358
|
description: def.description,
|
|
@@ -251,7 +367,8 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
251
367
|
|
|
252
368
|
private next(): never {
|
|
253
369
|
const data = this[PIPELINE];
|
|
254
|
-
if (extended.has(this) && !data.problems.includes(ReusedProblem))
|
|
370
|
+
if (extended.has(this) && !data.problems.includes(ReusedProblem))
|
|
371
|
+
data.problems.push(ReusedProblem);
|
|
255
372
|
extended.add(this);
|
|
256
373
|
const handle = Object.create(Object.getPrototypeOf(this)) as Pipeline<In, A>;
|
|
257
374
|
(handle as { [PIPELINE]: PipelineData })[PIPELINE] = data;
|
|
@@ -259,8 +376,14 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
259
376
|
}
|
|
260
377
|
|
|
261
378
|
/** Map a GitHub push to the pipeline input (`z.input` view). Runs as code in a `map-trigger` Job per event. */
|
|
262
|
-
onPush<R extends InferInput<In>>(
|
|
263
|
-
|
|
379
|
+
onPush<R extends InferInput<In>>(
|
|
380
|
+
map: (event: PushEvent) => Exact<R, InferInput<In>>,
|
|
381
|
+
): this {
|
|
382
|
+
this[PIPELINE].triggers.push({
|
|
383
|
+
kind: 'push',
|
|
384
|
+
name: 'push',
|
|
385
|
+
map: map as (event: unknown) => unknown,
|
|
386
|
+
});
|
|
264
387
|
return this.next();
|
|
265
388
|
}
|
|
266
389
|
|
|
@@ -268,21 +391,25 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
268
391
|
* Poll a URL; when the JSON value at `valuePath` changes, map it to the pipeline input.
|
|
269
392
|
* Header values may reference the poll's own declared secrets as `$SECRET:NAME`.
|
|
270
393
|
*/
|
|
271
|
-
onPoll<
|
|
394
|
+
onPoll<
|
|
395
|
+
R extends InferInput<In>,
|
|
396
|
+
const Sec extends readonly Secret[] = [],
|
|
397
|
+
const H extends Record<string, string> = {},
|
|
398
|
+
>(
|
|
272
399
|
cfg: {
|
|
273
400
|
/** Trigger id, unique per pipeline (default "poll"). Used by `map-trigger <name>`. */
|
|
274
401
|
name?: string;
|
|
275
402
|
url: string;
|
|
276
403
|
secrets?: Sec;
|
|
277
|
-
headers?: { [K in keyof H]: CheckHeader<H[K], Sec[number][
|
|
404
|
+
headers?: { [K in keyof H]: CheckHeader<H[K], Sec[number]['name']> };
|
|
278
405
|
valuePath: string;
|
|
279
406
|
intervalSeconds: number;
|
|
280
407
|
},
|
|
281
408
|
map: (value: string) => Exact<R, InferInput<In>>,
|
|
282
409
|
): this {
|
|
283
410
|
this[PIPELINE].triggers.push({
|
|
284
|
-
kind:
|
|
285
|
-
name: cfg.name ??
|
|
411
|
+
kind: 'poll',
|
|
412
|
+
name: cfg.name ?? 'poll',
|
|
286
413
|
url: cfg.url,
|
|
287
414
|
headers: { ...((cfg.headers ?? {}) as Record<string, string>) },
|
|
288
415
|
secrets: [...(cfg.secrets ?? [])],
|
|
@@ -301,7 +428,12 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
301
428
|
cfg: { name: string; verify: WebhookVerify },
|
|
302
429
|
map: (event: WebhookEvent) => Exact<R, InferInput<In>> | null,
|
|
303
430
|
): this {
|
|
304
|
-
this[PIPELINE].triggers.push({
|
|
431
|
+
this[PIPELINE].triggers.push({
|
|
432
|
+
kind: 'webhook',
|
|
433
|
+
name: cfg.name,
|
|
434
|
+
verify: cfg.verify,
|
|
435
|
+
map: map as (event: WebhookEvent) => unknown,
|
|
436
|
+
});
|
|
305
437
|
return this.next();
|
|
306
438
|
}
|
|
307
439
|
|
|
@@ -310,34 +442,54 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
310
442
|
* `[step, from => ({ … })]` (explicit mapping). Every entry sees only what was available
|
|
311
443
|
* *before* this stage.
|
|
312
444
|
*/
|
|
313
|
-
stage<const E extends readonly StageEntry<A>[]>(
|
|
445
|
+
stage<const E extends readonly StageEntry<A>[]>(
|
|
446
|
+
...entries: CheckStage<E, A>
|
|
447
|
+
): Pipeline<In, Simplify<A & OutputsOf<E, false>>>;
|
|
314
448
|
stage(...entries: readonly unknown[]): unknown {
|
|
315
449
|
const data = this[PIPELINE];
|
|
316
450
|
const steps: StageStepNode[] = [];
|
|
317
451
|
entries.forEach((e, i) => {
|
|
318
|
-
if (
|
|
452
|
+
if (
|
|
453
|
+
Array.isArray(e) &&
|
|
454
|
+
e.length === 2 &&
|
|
455
|
+
isStep(e[0]) &&
|
|
456
|
+
typeof e[1] === 'function'
|
|
457
|
+
) {
|
|
319
458
|
steps.push({ step: e[0], mapper: e[1] as MapperFunction });
|
|
320
459
|
} else if (isStep(e)) {
|
|
321
460
|
steps.push({ step: e });
|
|
322
|
-
} else if (typeof e ===
|
|
323
|
-
data.problems.push(
|
|
461
|
+
} else if (typeof e === 'function') {
|
|
462
|
+
data.problems.push(
|
|
463
|
+
`stage entry ${i + 1} is a bare function; mappers are given as [step, from => ({ ... })] tuples`,
|
|
464
|
+
);
|
|
324
465
|
} else {
|
|
325
|
-
data.problems.push(
|
|
466
|
+
data.problems.push(
|
|
467
|
+
`stage entry ${i + 1} is neither a step nor a [step, mapper] pair`,
|
|
468
|
+
);
|
|
326
469
|
}
|
|
327
470
|
});
|
|
328
|
-
if (entries.length === 0) data.problems.push(
|
|
329
|
-
data.nodes.push({ kind:
|
|
471
|
+
if (entries.length === 0) data.problems.push('empty stage');
|
|
472
|
+
data.nodes.push({ kind: 'stage', steps });
|
|
330
473
|
return this.next();
|
|
331
474
|
}
|
|
332
475
|
|
|
333
476
|
/** Declarative condition on literal-like pipeline-input fields. Outputs produced inside become maybe-available after. */
|
|
334
|
-
when<B extends AvailMap>(
|
|
477
|
+
when<B extends AvailMap>(
|
|
478
|
+
cond: WhenCond<In>,
|
|
479
|
+
block: (p: Pipeline<In, A>) => Pipeline<In, B>,
|
|
480
|
+
): Pipeline<In, Simplify<A & MakeMaybe<Omit<B, keyof A>>>> {
|
|
335
481
|
const data = this[PIPELINE];
|
|
336
482
|
const inner = new Pipeline<In, A>({ name: data.name, input: data.input as In }, data);
|
|
337
483
|
const returned = block(inner);
|
|
338
484
|
if (!isPipeline(returned) || pipelineData(returned) !== inner[PIPELINE])
|
|
339
|
-
data.problems.push(
|
|
340
|
-
|
|
485
|
+
data.problems.push(
|
|
486
|
+
'a when block must return the pipeline it was given (p => p.stage(...))',
|
|
487
|
+
);
|
|
488
|
+
data.nodes.push({
|
|
489
|
+
kind: 'when',
|
|
490
|
+
cond: { ...(cond as object) },
|
|
491
|
+
nodes: inner[PIPELINE].nodes,
|
|
492
|
+
});
|
|
341
493
|
return this.next();
|
|
342
494
|
}
|
|
343
495
|
|
|
@@ -348,42 +500,62 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
348
500
|
*/
|
|
349
501
|
select<U, const C extends SelectCases<In, A, U, DiscriminatorOf<U>>>(
|
|
350
502
|
on: ConfigRef<U> & ([SelectOnError<U>] extends [never] ? unknown : SelectOnError<U>),
|
|
351
|
-
cases: C & {
|
|
503
|
+
cases: C & {
|
|
504
|
+
readonly [
|
|
505
|
+
K in Exclude<keyof C, keyof SelectCases<In, A, U, DiscriminatorOf<U>>>
|
|
506
|
+
]: `❌ '${K & string}' is not a case of this select`;
|
|
507
|
+
},
|
|
352
508
|
): Pipeline<In, AfterSelect<A, CaseAvail<C>>> {
|
|
353
509
|
const data = this[PIPELINE];
|
|
354
510
|
const recorded: Record<string, StageNode[]> = {};
|
|
355
511
|
const union = isConfigRef(on) ? unionAt(on) : undefined;
|
|
356
|
-
if (!union)
|
|
512
|
+
if (!union)
|
|
513
|
+
data.problems.push(
|
|
514
|
+
'select: the first argument must reference a z.discriminatedUnion in a config entry',
|
|
515
|
+
);
|
|
357
516
|
for (const [value, fn] of Object.entries(cases as Record<string, unknown>)) {
|
|
358
|
-
if (typeof fn !==
|
|
517
|
+
if (typeof fn !== 'function') {
|
|
359
518
|
data.problems.push(`select: case '${value}' is not a function`);
|
|
360
519
|
continue;
|
|
361
520
|
}
|
|
362
|
-
const inner = new Pipeline<In, A>(
|
|
363
|
-
|
|
521
|
+
const inner = new Pipeline<In, A>(
|
|
522
|
+
{ name: data.name, input: data.input as In },
|
|
523
|
+
data,
|
|
524
|
+
);
|
|
525
|
+
const returned = (fn as (p: unknown, ref: unknown) => unknown)(
|
|
526
|
+
inner,
|
|
527
|
+
union ? narrowConfigRef(on, union.discriminator, value) : on,
|
|
528
|
+
);
|
|
364
529
|
if (!isPipeline(returned) || pipelineData(returned) !== inner[PIPELINE])
|
|
365
|
-
data.problems.push(
|
|
530
|
+
data.problems.push(
|
|
531
|
+
`select: case '${value}' must return the pipeline it was given ((p, ref) => p.stage(...))`,
|
|
532
|
+
);
|
|
366
533
|
recorded[value] = inner[PIPELINE].nodes;
|
|
367
534
|
}
|
|
368
|
-
data.nodes.push({ kind:
|
|
535
|
+
data.nodes.push({ kind: 'select', on, cases: recorded });
|
|
369
536
|
return this.next();
|
|
370
537
|
}
|
|
371
538
|
|
|
372
539
|
/** Run `handler` (a step whose inputs are all FailureContext) when one of `steps` (default: any) fails. Untracked, best-effort. */
|
|
373
540
|
onFailure<S extends AnyStep>(
|
|
374
541
|
handler: S &
|
|
375
|
-
([Exclude<S[
|
|
542
|
+
([Exclude<S['input'][keyof S['input']], typeof FailureContext>] extends [never]
|
|
376
543
|
? unknown
|
|
377
544
|
: "❌ a failure handler's inputs must all be FailureContext"),
|
|
378
545
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
546
|
opts?: { steps?: readonly Step<StepNames<A>, any, any, any, any>[] },
|
|
380
547
|
): this {
|
|
381
|
-
this[PIPELINE].failureHandlers.push({
|
|
548
|
+
this[PIPELINE].failureHandlers.push({
|
|
549
|
+
step: handler as AnyStep,
|
|
550
|
+
steps: opts?.steps ? [...(opts.steps as readonly AnyStep[])] : null,
|
|
551
|
+
});
|
|
382
552
|
return this.next();
|
|
383
553
|
}
|
|
384
554
|
}
|
|
385
555
|
|
|
386
556
|
/** Start a pipeline definition. `pipeline.ts` must default-export the result. */
|
|
387
|
-
export function pipeline<In extends AnyModel>(
|
|
557
|
+
export function pipeline<In extends AnyModel>(
|
|
558
|
+
def: PipelineDef<In>,
|
|
559
|
+
): Pipeline<In, InitialAvail<In>> {
|
|
388
560
|
return new Pipeline(def) as never;
|
|
389
561
|
}
|
package/src/builder/refs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { RefBrand } from
|
|
2
|
-
import type { AnyStep, InferEntry } from
|
|
3
|
-
import type { External } from
|
|
4
|
-
import type { Primitive } from
|
|
1
|
+
import type { RefBrand } from '../core/brands.ts';
|
|
2
|
+
import type { AnyStep, InferEntry } from '../core/steps.ts';
|
|
3
|
+
import type { External } from '../core/external.ts';
|
|
4
|
+
import type { Primitive } from '../core/types.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A typed reference into the run pool — what mappers see. `symbol &` makes template strings, `+`
|
|
@@ -13,7 +13,11 @@ type RefFields<T> = [NonNullable<T>] extends [Primitive]
|
|
|
13
13
|
? {}
|
|
14
14
|
: NonNullable<T> extends readonly unknown[]
|
|
15
15
|
? {}
|
|
16
|
-
: {
|
|
16
|
+
: {
|
|
17
|
+
readonly [K in keyof NonNullable<T> & string]-?: Ref<
|
|
18
|
+
NonNullable<T>[K] | (undefined extends T ? undefined : never)
|
|
19
|
+
>;
|
|
20
|
+
};
|
|
17
21
|
|
|
18
22
|
/** What a mapper may put where a T is expected: a Ref<T>, a literal, or an object of those. */
|
|
19
23
|
export type Mappable<T> =
|
|
@@ -26,5 +30,7 @@ export type Mappable<T> =
|
|
|
26
30
|
|
|
27
31
|
/** What a mapper provides: every input except the external ones, which come from outside the run. */
|
|
28
32
|
export type MapperResult<S extends AnyStep> = {
|
|
29
|
-
[K in keyof S[
|
|
33
|
+
[K in keyof S['input'] as S['input'][K] extends External ? never : K]: Mappable<
|
|
34
|
+
InferEntry<S['input'][K]>
|
|
35
|
+
>;
|
|
30
36
|
};
|
package/src/core/brands.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Runtime markers. `Symbol.for` (not `Symbol()`) so that two copies of the SDK in one process — e.g.
|
|
2
2
|
// a dev runner and the user's installed package — still recognise each other's objects.
|
|
3
|
-
export const STEP = Symbol.for(
|
|
4
|
-
export const IMPLEMENTATION = Symbol.for(
|
|
5
|
-
export const ORIGIN = Symbol.for(
|
|
6
|
-
export const SKIPPED = Symbol.for(
|
|
7
|
-
export const PIPELINE = Symbol.for(
|
|
8
|
-
export const REF_PATH = Symbol.for(
|
|
3
|
+
export const STEP = Symbol.for('olve.pipelines.step');
|
|
4
|
+
export const IMPLEMENTATION = Symbol.for('olve.pipelines.implementation');
|
|
5
|
+
export const ORIGIN = Symbol.for('olve.pipelines.origin');
|
|
6
|
+
export const SKIPPED = Symbol.for('olve.pipelines.skipped');
|
|
7
|
+
export const PIPELINE = Symbol.for('olve.pipelines.pipeline');
|
|
8
|
+
export const REF_PATH = Symbol.for('olve.pipelines.refPath');
|
|
9
9
|
|
|
10
10
|
// Type-only brands (never exist at runtime).
|
|
11
11
|
export declare const ModelBrand: unique symbol;
|