@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.
Files changed (64) hide show
  1. package/README.md +163 -78
  2. package/manifest.schema.json +300 -16
  3. package/package.json +6 -4
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +98 -64
  6. package/src/build/cli.ts +33 -23
  7. package/src/build/elf.ts +16 -7
  8. package/src/build/hashes.ts +40 -51
  9. package/src/build/project.ts +55 -52
  10. package/src/builder/pipeline.ts +348 -89
  11. package/src/builder/refs.ts +15 -5
  12. package/src/core/brands.ts +6 -5
  13. package/src/core/config-refs.ts +103 -0
  14. package/src/core/config.ts +224 -0
  15. package/src/core/external.ts +86 -0
  16. package/src/core/handlers.ts +62 -15
  17. package/src/core/matrix.ts +150 -0
  18. package/src/core/models.ts +37 -15
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +113 -38
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +119 -42
  24. package/src/ctx/index.ts +103 -38
  25. package/src/ctx/links.ts +30 -0
  26. package/src/ctx/sh.ts +104 -44
  27. package/src/ctx/types.ts +24 -4
  28. package/src/describe/config-schema.ts +98 -0
  29. package/src/describe/context.ts +187 -0
  30. package/src/describe/describe.ts +63 -355
  31. package/src/describe/failure-handlers.ts +34 -0
  32. package/src/describe/manifest-schema.ts +304 -61
  33. package/src/describe/mapper-source.ts +252 -105
  34. package/src/describe/record.ts +67 -37
  35. package/src/describe/schemas.ts +71 -0
  36. package/src/describe/stages.ts +166 -0
  37. package/src/describe/step.ts +544 -0
  38. package/src/describe/triggers.ts +72 -0
  39. package/src/dev/cli.ts +62 -35
  40. package/src/harness/env.ts +47 -25
  41. package/src/harness/execute.ts +74 -30
  42. package/src/harness/exit.ts +1 -1
  43. package/src/harness/index.ts +19 -8
  44. package/src/harness/main.ts +234 -98
  45. package/src/harness/result.ts +45 -24
  46. package/src/harness/validate.ts +82 -15
  47. package/src/index.ts +139 -10
  48. package/src/library/checks.ts +11 -6
  49. package/src/library/compose-deploy.ts +115 -0
  50. package/src/library/github-source.ts +100 -47
  51. package/src/library/helm-deploy-ssh.ts +426 -112
  52. package/src/library/helm-deploy.ts +529 -0
  53. package/src/library/http-health.ts +7 -5
  54. package/src/library/image-build.ts +510 -162
  55. package/src/library/index.ts +48 -14
  56. package/src/library/models.ts +35 -14
  57. package/src/library/ssh.ts +44 -18
  58. package/src/testing/index.ts +100 -35
  59. package/src/util/canonical.ts +7 -4
  60. package/src/util/code-hash.ts +17 -0
  61. package/src/util/hash.ts +5 -5
  62. package/src/util/tar.ts +83 -38
  63. package/src/library/script.ts +0 -85
  64. package/src/util/handler-hash.ts +0 -22
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { canonicalize } from '../util/canonical.ts';
3
+ import type { ModelSchemasT } from './manifest-schema.ts';
4
+
5
+ export type JsonSchema = Record<string, unknown>;
6
+
7
+ function stripStrict(v: unknown): unknown {
8
+ if (Array.isArray(v)) return v.map(stripStrict);
9
+ if (v !== null && typeof v === 'object') {
10
+ const out: Record<string, unknown> = {};
11
+ for (const [k, x] of Object.entries(v))
12
+ if (!(k === 'additionalProperties' && x === false)) out[k] = stripStrict(x);
13
+ return out;
14
+ }
15
+ return v;
16
+ }
17
+
18
+ /** JSON Schema views of a zod schema. Throws if it is not representable (dates, transforms, …). */
19
+ export function schemaViews(schema: z.ZodType): ModelSchemasT {
20
+ const view = (io: 'input' | 'output') => {
21
+ const js = z.toJSONSchema(schema, { io, target: 'draft-2020-12' }) as JsonSchema;
22
+ delete js.$schema; // constant noise; apiVersion pins the dialect
23
+ return canonicalize(js) as JsonSchema;
24
+ };
25
+ const output = view('output');
26
+ const input = view('input');
27
+ const same = JSON.stringify(stripStrict(output)) === JSON.stringify(input);
28
+ return same ? { output } : { output, input };
29
+ }
30
+
31
+ /** Whether `segments` lead to a field of a JSON Schema. */
32
+ export function resolveField(
33
+ schema: JsonSchema | undefined,
34
+ segments: readonly string[],
35
+ ): boolean {
36
+ if (!schema) return false;
37
+ if (segments.length === 0) return true;
38
+ const [seg, ...rest] = segments as [string, ...string[]];
39
+ if (Array.isArray(schema.anyOf))
40
+ return (schema.anyOf as JsonSchema[]).some((b) => resolveField(b, segments));
41
+ if (Array.isArray(schema.oneOf))
42
+ return (schema.oneOf as JsonSchema[]).some((b) => resolveField(b, segments));
43
+ if (Array.isArray(schema.allOf))
44
+ return (schema.allOf as JsonSchema[]).some((b) => resolveField(b, segments));
45
+ const props = schema.properties as Record<string, JsonSchema> | undefined;
46
+ if (props && Object.hasOwn(props, seg)) return resolveField(props[seg], rest);
47
+ const extra = schema.additionalProperties;
48
+ if (extra && typeof extra === 'object') return resolveField(extra as JsonSchema, rest);
49
+ // A schema without type/properties/anyOf accepts anything (z.unknown / z.any).
50
+ return Object.keys(schema).every(
51
+ (k) => k === 'description' || k === 'title' || k === 'default',
52
+ );
53
+ }
54
+
55
+ export function literalValues(field: JsonSchema | undefined): unknown[] | undefined {
56
+ if (!field) return undefined;
57
+ if (Array.isArray(field.enum)) return field.enum as unknown[];
58
+ if (field.const !== undefined) return [field.const];
59
+ if (field.type === 'boolean') return [true, false];
60
+ const alts = (field.anyOf ?? field.oneOf) as JsonSchema[] | undefined;
61
+ if (Array.isArray(alts)) {
62
+ const all: unknown[] = [];
63
+ for (const a of alts) {
64
+ const l = literalValues(a);
65
+ if (!l) return undefined;
66
+ all.push(...l);
67
+ }
68
+ return all;
69
+ }
70
+ return undefined;
71
+ }
@@ -0,0 +1,166 @@
1
+ import { configRefPath, configRefTarget, isConfigRef, unionAt } from '../core/config.ts';
2
+ import { isStep } from '../core/steps.ts';
3
+ import type { StageNode } from '../builder/pipeline.ts';
4
+ import type { Stage, StepDescriptorT } from './manifest-schema.ts';
5
+ import { literalValues, type JsonSchema } from './schemas.ts';
6
+ import type { Avail, CaseOf, DescribeContext } from './context.ts';
7
+ import { describeStep } from './step.ts';
8
+
9
+ type StageGroup = Extract<StageNode, { kind: 'stage' }>;
10
+ type WhenNode = Extract<StageNode, { kind: 'when' }>;
11
+ type SelectNode = Extract<StageNode, { kind: 'select' }>;
12
+
13
+ /** Describes stages, `when` blocks and selects in order, adding what each makes available to `avail`. */
14
+ export function describeStages(
15
+ ctx: DescribeContext,
16
+ input: { name: string },
17
+ nodes: readonly StageNode[],
18
+ avail: Map<string, Avail>,
19
+ cases: readonly CaseOf[] = [],
20
+ ): Stage[] {
21
+ const out: Stage[] = [];
22
+ for (const n of nodes) {
23
+ if (n.kind === 'select') {
24
+ out.push(describeSelect(ctx, input, n, avail, cases, ctx.nextSelectId()));
25
+ continue;
26
+ }
27
+ if (n.kind === 'stage') {
28
+ const stage = describeStage(ctx, n, avail, cases);
29
+ if (stage) out.push(stage);
30
+ } else out.push(describeWhen(ctx, input, n, avail, cases));
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function describeStage(
36
+ ctx: DescribeContext,
37
+ n: StageGroup,
38
+ avail: Map<string, Avail>,
39
+ cases: readonly CaseOf[],
40
+ ): Stage | undefined {
41
+ const descs = n.steps
42
+ .map((ss) => {
43
+ if (!isStep(ss.step)) {
44
+ ctx.problems.push('a stage entry is not a step');
45
+ return undefined;
46
+ }
47
+ ctx.registerStep(ss.step, 'stage', cases);
48
+ return describeStep(ctx, ss, avail, false, cases);
49
+ })
50
+ .filter((d): d is StepDescriptorT => d !== undefined);
51
+ // Outputs become available only after the barrier (same-stage siblings can't see each other).
52
+ for (const ss of n.steps)
53
+ if (isStep(ss.step))
54
+ avail.set(ss.step.name, {
55
+ model: ss.step.output?.name ?? null,
56
+ maybe: false,
57
+ ...(ss.step.matrix ? { matrix: true } : {}),
58
+ });
59
+ return descs.length ? { steps: descs } : undefined;
60
+ }
61
+
62
+ function describeWhen(
63
+ ctx: DescribeContext,
64
+ input: { name: string },
65
+ n: WhenNode,
66
+ avail: Map<string, Avail>,
67
+ cases: readonly CaseOf[],
68
+ ): Stage {
69
+ const problems = ctx.problems;
70
+ const when: Record<string, (string | number | boolean)[]> = {};
71
+ const props = (ctx.models.get(input.name)?.schemas.output.properties ?? {}) as Record<
72
+ string,
73
+ JsonSchema
74
+ >;
75
+ for (const [field, raw] of Object.entries(n.cond)) {
76
+ const values = (Array.isArray(raw) ? raw : [raw]) as (string | number | boolean)[];
77
+ if (!Object.hasOwn(props, field))
78
+ problems.push(
79
+ `when: '${field}' is not a field of the pipeline input ${input.name}`,
80
+ );
81
+ else {
82
+ const literals = literalValues(props[field]);
83
+ if (!literals)
84
+ problems.push(
85
+ `when: '${field}' is not a literal-like field (enum, boolean or literal)`,
86
+ );
87
+ else
88
+ for (const v of values)
89
+ if (!literals.includes(v))
90
+ problems.push(
91
+ `when: ${JSON.stringify(v)} is not a valid value of '${field}' (${literals.map((l) => JSON.stringify(l)).join(', ')})`,
92
+ );
93
+ }
94
+ if (values.length === 0) problems.push(`when: '${field}' has no values`);
95
+ when[field] = values;
96
+ }
97
+ const inner = new Map(avail);
98
+ const stages = describeStages(ctx, input, n.nodes, inner, cases);
99
+ for (const [name, a] of inner)
100
+ if (!avail.has(name)) avail.set(name, { ...a, maybe: true });
101
+ return { when, stages };
102
+ }
103
+
104
+ function describeSelect(
105
+ ctx: DescribeContext,
106
+ input: { name: string },
107
+ n: SelectNode,
108
+ avail: Map<string, Avail>,
109
+ cases: readonly CaseOf[],
110
+ id: number,
111
+ ): Stage {
112
+ const problems = ctx.problems;
113
+ if (!isConfigRef(n.on)) {
114
+ problems.push('select: the first argument must be a config reference');
115
+ return { select: '', discriminator: '', cases: {} };
116
+ }
117
+ const ref = configRefPath(n.on);
118
+ const { entry, case: kase } = configRefTarget(n.on);
119
+ ctx.useConfig(entry, `select '${ref}'`);
120
+ if (kase !== undefined && !cases.some((c) => c.cond === kase))
121
+ problems.push(
122
+ `select '${ref}': the reference is narrowed to the case '${kase}' and can only be used inside it`,
123
+ );
124
+ const union = unionAt(n.on);
125
+ if (!union)
126
+ problems.push(
127
+ `select '${ref}': it must reference a z.discriminatedUnion in config '${entry.name}'`,
128
+ );
129
+ else {
130
+ for (const v of union.values)
131
+ if (!Object.hasOwn(n.cases, v))
132
+ problems.push(`select '${ref}': no case for ${union.discriminator} '${v}'`);
133
+ for (const v of Object.keys(n.cases))
134
+ if (!union.values.includes(v))
135
+ problems.push(
136
+ `select '${ref}': '${v}' is not a value of ${union.discriminator} (${union.values.join(', ')})`,
137
+ );
138
+ }
139
+ const discriminator = union?.discriminator ?? '';
140
+ const branches = Object.entries(n.cases).map(([value, nodes]) => {
141
+ const inner = new Map(avail);
142
+ const stages = describeStages(ctx, input, nodes, inner, [
143
+ ...cases,
144
+ { select: id, value, cond: `${ref}.${discriminator}=${value}` },
145
+ ]);
146
+ return { value, inner, stages };
147
+ });
148
+ // A step every case has is as available as in its cases; one only some cases have is maybe-available.
149
+ const added = new Set(
150
+ branches.flatMap((b) => [...b.inner.keys()].filter((k) => !avail.has(k))),
151
+ );
152
+ for (const name of added) {
153
+ const present = branches
154
+ .map((b) => b.inner.get(name))
155
+ .filter((a): a is Avail => a !== undefined);
156
+ avail.set(name, {
157
+ ...present[0]!,
158
+ maybe: present.length < branches.length || present.some((a) => a.maybe),
159
+ });
160
+ }
161
+ return {
162
+ select: ref,
163
+ discriminator,
164
+ cases: Object.fromEntries(branches.map((b) => [b.value, b.stages])),
165
+ };
166
+ }