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