@olivervea/pipelines 2.0.0-beta.1 → 2.0.0-beta.10
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 +121 -61
- package/manifest.schema.json +161 -11
- package/package.json +1 -1
- package/src/build/build.ts +19 -17
- package/src/build/cli.ts +1 -1
- package/src/build/hashes.ts +32 -3
- package/src/build/project.ts +33 -5
- package/src/builder/pipeline.ts +92 -10
- package/src/core/brands.ts +2 -0
- package/src/core/config-refs.ts +77 -0
- package/src/core/config.ts +147 -0
- package/src/core/handlers.ts +16 -10
- package/src/core/origin.ts +20 -0
- package/src/core/steps.ts +53 -22
- package/src/core/webhooks.ts +30 -0
- package/src/ctx/index.ts +3 -0
- package/src/ctx/sh.ts +2 -1
- package/src/ctx/types.ts +4 -1
- package/src/describe/describe.ts +213 -27
- package/src/describe/manifest-schema.ts +53 -5
- package/src/dev/cli.ts +16 -9
- package/src/harness/execute.ts +7 -4
- package/src/harness/index.ts +1 -1
- package/src/harness/main.ts +68 -17
- package/src/harness/validate.ts +19 -1
- package/src/index.ts +7 -3
- package/src/library/checks.ts +23 -0
- package/src/library/compose-deploy.ts +96 -0
- package/src/library/github-source.ts +61 -38
- package/src/library/helm-deploy-ssh.ts +306 -99
- package/src/library/helm-deploy.ts +271 -0
- package/src/library/image-build.ts +350 -66
- package/src/library/index.ts +11 -7
- package/src/library/models.ts +17 -1
- package/src/library/ssh.ts +50 -0
- package/src/testing/index.ts +11 -15
- package/src/util/handler-hash.ts +4 -4
- package/src/util/tar.ts +13 -0
- package/src/library/script.ts +0 -85
package/manifest.schema.json
CHANGED
|
@@ -47,6 +47,42 @@
|
|
|
47
47
|
"const": "2.0",
|
|
48
48
|
"type": "string"
|
|
49
49
|
},
|
|
50
|
+
"config": {
|
|
51
|
+
"description": "Config entries used by steps, sorted by name (omitted when none). Fields of kind secretRef ({secret: NAME}) name a secret.",
|
|
52
|
+
"items": {
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"properties": {
|
|
55
|
+
"description": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"name": {
|
|
59
|
+
"description": "Config entry name; a key of the pipeline's config document.",
|
|
60
|
+
"pattern": "^[a-z][a-zA-Z0-9]*$",
|
|
61
|
+
"type": "string"
|
|
62
|
+
},
|
|
63
|
+
"required": {
|
|
64
|
+
"description": "False when the schema accepts a missing value (optional, or a default).",
|
|
65
|
+
"type": "boolean"
|
|
66
|
+
},
|
|
67
|
+
"schema": {
|
|
68
|
+
"additionalProperties": {},
|
|
69
|
+
"description": "z.input view: validates the config document's value for this entry (defaults listed).",
|
|
70
|
+
"propertyNames": {
|
|
71
|
+
"type": "string"
|
|
72
|
+
},
|
|
73
|
+
"type": "object"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"required": [
|
|
77
|
+
"name",
|
|
78
|
+
"description",
|
|
79
|
+
"required",
|
|
80
|
+
"schema"
|
|
81
|
+
],
|
|
82
|
+
"type": "object"
|
|
83
|
+
},
|
|
84
|
+
"type": "array"
|
|
85
|
+
},
|
|
50
86
|
"description": {
|
|
51
87
|
"type": "string"
|
|
52
88
|
},
|
|
@@ -92,7 +128,7 @@
|
|
|
92
128
|
"type": "string"
|
|
93
129
|
},
|
|
94
130
|
"sdk": {
|
|
95
|
-
"description": "@
|
|
131
|
+
"description": "@olivervea/pipelines package version.",
|
|
96
132
|
"type": "string"
|
|
97
133
|
},
|
|
98
134
|
"sdkHash": {
|
|
@@ -297,28 +333,76 @@
|
|
|
297
333
|
"stages"
|
|
298
334
|
],
|
|
299
335
|
"type": "object"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"additionalProperties": false,
|
|
339
|
+
"properties": {
|
|
340
|
+
"cases": {
|
|
341
|
+
"additionalProperties": {
|
|
342
|
+
"items": {
|
|
343
|
+
"$ref": "#/$defs/Stage"
|
|
344
|
+
},
|
|
345
|
+
"type": "array"
|
|
346
|
+
},
|
|
347
|
+
"description": "Discriminator value → stages. Exactly one case runs; a step name may repeat across cases, nowhere else.",
|
|
348
|
+
"propertyNames": {
|
|
349
|
+
"type": "string"
|
|
350
|
+
},
|
|
351
|
+
"type": "object"
|
|
352
|
+
},
|
|
353
|
+
"discriminator": {
|
|
354
|
+
"description": "The union's literal field whose value names the case.",
|
|
355
|
+
"minLength": 1,
|
|
356
|
+
"type": "string"
|
|
357
|
+
},
|
|
358
|
+
"select": {
|
|
359
|
+
"description": "Config reference (`<entry>.<field>…`) to a discriminated union; the run's config snapshot picks the case.",
|
|
360
|
+
"minLength": 1,
|
|
361
|
+
"type": "string"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"required": [
|
|
365
|
+
"select",
|
|
366
|
+
"discriminator",
|
|
367
|
+
"cases"
|
|
368
|
+
],
|
|
369
|
+
"type": "object"
|
|
300
370
|
}
|
|
301
371
|
]
|
|
302
372
|
},
|
|
303
373
|
"Step": {
|
|
304
374
|
"additionalProperties": false,
|
|
305
375
|
"properties": {
|
|
376
|
+
"clusterAccess": {
|
|
377
|
+
"const": true,
|
|
378
|
+
"description": "The step pod gets the instance's step ServiceAccount token (Kubernetes:StepServiceAccount), if the instance grants one. Omitted = no token.",
|
|
379
|
+
"type": "boolean"
|
|
380
|
+
},
|
|
306
381
|
"config": {
|
|
307
382
|
"additionalProperties": {
|
|
308
383
|
"$ref": "#/$defs/Json"
|
|
309
384
|
},
|
|
310
|
-
"description": "
|
|
385
|
+
"description": "The step's settings (display only; already covered by handlerHash). A config reference is recorded as {\"$config\": \"<entry>.<field>…\"}; the entry is in configEntries.",
|
|
311
386
|
"propertyNames": {
|
|
312
387
|
"type": "string"
|
|
313
388
|
},
|
|
314
389
|
"type": "object"
|
|
315
390
|
},
|
|
391
|
+
"configEntries": {
|
|
392
|
+
"description": "Config entries the step receives (sorted; omitted when none). The service delivers them in input.json under '$config'.",
|
|
393
|
+
"items": {
|
|
394
|
+
"description": "Config entry name; a key of the pipeline's config document.",
|
|
395
|
+
"pattern": "^[a-z][a-zA-Z0-9]*$",
|
|
396
|
+
"type": "string"
|
|
397
|
+
},
|
|
398
|
+
"type": "array"
|
|
399
|
+
},
|
|
316
400
|
"description": {
|
|
317
401
|
"description": "What the step does, shown in the UI (may be empty).",
|
|
318
402
|
"type": "string"
|
|
319
403
|
},
|
|
320
404
|
"handlerHash": {
|
|
321
|
-
"description": "Implementation identity: handler bundle (contract files excluded) + harness (bun + sdkHash); for
|
|
405
|
+
"description": "Implementation identity: handler bundle (contract files excluded) + harness (bun + sdkHash); for a step that carries its implementation kind + config + harness + the hash of the code of its kind.",
|
|
322
406
|
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
323
407
|
"type": "string"
|
|
324
408
|
},
|
|
@@ -348,14 +432,8 @@
|
|
|
348
432
|
"type": "object"
|
|
349
433
|
},
|
|
350
434
|
"kind": {
|
|
351
|
-
"description": "'custom' = handlers
|
|
352
|
-
"
|
|
353
|
-
"custom",
|
|
354
|
-
"script",
|
|
355
|
-
"gitHubSource",
|
|
356
|
-
"imageBuild",
|
|
357
|
-
"helmDeployOverSsh"
|
|
358
|
-
],
|
|
435
|
+
"description": "'custom' = the handler is a handlers/*.ts file; any other kind is a step that carries its implementation (an SDK or package step). A label: the service accepts any kind.",
|
|
436
|
+
"minLength": 1,
|
|
359
437
|
"type": "string"
|
|
360
438
|
},
|
|
361
439
|
"name": {
|
|
@@ -483,6 +561,78 @@
|
|
|
483
561
|
"intervalSeconds"
|
|
484
562
|
],
|
|
485
563
|
"type": "object"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"additionalProperties": false,
|
|
567
|
+
"description": "Inbound HTTP delivery to /api/hooks/<pipeline id>/<name>. Event = {headers, body}; a null mapping ignores it.",
|
|
568
|
+
"properties": {
|
|
569
|
+
"kind": {
|
|
570
|
+
"const": "webhook",
|
|
571
|
+
"type": "string"
|
|
572
|
+
},
|
|
573
|
+
"name": {
|
|
574
|
+
"pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
|
|
575
|
+
"type": "string"
|
|
576
|
+
},
|
|
577
|
+
"verify": {
|
|
578
|
+
"oneOf": [
|
|
579
|
+
{
|
|
580
|
+
"additionalProperties": false,
|
|
581
|
+
"properties": {
|
|
582
|
+
"header": {
|
|
583
|
+
"minLength": 1,
|
|
584
|
+
"type": "string"
|
|
585
|
+
},
|
|
586
|
+
"prefix": {
|
|
587
|
+
"description": "Text before the hex digest in the header, e.g. 'sha256='.",
|
|
588
|
+
"type": "string"
|
|
589
|
+
},
|
|
590
|
+
"scheme": {
|
|
591
|
+
"const": "hmac-sha256",
|
|
592
|
+
"type": "string"
|
|
593
|
+
},
|
|
594
|
+
"secret": {
|
|
595
|
+
"description": "Secret name; also the env var name in the step container.",
|
|
596
|
+
"pattern": "^[A-Z_][A-Z0-9_]*$",
|
|
597
|
+
"type": "string"
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
"required": [
|
|
601
|
+
"scheme",
|
|
602
|
+
"secret",
|
|
603
|
+
"header",
|
|
604
|
+
"prefix"
|
|
605
|
+
],
|
|
606
|
+
"type": "object"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"additionalProperties": false,
|
|
610
|
+
"properties": {
|
|
611
|
+
"scheme": {
|
|
612
|
+
"const": "bearer",
|
|
613
|
+
"type": "string"
|
|
614
|
+
},
|
|
615
|
+
"secret": {
|
|
616
|
+
"description": "Secret name; also the env var name in the step container.",
|
|
617
|
+
"pattern": "^[A-Z_][A-Z0-9_]*$",
|
|
618
|
+
"type": "string"
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
"required": [
|
|
622
|
+
"scheme",
|
|
623
|
+
"secret"
|
|
624
|
+
],
|
|
625
|
+
"type": "object"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
"required": [
|
|
631
|
+
"kind",
|
|
632
|
+
"name",
|
|
633
|
+
"verify"
|
|
634
|
+
],
|
|
635
|
+
"type": "object"
|
|
486
636
|
}
|
|
487
637
|
]
|
|
488
638
|
},
|
package/package.json
CHANGED
package/src/build/build.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// olve-build: pipeline dir → single executable + step.tar.
|
|
1
|
+
// olve-build: pipeline dir → single executable + step.tar.zst + manifest.json.
|
|
2
2
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { canonicalJson } from "../util/canonical.ts";
|
|
5
5
|
import { sha256File } from "../util/hash.ts";
|
|
6
6
|
import type { HarnessInfo } from "../util/handler-hash.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { writeTarZst } from "../util/tar.ts";
|
|
8
8
|
import { MANIFEST_BEGIN, MANIFEST_END, uploadFile } from "../harness/result.ts";
|
|
9
9
|
import { GLIBC_INTERP, MUSL_INTERP, readInterpreter } from "./elf.ts";
|
|
10
|
-
import { handlerHashes, type HandlerHash } from "./hashes.ts";
|
|
11
|
-
import { harnessInfo, loadProject, relPosix, type Project } from "./project.ts";
|
|
10
|
+
import { handlerHashes, kindHashes, type HandlerHash } from "./hashes.ts";
|
|
11
|
+
import { harnessInfo, loadPipeline, loadProject, relPosix, type Project } from "./project.ts";
|
|
12
12
|
|
|
13
13
|
export const BUILD_FLAGS = ["--no-compile-autoload-dotenv", "--no-compile-autoload-bunfig", "--bytecode", "--format=esm"] as const;
|
|
14
14
|
export type Target = "bun-linux-x64" | "bun-linux-x64-musl";
|
|
@@ -23,7 +23,7 @@ export class BuildError extends Error {
|
|
|
23
23
|
export interface BuildOptions {
|
|
24
24
|
out: string;
|
|
25
25
|
target?: Target;
|
|
26
|
-
/** Pre-signed PUT for step.
|
|
26
|
+
/** Pre-signed PUT for the step archive. */
|
|
27
27
|
uploadUrl?: string;
|
|
28
28
|
/** Print the manifest between sentinels on stdout (for the build Job's pod log). */
|
|
29
29
|
sentinels?: boolean;
|
|
@@ -41,16 +41,17 @@ export interface BuildResult {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Writes `<dir>/.olve/gen/{handlers,entry}.ts`: the literal-import handler registry + binary entry. */
|
|
44
|
-
export function generateEntry(project: Project, hashes: Record<string, string>, harness: HarnessInfo): string {
|
|
44
|
+
export function generateEntry(project: Project, hashes: Record<string, string>, harness: HarnessInfo, kinds: Record<string, string> = {}): string {
|
|
45
45
|
const gen = join(project.dir, ".olve", "gen");
|
|
46
46
|
mkdirSync(gen, { recursive: true });
|
|
47
47
|
const registry = project.handlers.map(([name, file]) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(relPosix(gen, file))}),`).join("\n");
|
|
48
48
|
writeFileSync(
|
|
49
49
|
join(gen, "handlers.ts"),
|
|
50
50
|
`// GENERATED by olve-build — do not edit.\n` +
|
|
51
|
-
`import type { HandlerRegistry, HarnessInfo } from "@
|
|
51
|
+
`import type { HandlerRegistry, HarnessInfo } from "@olivervea/pipelines/harness";\n\n` +
|
|
52
52
|
`export const handlers: HandlerRegistry = {\n${registry}\n};\n\n` +
|
|
53
53
|
`export const handlerHashes: Record<string, string> = ${JSON.stringify(hashes, null, 2)};\n\n` +
|
|
54
|
+
`export const kindHashes: Record<string, string> = ${JSON.stringify(kinds, null, 2)};\n\n` +
|
|
54
55
|
`export const harness: HarnessInfo = ${JSON.stringify(harness, null, 2)};\n`,
|
|
55
56
|
);
|
|
56
57
|
const entry = join(gen, "entry.ts");
|
|
@@ -58,9 +59,9 @@ export function generateEntry(project: Project, hashes: Record<string, string>,
|
|
|
58
59
|
entry,
|
|
59
60
|
`// GENERATED by olve-build — do not edit. Entry point of the step binary.\n` +
|
|
60
61
|
`import pipeline from ${JSON.stringify(relPosix(gen, project.pipelineFile))};\n` +
|
|
61
|
-
`import { runHarness } from "@
|
|
62
|
-
`import { handlers, handlerHashes, harness } from "./handlers.ts";\n\n` +
|
|
63
|
-
`process.exit(await runHarness({ pipeline, handlers, handlerHashes, harness }, process.argv.slice(2)));\n`,
|
|
62
|
+
`import { runHarness } from "@olivervea/pipelines/harness";\n` +
|
|
63
|
+
`import { handlers, handlerHashes, kindHashes, harness } from "./handlers.ts";\n\n` +
|
|
64
|
+
`process.exit(await runHarness({ pipeline, handlers, handlerHashes, kindHashes, harness }, process.argv.slice(2)));\n`,
|
|
64
65
|
);
|
|
65
66
|
return entry;
|
|
66
67
|
}
|
|
@@ -69,14 +70,15 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
|
|
|
69
70
|
const log = opts.log ?? ((l: string) => process.stderr.write(l + "\n"));
|
|
70
71
|
const stdout = opts.stdout ?? (async (t: string) => void (await Bun.write(Bun.stdout, t)));
|
|
71
72
|
const target = opts.target ?? "bun-linux-x64";
|
|
72
|
-
const project = loadProject(dir);
|
|
73
|
+
const project = await loadProject(dir);
|
|
73
74
|
const out = resolve(opts.out);
|
|
74
75
|
mkdirSync(out, { recursive: true });
|
|
75
76
|
const harness = harnessInfo();
|
|
76
77
|
|
|
77
78
|
const t0 = performance.now();
|
|
78
79
|
const hashes = await handlerHashes(project, harness);
|
|
79
|
-
const
|
|
80
|
+
const kinds = await kindHashes(project, await loadPipeline(project));
|
|
81
|
+
const entry = generateEntry(project, Object.fromEntries(Object.entries(hashes).map(([k, v]) => [k, v.handlerHash])), harness, kinds);
|
|
80
82
|
log(`olve-build: ${project.handlers.length} handler(s) hashed in ${((performance.now() - t0) / 1000).toFixed(2)}s`);
|
|
81
83
|
|
|
82
84
|
// 1. compile
|
|
@@ -107,18 +109,18 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
|
|
|
107
109
|
const manifestFile = join(out, "manifest.json");
|
|
108
110
|
writeFileSync(manifestFile, manifest);
|
|
109
111
|
|
|
110
|
-
// 4. step.tar.
|
|
111
|
-
const archive = join(out, "step.tar.
|
|
112
|
-
await
|
|
112
|
+
// 4. step.tar.zst (deterministic: `step` at the root, 0755, mtime 0)
|
|
113
|
+
const archive = join(out, "step.tar.zst");
|
|
114
|
+
await writeTarZst(archive, [{ path: "step", type: "file", mode: 0o755, source: binary }]);
|
|
113
115
|
writeFileSync(
|
|
114
116
|
join(out, "build.json"),
|
|
115
|
-
canonicalJson({ target, flags: BUILD_FLAGS, harness, handlers: hashes, binarySha256: sha256File(binary), archiveSha256: sha256File(archive) }),
|
|
117
|
+
canonicalJson({ target, flags: BUILD_FLAGS, harness, handlers: hashes, kinds, binarySha256: sha256File(binary), archiveSha256: sha256File(archive) }),
|
|
116
118
|
);
|
|
117
119
|
log(`olve-build: wrote ${archive}`);
|
|
118
120
|
|
|
119
121
|
if (opts.uploadUrl) {
|
|
120
122
|
await uploadFile(opts.uploadUrl, archive, log);
|
|
121
|
-
log("olve-build: uploaded step.tar.
|
|
123
|
+
log("olve-build: uploaded step.tar.zst");
|
|
122
124
|
}
|
|
123
125
|
if (opts.sentinels) await stdout(`${MANIFEST_BEGIN}\n${manifest}${MANIFEST_END}\n`);
|
|
124
126
|
return { binary, archive, manifestFile, manifest, hashes, harness };
|
package/src/build/cli.ts
CHANGED
|
@@ -32,7 +32,7 @@ async function main(): Promise<number> {
|
|
|
32
32
|
const dir = positionals[0]!;
|
|
33
33
|
try {
|
|
34
34
|
if (values["hash-only"]) {
|
|
35
|
-
const h = await handlerHashes(loadProject(dir), harnessInfo());
|
|
35
|
+
const h = await handlerHashes(await loadProject(dir), harnessInfo());
|
|
36
36
|
await Bun.write(Bun.stdout, canonicalJson({ harness: harnessInfo(), handlers: h }));
|
|
37
37
|
return 0;
|
|
38
38
|
}
|
package/src/build/hashes.ts
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
// (tree-shaken → function-level granularity; embedded scripts are bundle outputs too), then hash the
|
|
3
3
|
// outputs. Contract changes are versioned through the manifest's model/step entries, SDK changes
|
|
4
4
|
// through `harness`; both are combined in composeHandlerHash.
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import type { BunPlugin } from "bun";
|
|
7
|
+
import { isImplementation } from "../core/handlers.ts";
|
|
8
|
+
import { originsOf } from "../core/origin.ts";
|
|
9
|
+
import { pipelineData } from "../builder/pipeline.ts";
|
|
10
|
+
import { allSteps } from "../harness/main.ts";
|
|
7
11
|
import { composeHandlerHash, type HarnessInfo } from "../util/handler-hash.ts";
|
|
8
12
|
import { sha256Hex } from "../util/hash.ts";
|
|
9
13
|
import type { Project } from "./project.ts";
|
|
@@ -22,7 +26,7 @@ function externals(project: Project): BunPlugin {
|
|
|
22
26
|
return {
|
|
23
27
|
name: "olve-handler-hash-externals",
|
|
24
28
|
setup(b) {
|
|
25
|
-
b.onResolve({ filter: /^@
|
|
29
|
+
b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({ path: args.path, external: true }));
|
|
26
30
|
b.onResolve({ filter: /^\.{1,2}\// }, (args) => {
|
|
27
31
|
const base = resolve(dirname(args.importer), args.path);
|
|
28
32
|
for (const cand of [base, `${base}.ts`, join(base, "index.ts")]) if (contracts.has(cand)) return { path: args.path, external: true };
|
|
@@ -33,11 +37,12 @@ function externals(project: Project): BunPlugin {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export async function bundleHash(project: Project, entry: string): Promise<{ hash: string; outputs: string[] }> {
|
|
40
|
+
const inside = !relative(project.dir, entry).startsWith("..");
|
|
36
41
|
const r = await Bun.build({
|
|
37
42
|
entrypoints: [entry],
|
|
38
43
|
target: "bun",
|
|
39
44
|
format: "esm",
|
|
40
|
-
root: project.dir,
|
|
45
|
+
root: inside ? project.dir : dirname(entry),
|
|
41
46
|
naming: { entry: "[dir]/[name].[ext]", chunk: "[dir]/[name].[ext]", asset: "[dir]/[name].[ext]" },
|
|
42
47
|
plugins: [externals(project)],
|
|
43
48
|
throw: false,
|
|
@@ -65,3 +70,27 @@ export async function handlerHashes(project: Project, harness: HarnessInfo): Pro
|
|
|
65
70
|
}
|
|
66
71
|
return out;
|
|
67
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Kind → hash of the modules that implement the steps of that kind which carry their implementation
|
|
76
|
+
* (each module bundled like a handler: the SDK and contract files external).
|
|
77
|
+
*/
|
|
78
|
+
export async function kindHashes(project: Project, pipeline: unknown): Promise<Record<string, string>> {
|
|
79
|
+
const origins = new Map<string, Set<string>>();
|
|
80
|
+
for (const s of allSteps(pipelineData(pipeline))) {
|
|
81
|
+
if (!isImplementation(s)) continue;
|
|
82
|
+
const files = originsOf(s).filter((f) => !project.contractFiles.includes(f));
|
|
83
|
+
if (files.length === 0)
|
|
84
|
+
throw new Error(`step '${s.name}' carries its implementation but is defined in ${project.contractFiles.map((f) => relative(project.dir, f)).join(" or ")}; define and implement it in a module of its own (or a package)`);
|
|
85
|
+
const set = origins.get(s.kind) ?? new Set<string>();
|
|
86
|
+
for (const f of files) set.add(f);
|
|
87
|
+
origins.set(s.kind, set);
|
|
88
|
+
}
|
|
89
|
+
const out: Record<string, string> = {};
|
|
90
|
+
for (const [kind, files] of [...origins].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
for (const f of [...files].sort()) parts.push((await bundleHash(project, f)).hash);
|
|
93
|
+
out[kind] = "sha256:" + sha256Hex(parts.join("\n"));
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
package/src/build/project.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Layout of a pipeline directory and the identity of the SDK building it.
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { isImplementation } from "../core/handlers.ts";
|
|
4
5
|
import { sha256Hex } from "../util/hash.ts";
|
|
5
6
|
import type { HarnessInfo } from "../util/handler-hash.ts";
|
|
6
7
|
|
|
@@ -11,7 +12,7 @@ export interface Project {
|
|
|
11
12
|
pipelineFile: string;
|
|
12
13
|
/** Contract files (excluded from handler bundles): pipeline.ts, models.ts. */
|
|
13
14
|
contractFiles: string[];
|
|
14
|
-
/** step name → absolute handler file, sorted by name. */
|
|
15
|
+
/** step name → absolute handler file, sorted by step name. The step is the one the file's `implement(step, …)` names. */
|
|
15
16
|
handlers: [string, string][];
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -19,20 +20,47 @@ export class ProjectError extends Error {
|
|
|
19
20
|
override name = "ProjectError";
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export function loadProject(dirArg: string): Project {
|
|
23
|
+
export async function loadProject(dirArg: string): Promise<Project> {
|
|
23
24
|
const dir = resolve(dirArg);
|
|
24
25
|
if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new ProjectError(`${dirArg} is not a directory`);
|
|
25
26
|
const pipelineFile = join(dir, "pipeline.ts");
|
|
26
27
|
if (!existsSync(pipelineFile)) throw new ProjectError(`${join(dirArg, "pipeline.ts")} not found (it must default-export pipeline({...}))`);
|
|
27
28
|
const contractFiles = ["pipeline.ts", "models.ts"].map((f) => join(dir, f)).filter((f) => existsSync(f));
|
|
28
29
|
const hdir = join(dir, "handlers");
|
|
29
|
-
const
|
|
30
|
+
const files = existsSync(hdir)
|
|
30
31
|
? readdirSync(hdir)
|
|
31
32
|
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && statSync(join(hdir, f)).isFile())
|
|
32
33
|
.sort()
|
|
33
|
-
.map((f) => [f.slice(0, -3), join(hdir, f)])
|
|
34
34
|
: [];
|
|
35
|
-
return { dir, pipelineFile, contractFiles, handlers };
|
|
35
|
+
return { dir, pipelineFile, contractFiles, handlers: await resolveHandlers(hdir, files) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The default export of pipeline.ts. */
|
|
39
|
+
export async function loadPipeline(project: Project): Promise<unknown> {
|
|
40
|
+
const mod = (await import(project.pipelineFile)) as { default?: unknown };
|
|
41
|
+
if (!mod.default) throw new ProjectError(`${project.pipelineFile} has no default export (export default pipeline({...})...)`);
|
|
42
|
+
return mod.default;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pairs each handler file with the step its default export implements (file names don't matter). */
|
|
46
|
+
async function resolveHandlers(hdir: string, files: string[]): Promise<[string, string][]> {
|
|
47
|
+
const byStep = new Map<string, string>();
|
|
48
|
+
for (const f of files) {
|
|
49
|
+
const file = join(hdir, f);
|
|
50
|
+
let impl: unknown;
|
|
51
|
+
try {
|
|
52
|
+
impl = ((await import(file)) as { default?: unknown }).default;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new ProjectError(`handlers/${f} failed to load: ${(e as Error)?.message ?? String(e)}`);
|
|
55
|
+
}
|
|
56
|
+
if (!isImplementation(impl))
|
|
57
|
+
throw new ProjectError(`handlers/${f} must default-export implement(<step>, …); put shared code in a subfolder such as handlers/lib/`);
|
|
58
|
+
const step = (impl.step as { name: string }).name;
|
|
59
|
+
const other = byStep.get(step);
|
|
60
|
+
if (other) throw new ProjectError(`handlers/${other.slice(hdir.length + 1)} and handlers/${f} both implement step '${step}'`);
|
|
61
|
+
byStep.set(step, file);
|
|
62
|
+
}
|
|
63
|
+
return [...byStep].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
function listFiles(dir: string): string[] {
|