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