@olivervea/pipelines 2.0.0-beta.17 → 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.
Files changed (53) hide show
  1. package/README.md +2 -0
  2. package/manifest.schema.json +22 -0
  3. package/package.json +4 -2
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +111 -43
  6. package/src/build/cli.ts +28 -21
  7. package/src/build/elf.ts +8 -7
  8. package/src/build/hashes.ts +61 -28
  9. package/src/build/project.ts +69 -30
  10. package/src/builder/pipeline.ts +288 -102
  11. package/src/builder/refs.ts +12 -6
  12. package/src/core/brands.ts +6 -6
  13. package/src/core/config-refs.ts +46 -20
  14. package/src/core/config.ts +110 -33
  15. package/src/core/external.ts +30 -12
  16. package/src/core/handlers.ts +54 -14
  17. package/src/core/models.ts +29 -14
  18. package/src/core/origin.ts +7 -4
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +39 -23
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +111 -40
  24. package/src/ctx/index.ts +75 -34
  25. package/src/ctx/sh.ts +104 -44
  26. package/src/ctx/types.ts +4 -4
  27. package/src/describe/describe.ts +558 -194
  28. package/src/describe/manifest-schema.ts +217 -79
  29. package/src/describe/mapper-source.ts +249 -104
  30. package/src/describe/record.ts +67 -37
  31. package/src/dev/cli.ts +64 -34
  32. package/src/harness/env.ts +33 -27
  33. package/src/harness/execute.ts +55 -29
  34. package/src/harness/index.ts +21 -8
  35. package/src/harness/main.ts +197 -74
  36. package/src/harness/result.ts +38 -22
  37. package/src/harness/validate.ts +62 -19
  38. package/src/index.ts +123 -14
  39. package/src/library/checks.ts +11 -6
  40. package/src/library/compose-deploy.ts +40 -23
  41. package/src/library/github-source.ts +69 -39
  42. package/src/library/helm-deploy-ssh.ts +213 -76
  43. package/src/library/helm-deploy.ts +263 -85
  44. package/src/library/http-health.ts +7 -5
  45. package/src/library/image-build.ts +293 -117
  46. package/src/library/index.ts +47 -15
  47. package/src/library/models.ts +17 -9
  48. package/src/library/ssh.ts +31 -14
  49. package/src/testing/index.ts +63 -24
  50. package/src/util/canonical.ts +7 -4
  51. package/src/util/handler-hash.ts +15 -5
  52. package/src/util/hash.ts +5 -5
  53. package/src/util/tar.ts +74 -37
@@ -1,76 +1,113 @@
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 { isConfigRef, narrowConfigRef, unionAt, type ConfigNode, type ConfigRef, type DiscriminatorOf, type VariantOf } from "../core/config.ts";
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<M extends string | null = string | null, V = unknown, Maybe extends boolean = boolean> {
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> = { input: Slot<In["name"], Infer<In>, false> };
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]["model"] extends null ? never : K]: Ref<
27
- A[K]["maybe"] extends true ? A[K]["value"] | undefined : A[K]["value"]
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> = { [K in keyof A]: A[K]["model"] extends MN ? K : never }[keyof A];
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]["model"] extends MN ? (A[K]["maybe"] extends false ? K : never) : never;
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<SN extends string, Key extends string, E, A extends AvailMap> =
37
- E extends External ? never
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["name"]>] extends [never]
40
- ? `❌ ${M["name"]} (input '${Key}' of step '${SN}') is not available at this stage`
41
- : IsUnion<ProducersOf<A, M["name"]>> extends true
42
- ? `❌ ${M["name"]} (input '${Key}' of step '${SN}') has several producers; use a mapper to pick one`
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["name"]>] extends [never]
46
- ? `❌ ${E["name"]} (input '${Key}' of step '${SN}') is not available at this stage`
47
- : [DefiniteProducersOf<A, E["name"]>] extends [never]
48
- ? `❌ ${E["name"]} (input '${Key}' of step '${SN}') is only maybe-available (produced inside a when block); declare it optional(${E["name"]})`
49
- : IsUnion<ProducersOf<A, E["name"]>> extends true
50
- ? `❌ ${E["name"]} (input '${Key}' of step '${SN}') has several producers; use a mapper to pick one`
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["input"] & string]: EntryError<S["name"], K, S["input"][K], A>;
56
- }[keyof S["input"] & string] extends infer X extends string ? X : never; // `infer` forces display of the literal
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["name"] extends "input" ? "❌ 'input' is reserved for the pipeline input and cannot be a step name"
62
- : S["name"] extends keyof A ? `❌ duplicate step name '${S["name"]}' (already used in an earlier stage)`
63
- : S["name"] extends SiblingNames ? `❌ duplicate step name '${S["name"]}' (used twice in this stage)`
64
- : never;
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> = (from: InRefs<A>) => MapperResult<S>;
69
- export type StageEntry<A extends AvailMap> = AnyStep | readonly [AnyStep, (from: InRefs<A>) => unknown];
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] ? S : X extends AnyStep ? X : never;
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]>["name"];
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<E extends readonly unknown[], K, X, A extends AvailMap> =
84
- X extends readonly [infer S extends AnyStep, unknown]
85
- ? readonly [S, (from: InRefs<A>) => OrError<NameError<S, A, SiblingNames<E, K>>, MapperResult<S>>]
86
- : X extends AnyStep
87
- ? string extends X["name"] ? X : OrError<NameError<X, A, SiblingNames<E, K>> | DefaultMatchError<X, A>, X>
88
- : "❌ a stage entry must be a step or a [step, mapper] pair";
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> = { [K in keyof E]: CheckEntry<NoInfer<E>, K, E[K], A> };
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["name"]]: Slot<
95
- S["output"] extends AnyModel ? S["output"]["name"] : null,
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> = { [K in keyof X]: Slot<X[K]["model"], X[K]["value"], true> };
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 } ? X : never;
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> = (p: Pipeline<In, A>, ref: R) => Pipeline<In, AvailMap>;
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<In, A, ConfigNode<VariantOf<U, D, V>>>;
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> = { [K in keyof C]: C[K] extends (...args: never[]) => Pipeline<AnyModel, infer B> ? B : never }[keyof 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
- { [K in Exclude<keyof B, keyof A>]: Slot<SlotIn<B, K>["model"], SlotIn<B, K>["value"], SlotIn<B, K>["maybe"]> } &
119
- { [K in Exclude<KeysOfUnion<B>, keyof B | keyof A>]: Slot<SlotIn<B, K>["model"], SlotIn<B, K>["value"], true> }
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
- ? "❌ select needs a reference to a discriminated union in config (z.discriminatedUnion)"
123
- : IsUnion<DiscriminatorOf<U>> extends true ? "❌ the union has more than one literal field; select can't tell which is the discriminator" : 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;
124
199
 
125
200
  // ─── Conditions / triggers ───────────────────────────────────────────────────────────────────
126
201
 
127
202
  type IsLiteralLike<T> = [T] extends [string | number | boolean]
128
- ? string extends T ? false : number extends T ? false : true
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 ? V : `❌ secret '${X}' is not declared in this poll trigger's secrets`
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 & { [K in Exclude<keyof R, keyof T>]: `❌ '${K & string}' is not a field of the pipeline input` };
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, "input"> & string;
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: "stage"; readonly steps: readonly StageStepNode[] }
163
- | { readonly kind: "when"; readonly cond: Readonly<Record<string, unknown>>; readonly nodes: readonly StageNode[] }
164
- | { readonly kind: "select"; readonly on: ConfigRef; readonly cases: Readonly<Record<string, readonly StageNode[]>> };
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: "poll";
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
- | { readonly kind: "webhook"; readonly name: string; readonly verify: WebhookVerify; readonly map: (event: WebhookEvent) => unknown };
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;
@@ -183,10 +285,21 @@ export interface FailureHandlerNode {
183
285
  readonly steps: readonly AnyStep[] | null;
184
286
  }
185
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
+
186
298
  export interface PipelineData {
187
299
  readonly name: string;
188
300
  readonly description: string | undefined;
189
301
  readonly input: AnyModel;
302
+ readonly retention: RetentionPolicy | undefined;
190
303
  readonly nodes: StageNode[];
191
304
  readonly triggers: TriggerNode[];
192
305
  readonly failureHandlers: FailureHandlerNode[];
@@ -195,12 +308,18 @@ export interface PipelineData {
195
308
  }
196
309
 
197
310
  export function pipelineData(p: unknown): PipelineData {
198
- const d = typeof p === "object" && p !== null ? (p as Record<symbol, unknown>)[PIPELINE] : undefined;
199
- if (!d) throw new TypeError("not a pipeline (pipeline.ts must default-export pipeline({...})...)");
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
+ );
200
319
  return d as PipelineData;
201
320
  }
202
321
  export function isPipeline(p: unknown): boolean {
203
- return typeof p === "object" && p !== null && PIPELINE in p;
322
+ return typeof p === 'object' && p !== null && PIPELINE in p;
204
323
  }
205
324
 
206
325
  // ─── Pipeline builder ────────────────────────────────────────────────────────────────────────
@@ -208,7 +327,8 @@ export function isPipeline(p: unknown): boolean {
208
327
  // Builder calls record into shared data and hand back a fresh handle; a handle that was already
209
328
  // extended no longer matches its type (it would claim fewer stages than the data has), so reuse is a problem.
210
329
  const extended = new WeakSet<object>();
211
- const ReusedProblem = "a pipeline value was extended twice: chain the calls (pipeline({...}).stage(a).stage(b)) instead of reusing an earlier value";
330
+ const ReusedProblem =
331
+ 'a pipeline value was extended twice: chain the calls (pipeline({...}).stage(a).stage(b)) instead of reusing an earlier value';
212
332
 
213
333
  export interface PipelineDef<In extends AnyModel> {
214
334
  /** Display name. */
@@ -216,6 +336,8 @@ export interface PipelineDef<In extends AnyModel> {
216
336
  description?: string;
217
337
  /** The pipeline input model (what triggers produce, what `when` matches on). */
218
338
  input: In;
339
+ /** Retention overrides for this pipeline's runs and binaries (unset fields keep the instance's values). */
340
+ retention?: RetentionPolicy;
219
341
  }
220
342
 
221
343
  export class Pipeline<In extends AnyModel, A extends AvailMap> {
@@ -223,11 +345,19 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
223
345
  declare readonly __avail: A;
224
346
  readonly [PIPELINE]: PipelineData;
225
347
 
226
- constructor(def: PipelineDef<In>, shared?: { triggers: TriggerNode[]; failureHandlers: FailureHandlerNode[]; problems: string[] }) {
348
+ constructor(
349
+ def: PipelineDef<In>,
350
+ shared?: {
351
+ triggers: TriggerNode[];
352
+ failureHandlers: FailureHandlerNode[];
353
+ problems: string[];
354
+ },
355
+ ) {
227
356
  this[PIPELINE] = {
228
357
  name: def.name,
229
358
  description: def.description,
230
359
  input: def.input,
360
+ retention: def.retention,
231
361
  nodes: [],
232
362
  triggers: shared?.triggers ?? [],
233
363
  failureHandlers: shared?.failureHandlers ?? [],
@@ -237,7 +367,8 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
237
367
 
238
368
  private next(): never {
239
369
  const data = this[PIPELINE];
240
- if (extended.has(this) && !data.problems.includes(ReusedProblem)) data.problems.push(ReusedProblem);
370
+ if (extended.has(this) && !data.problems.includes(ReusedProblem))
371
+ data.problems.push(ReusedProblem);
241
372
  extended.add(this);
242
373
  const handle = Object.create(Object.getPrototypeOf(this)) as Pipeline<In, A>;
243
374
  (handle as { [PIPELINE]: PipelineData })[PIPELINE] = data;
@@ -245,8 +376,14 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
245
376
  }
246
377
 
247
378
  /** Map a GitHub push to the pipeline input (`z.input` view). Runs as code in a `map-trigger` Job per event. */
248
- onPush<R extends InferInput<In>>(map: (event: PushEvent) => Exact<R, InferInput<In>>): this {
249
- this[PIPELINE].triggers.push({ kind: "push", name: "push", map: map as (event: unknown) => unknown });
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
+ });
250
387
  return this.next();
251
388
  }
252
389
 
@@ -254,21 +391,25 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
254
391
  * Poll a URL; when the JSON value at `valuePath` changes, map it to the pipeline input.
255
392
  * Header values may reference the poll's own declared secrets as `$SECRET:NAME`.
256
393
  */
257
- onPoll<R extends InferInput<In>, const Sec extends readonly Secret[] = [], const H extends Record<string, string> = {}>(
394
+ onPoll<
395
+ R extends InferInput<In>,
396
+ const Sec extends readonly Secret[] = [],
397
+ const H extends Record<string, string> = {},
398
+ >(
258
399
  cfg: {
259
400
  /** Trigger id, unique per pipeline (default "poll"). Used by `map-trigger <name>`. */
260
401
  name?: string;
261
402
  url: string;
262
403
  secrets?: Sec;
263
- headers?: { [K in keyof H]: CheckHeader<H[K], Sec[number]["name"]> };
404
+ headers?: { [K in keyof H]: CheckHeader<H[K], Sec[number]['name']> };
264
405
  valuePath: string;
265
406
  intervalSeconds: number;
266
407
  },
267
408
  map: (value: string) => Exact<R, InferInput<In>>,
268
409
  ): this {
269
410
  this[PIPELINE].triggers.push({
270
- kind: "poll",
271
- name: cfg.name ?? "poll",
411
+ kind: 'poll',
412
+ name: cfg.name ?? 'poll',
272
413
  url: cfg.url,
273
414
  headers: { ...((cfg.headers ?? {}) as Record<string, string>) },
274
415
  secrets: [...(cfg.secrets ?? [])],
@@ -287,7 +428,12 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
287
428
  cfg: { name: string; verify: WebhookVerify },
288
429
  map: (event: WebhookEvent) => Exact<R, InferInput<In>> | null,
289
430
  ): this {
290
- this[PIPELINE].triggers.push({ kind: "webhook", name: cfg.name, verify: cfg.verify, map: map as (event: WebhookEvent) => unknown });
431
+ this[PIPELINE].triggers.push({
432
+ kind: 'webhook',
433
+ name: cfg.name,
434
+ verify: cfg.verify,
435
+ map: map as (event: WebhookEvent) => unknown,
436
+ });
291
437
  return this.next();
292
438
  }
293
439
 
@@ -296,34 +442,54 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
296
442
  * `[step, from => ({ … })]` (explicit mapping). Every entry sees only what was available
297
443
  * *before* this stage.
298
444
  */
299
- stage<const E extends readonly StageEntry<A>[]>(...entries: CheckStage<E, A>): Pipeline<In, Simplify<A & OutputsOf<E, false>>>;
445
+ stage<const E extends readonly StageEntry<A>[]>(
446
+ ...entries: CheckStage<E, A>
447
+ ): Pipeline<In, Simplify<A & OutputsOf<E, false>>>;
300
448
  stage(...entries: readonly unknown[]): unknown {
301
449
  const data = this[PIPELINE];
302
450
  const steps: StageStepNode[] = [];
303
451
  entries.forEach((e, i) => {
304
- if (Array.isArray(e) && e.length === 2 && isStep(e[0]) && typeof e[1] === "function") {
452
+ if (
453
+ Array.isArray(e) &&
454
+ e.length === 2 &&
455
+ isStep(e[0]) &&
456
+ typeof e[1] === 'function'
457
+ ) {
305
458
  steps.push({ step: e[0], mapper: e[1] as MapperFunction });
306
459
  } else if (isStep(e)) {
307
460
  steps.push({ step: e });
308
- } else if (typeof e === "function") {
309
- data.problems.push(`stage entry ${i + 1} is a bare function; mappers are given as [step, from => ({ ... })] tuples`);
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
+ );
310
465
  } else {
311
- data.problems.push(`stage entry ${i + 1} is neither a step nor a [step, mapper] pair`);
466
+ data.problems.push(
467
+ `stage entry ${i + 1} is neither a step nor a [step, mapper] pair`,
468
+ );
312
469
  }
313
470
  });
314
- if (entries.length === 0) data.problems.push("empty stage");
315
- data.nodes.push({ kind: "stage", steps });
471
+ if (entries.length === 0) data.problems.push('empty stage');
472
+ data.nodes.push({ kind: 'stage', steps });
316
473
  return this.next();
317
474
  }
318
475
 
319
476
  /** Declarative condition on literal-like pipeline-input fields. Outputs produced inside become maybe-available after. */
320
- when<B extends AvailMap>(cond: WhenCond<In>, block: (p: Pipeline<In, A>) => Pipeline<In, B>): Pipeline<In, Simplify<A & MakeMaybe<Omit<B, keyof A>>>> {
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>>>> {
321
481
  const data = this[PIPELINE];
322
482
  const inner = new Pipeline<In, A>({ name: data.name, input: data.input as In }, data);
323
483
  const returned = block(inner);
324
484
  if (!isPipeline(returned) || pipelineData(returned) !== inner[PIPELINE])
325
- data.problems.push("a when block must return the pipeline it was given (p => p.stage(...))");
326
- data.nodes.push({ kind: "when", cond: { ...(cond as object) }, nodes: inner[PIPELINE].nodes });
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
+ });
327
493
  return this.next();
328
494
  }
329
495
 
@@ -334,42 +500,62 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
334
500
  */
335
501
  select<U, const C extends SelectCases<In, A, U, DiscriminatorOf<U>>>(
336
502
  on: ConfigRef<U> & ([SelectOnError<U>] extends [never] ? unknown : SelectOnError<U>),
337
- cases: C & { readonly [K in Exclude<keyof C, keyof SelectCases<In, A, U, DiscriminatorOf<U>>>]: `❌ '${K & string}' is not a case of this select` },
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
+ },
338
508
  ): Pipeline<In, AfterSelect<A, CaseAvail<C>>> {
339
509
  const data = this[PIPELINE];
340
510
  const recorded: Record<string, StageNode[]> = {};
341
511
  const union = isConfigRef(on) ? unionAt(on) : undefined;
342
- if (!union) data.problems.push("select: the first argument must reference a z.discriminatedUnion in a config entry");
512
+ if (!union)
513
+ data.problems.push(
514
+ 'select: the first argument must reference a z.discriminatedUnion in a config entry',
515
+ );
343
516
  for (const [value, fn] of Object.entries(cases as Record<string, unknown>)) {
344
- if (typeof fn !== "function") {
517
+ if (typeof fn !== 'function') {
345
518
  data.problems.push(`select: case '${value}' is not a function`);
346
519
  continue;
347
520
  }
348
- const inner = new Pipeline<In, A>({ name: data.name, input: data.input as In }, data);
349
- const returned = (fn as (p: unknown, ref: unknown) => unknown)(inner, union ? narrowConfigRef(on, union.discriminator, value) : on);
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
+ );
350
529
  if (!isPipeline(returned) || pipelineData(returned) !== inner[PIPELINE])
351
- data.problems.push(`select: case '${value}' must return the pipeline it was given ((p, ref) => p.stage(...))`);
530
+ data.problems.push(
531
+ `select: case '${value}' must return the pipeline it was given ((p, ref) => p.stage(...))`,
532
+ );
352
533
  recorded[value] = inner[PIPELINE].nodes;
353
534
  }
354
- data.nodes.push({ kind: "select", on, cases: recorded });
535
+ data.nodes.push({ kind: 'select', on, cases: recorded });
355
536
  return this.next();
356
537
  }
357
538
 
358
539
  /** Run `handler` (a step whose inputs are all FailureContext) when one of `steps` (default: any) fails. Untracked, best-effort. */
359
540
  onFailure<S extends AnyStep>(
360
541
  handler: S &
361
- ([Exclude<S["input"][keyof S["input"]], typeof FailureContext>] extends [never]
542
+ ([Exclude<S['input'][keyof S['input']], typeof FailureContext>] extends [never]
362
543
  ? unknown
363
544
  : "❌ a failure handler's inputs must all be FailureContext"),
364
545
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
365
546
  opts?: { steps?: readonly Step<StepNames<A>, any, any, any, any>[] },
366
547
  ): this {
367
- this[PIPELINE].failureHandlers.push({ step: handler as AnyStep, steps: opts?.steps ? [...(opts.steps as readonly AnyStep[])] : null });
548
+ this[PIPELINE].failureHandlers.push({
549
+ step: handler as AnyStep,
550
+ steps: opts?.steps ? [...(opts.steps as readonly AnyStep[])] : null,
551
+ });
368
552
  return this.next();
369
553
  }
370
554
  }
371
555
 
372
556
  /** Start a pipeline definition. `pipeline.ts` must default-export the result. */
373
- export function pipeline<In extends AnyModel>(def: PipelineDef<In>): Pipeline<In, InitialAvail<In>> {
557
+ export function pipeline<In extends AnyModel>(
558
+ def: PipelineDef<In>,
559
+ ): Pipeline<In, InitialAvail<In>> {
374
560
  return new Pipeline(def) as never;
375
561
  }