@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
@@ -5,6 +5,11 @@
5
5
  {
6
6
  "additionalProperties": false,
7
7
  "properties": {
8
+ "cells": {
9
+ "const": true,
10
+ "description": "cells(Model): every cell of a matrix step whose output is the model, mapped with a whole-step ref; the value is [{cell, output}, …] in cell order. Omitted otherwise.",
11
+ "type": "boolean"
12
+ },
8
13
  "model": {
9
14
  "description": "Model name (key of `models`).",
10
15
  "pattern": "^[A-Za-z][A-Za-z0-9_]*$",
@@ -47,6 +52,47 @@
47
52
  "const": "2.0",
48
53
  "type": "string"
49
54
  },
55
+ "codeHash": {
56
+ "description": "Hash of the whole build: the pipeline and its code bundled, plus the harness. Any code change makes a new pipeline version.",
57
+ "pattern": "^sha256:[0-9a-f]{64}$",
58
+ "type": "string"
59
+ },
60
+ "config": {
61
+ "description": "Config entries used by steps, sorted by name (omitted when none). Fields of kind secretRef ({secret: NAME}) name a secret.",
62
+ "items": {
63
+ "additionalProperties": false,
64
+ "properties": {
65
+ "description": {
66
+ "type": "string"
67
+ },
68
+ "name": {
69
+ "description": "Config entry name; a key of the pipeline's config document.",
70
+ "pattern": "^[a-z][a-zA-Z0-9]*$",
71
+ "type": "string"
72
+ },
73
+ "required": {
74
+ "description": "False when the schema accepts a missing value (optional, or a default).",
75
+ "type": "boolean"
76
+ },
77
+ "schema": {
78
+ "additionalProperties": {},
79
+ "description": "z.input view: validates the config document's value for this entry (defaults listed).",
80
+ "propertyNames": {
81
+ "type": "string"
82
+ },
83
+ "type": "object"
84
+ }
85
+ },
86
+ "required": [
87
+ "name",
88
+ "description",
89
+ "required",
90
+ "schema"
91
+ ],
92
+ "type": "object"
93
+ },
94
+ "type": "array"
95
+ },
50
96
  "description": {
51
97
  "type": "string"
52
98
  },
@@ -85,7 +131,7 @@
85
131
  },
86
132
  "harness": {
87
133
  "additionalProperties": false,
88
- "description": "Harness identity. Already folded into every handlerHash.",
134
+ "description": "Harness identity. Already folded into codeHash.",
89
135
  "properties": {
90
136
  "bun": {
91
137
  "description": "Bun runtime version compiled into the binary.",
@@ -129,6 +175,28 @@
129
175
  "minLength": 1,
130
176
  "type": "string"
131
177
  },
178
+ "retention": {
179
+ "additionalProperties": false,
180
+ "description": "Retention overrides for this pipeline (omitted when none). Each field replaces the instance setting of the same meaning.",
181
+ "properties": {
182
+ "buildGraceDays": {
183
+ "maximum": 9007199254740991,
184
+ "minimum": 0,
185
+ "type": "integer"
186
+ },
187
+ "keepLatestRuns": {
188
+ "maximum": 9007199254740991,
189
+ "minimum": 0,
190
+ "type": "integer"
191
+ },
192
+ "minRunAgeDays": {
193
+ "maximum": 9007199254740991,
194
+ "minimum": 0,
195
+ "type": "integer"
196
+ }
197
+ },
198
+ "type": "object"
199
+ },
132
200
  "secrets": {
133
201
  "description": "All declared secrets (steps + poll triggers), sorted by name.",
134
202
  "items": {
@@ -169,6 +237,7 @@
169
237
  "name",
170
238
  "input",
171
239
  "harness",
240
+ "codeHash",
172
241
  "models",
173
242
  "secrets",
174
243
  "triggers",
@@ -220,6 +289,84 @@
220
289
  }
221
290
  ]
222
291
  },
292
+ "Matrix": {
293
+ "additionalProperties": false,
294
+ "description": "A matrix step runs once per cell, named '<step>[<axis>=<value>,…]' in axis order; OLVE_CELL holds the cell as compact JSON.",
295
+ "properties": {
296
+ "axes": {
297
+ "description": "In order: the cells are the cross product, first axis outermost, minus the excludes (1 to 64 cells).",
298
+ "items": {
299
+ "additionalProperties": false,
300
+ "properties": {
301
+ "name": {
302
+ "pattern": "^[a-z][a-zA-Z0-9]{0,30}$",
303
+ "type": "string"
304
+ },
305
+ "values": {
306
+ "description": "Unique per axis.",
307
+ "items": {
308
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$",
309
+ "type": "string"
310
+ },
311
+ "minItems": 1,
312
+ "type": "array"
313
+ }
314
+ },
315
+ "required": [
316
+ "name",
317
+ "values"
318
+ ],
319
+ "type": "object"
320
+ },
321
+ "maxItems": 4,
322
+ "minItems": 1,
323
+ "type": "array"
324
+ },
325
+ "exclude": {
326
+ "description": "Each entry names one or more axes with one of their values; a cell matching every named axis is left out (omitted when none).",
327
+ "items": {
328
+ "additionalProperties": {
329
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$",
330
+ "type": "string"
331
+ },
332
+ "propertyNames": {
333
+ "pattern": "^[a-z][a-zA-Z0-9]{0,30}$",
334
+ "type": "string"
335
+ },
336
+ "type": "object"
337
+ },
338
+ "type": "array"
339
+ },
340
+ "failure": {
341
+ "additionalProperties": false,
342
+ "description": "keepRunning: every cell runs to the end. cascadeFailure: a cell whose last allowed attempt failed cancels the step's other scheduled or running cells.",
343
+ "properties": {
344
+ "kind": {
345
+ "enum": [
346
+ "keepRunning",
347
+ "cascadeFailure"
348
+ ],
349
+ "type": "string"
350
+ }
351
+ },
352
+ "required": [
353
+ "kind"
354
+ ],
355
+ "type": "object"
356
+ },
357
+ "maxParallel": {
358
+ "description": "At most this many of the step's cells running at once in a run.",
359
+ "maximum": 9007199254740991,
360
+ "minimum": 1,
361
+ "type": "integer"
362
+ }
363
+ },
364
+ "required": [
365
+ "axes",
366
+ "failure"
367
+ ],
368
+ "type": "object"
369
+ },
223
370
  "ModelSchemas": {
224
371
  "additionalProperties": false,
225
372
  "properties": {
@@ -297,30 +444,170 @@
297
444
  "stages"
298
445
  ],
299
446
  "type": "object"
447
+ },
448
+ {
449
+ "additionalProperties": false,
450
+ "properties": {
451
+ "cases": {
452
+ "additionalProperties": {
453
+ "items": {
454
+ "$ref": "#/$defs/Stage"
455
+ },
456
+ "type": "array"
457
+ },
458
+ "description": "Discriminator value → stages. Exactly one case runs; a step name may repeat across cases, nowhere else.",
459
+ "propertyNames": {
460
+ "type": "string"
461
+ },
462
+ "type": "object"
463
+ },
464
+ "discriminator": {
465
+ "description": "The union's literal field whose value names the case.",
466
+ "minLength": 1,
467
+ "type": "string"
468
+ },
469
+ "select": {
470
+ "description": "Config reference (`<entry>.<field>…`) to a discriminated union; the run's config snapshot picks the case.",
471
+ "minLength": 1,
472
+ "type": "string"
473
+ }
474
+ },
475
+ "required": [
476
+ "select",
477
+ "discriminator",
478
+ "cases"
479
+ ],
480
+ "type": "object"
300
481
  }
301
482
  ]
302
483
  },
303
484
  "Step": {
304
485
  "additionalProperties": false,
305
486
  "properties": {
487
+ "clusterAccess": {
488
+ "const": true,
489
+ "description": "The step pod gets the instance's step ServiceAccount token (Kubernetes:StepServiceAccount), if the instance grants one. Omitted = no token.",
490
+ "type": "boolean"
491
+ },
306
492
  "config": {
307
493
  "additionalProperties": {
308
494
  "$ref": "#/$defs/Json"
309
495
  },
310
- "description": "Library-step configuration (display only; already covered by handlerHash).",
496
+ "description": "The step's settings (display only). A config reference is recorded as {\"$config\": \"<entry>.<field>…\"}; the entry is in configEntries.",
311
497
  "propertyNames": {
312
498
  "type": "string"
313
499
  },
314
500
  "type": "object"
315
501
  },
502
+ "configEntries": {
503
+ "description": "Config entries the step receives (sorted; omitted when none). The service delivers them in input.json under '$config'.",
504
+ "items": {
505
+ "description": "Config entry name; a key of the pipeline's config document.",
506
+ "pattern": "^[a-z][a-zA-Z0-9]*$",
507
+ "type": "string"
508
+ },
509
+ "type": "array"
510
+ },
316
511
  "description": {
317
512
  "description": "What the step does, shown in the UI (may be empty).",
318
513
  "type": "string"
319
514
  },
320
- "handlerHash": {
321
- "description": "Implementation identity: handler bundle (contract files excluded) + harness (bun + sdkHash); for library steps kind + config + harness.",
322
- "pattern": "^sha256:[0-9a-f]{64}$",
323
- "type": "string"
515
+ "externalInputs": {
516
+ "additionalProperties": {
517
+ "additionalProperties": false,
518
+ "properties": {
519
+ "description": {
520
+ "type": "string"
521
+ },
522
+ "from": {
523
+ "description": "Who or what may answer.",
524
+ "items": {
525
+ "oneOf": [
526
+ {
527
+ "additionalProperties": false,
528
+ "properties": {
529
+ "kind": {
530
+ "const": "person",
531
+ "type": "string"
532
+ }
533
+ },
534
+ "required": [
535
+ "kind"
536
+ ],
537
+ "type": "object"
538
+ },
539
+ {
540
+ "additionalProperties": false,
541
+ "properties": {
542
+ "kind": {
543
+ "const": "callback",
544
+ "type": "string"
545
+ }
546
+ },
547
+ "required": [
548
+ "kind"
549
+ ],
550
+ "type": "object"
551
+ },
552
+ {
553
+ "additionalProperties": false,
554
+ "properties": {
555
+ "intervalSeconds": {
556
+ "maximum": 9007199254740991,
557
+ "minimum": 10,
558
+ "type": "integer"
559
+ },
560
+ "kind": {
561
+ "const": "poll",
562
+ "type": "string"
563
+ },
564
+ "url": {
565
+ "description": "GET while waiting; {run.id} and {run.number} are filled in.",
566
+ "type": "string"
567
+ },
568
+ "valuePath": {
569
+ "description": "Dot path to the value; '.' is the whole body.",
570
+ "minLength": 1,
571
+ "type": "string"
572
+ }
573
+ },
574
+ "required": [
575
+ "kind",
576
+ "url",
577
+ "valuePath",
578
+ "intervalSeconds"
579
+ ],
580
+ "type": "object"
581
+ }
582
+ ]
583
+ },
584
+ "minItems": 1,
585
+ "type": "array"
586
+ },
587
+ "model": {
588
+ "description": "The value is validated against this model's input view.",
589
+ "pattern": "^[A-Za-z][A-Za-z0-9_]*$",
590
+ "type": "string"
591
+ },
592
+ "title": {
593
+ "description": "What is asked, shown to whoever answers.",
594
+ "minLength": 1,
595
+ "type": "string"
596
+ }
597
+ },
598
+ "required": [
599
+ "model",
600
+ "title",
601
+ "description",
602
+ "from"
603
+ ],
604
+ "type": "object"
605
+ },
606
+ "description": "Inputs answered from outside the run (omitted when none): the step waits until every one has a value, which the service writes into input.json under its key.",
607
+ "propertyNames": {
608
+ "type": "string"
609
+ },
610
+ "type": "object"
324
611
  },
325
612
  "image": {
326
613
  "description": "Container image the step runs in.",
@@ -348,16 +635,14 @@
348
635
  "type": "object"
349
636
  },
350
637
  "kind": {
351
- "description": "'custom' = handlers/<name>.ts; others are SDK library steps.",
352
- "enum": [
353
- "custom",
354
- "script",
355
- "gitHubSource",
356
- "imageBuild",
357
- "helmDeployOverSsh"
358
- ],
638
+ "description": "A label for the step's kind: 'custom' unless the step names one.",
639
+ "minLength": 1,
359
640
  "type": "string"
360
641
  },
642
+ "matrix": {
643
+ "$ref": "#/$defs/Matrix",
644
+ "description": "Set on a matrix step (never on a failure handler); omitted otherwise."
645
+ },
361
646
  "name": {
362
647
  "description": "Step name: lower-case kebab, 1-63 chars. The step's identity.",
363
648
  "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
@@ -404,8 +689,7 @@
404
689
  "inputTypes",
405
690
  "input",
406
691
  "output",
407
- "secrets",
408
- "handlerHash"
692
+ "secrets"
409
693
  ],
410
694
  "type": "object"
411
695
  },
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@olivervea/pipelines",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.30",
4
4
  "description": "Olve.Pipelines v2 SDK: typed pipeline authoring, the step harness, and the build tool.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Oliver Vea",
8
- "homepage": "https://github.com/OliverVea/Olve.Pipelines/tree/v2/sdk#readme",
8
+ "homepage": "https://github.com/OliverVea/Olve.Pipelines/tree/main/sdk#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/OliverVea/Olve.Pipelines.git",
12
- "directory": "sdk"
12
+ "directory": "src/sdk"
13
13
  },
14
14
  "bugs": {
15
15
  "url": "https://github.com/OliverVea/Olve.Pipelines/issues"
@@ -50,13 +50,15 @@
50
50
  "test": "bun test",
51
51
  "check-compile-fail": "bun scripts/check-compile-fail.ts",
52
52
  "gen-schema": "bun scripts/gen-manifest-schema.ts",
53
- "check": "tsc --noEmit -p . && bun scripts/check-compile-fail.ts && bun test"
53
+ "check": "prettier --check . && tsc --noEmit -p . && bun scripts/check-compile-fail.ts && bun test",
54
+ "format": "prettier --write ."
54
55
  },
55
56
  "dependencies": {
56
57
  "zod": "4.6.7"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@types/bun": "1.4.3",
61
+ "prettier": "^3.5.0",
60
62
  "typescript": "7.0.2"
61
63
  }
62
64
  }
package/src/assets.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  // `import script from "./scripts/x.sh" with { type: "file" }` yields a path string
2
2
  // (inside the compiled binary it points into the embedded /$bunfs filesystem).
3
- declare module "*.sh" {
3
+ declare module '*.sh' {
4
4
  const path: string;
5
5
  export default path;
6
6
  }
7
- declare module "*.bash" {
7
+ declare module '*.bash' {
8
8
  const path: string;
9
9
  export default path;
10
10
  }
@@ -1,21 +1,29 @@
1
- // olve-build: pipeline dir → single executable + step.tar.gz + manifest.json.
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join, resolve } from "node:path";
4
- import { canonicalJson } from "../util/canonical.ts";
5
- import { sha256File } from "../util/hash.ts";
6
- import type { HarnessInfo } from "../util/handler-hash.ts";
7
- import { writeTarGz } from "../util/tar.ts";
8
- import { MANIFEST_BEGIN, MANIFEST_END, uploadFile } from "../harness/result.ts";
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";
1
+ // olve-build: pipeline dir → single executable + step.tar.zst + manifest.json.
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { canonicalJson } from '../util/canonical.ts';
5
+ import { sha256File } from '../util/hash.ts';
6
+ import type { HarnessInfo } from '../util/code-hash.ts';
7
+ import { writeTarZst } from '../util/tar.ts';
8
+ import { MANIFEST_BEGIN, MANIFEST_END, uploadFile } from '../harness/result.ts';
9
+ import { loaderProblem } from './elf.ts';
10
+ import { codeHash } from './hashes.ts';
11
+ import { harnessInfo, loadProject, relPosix, type Project } from './project.ts';
12
12
 
13
- export const BUILD_FLAGS = ["--no-compile-autoload-dotenv", "--no-compile-autoload-bunfig", "--bytecode", "--format=esm"] as const;
14
- export type Target = "bun-linux-x64" | "bun-linux-x64-musl";
13
+ export const BUILD_FLAGS = [
14
+ '--no-compile-autoload-dotenv',
15
+ '--no-compile-autoload-bunfig',
16
+ '--bytecode',
17
+ '--format=esm',
18
+ ] as const;
19
+ export type Target = 'bun-linux-x64' | 'bun-linux-x64-musl';
15
20
 
16
21
  export class BuildError extends Error {
17
- override name = "BuildError";
18
- constructor(message: string, readonly exitCode = 1) {
22
+ override name = 'BuildError';
23
+ constructor(
24
+ message: string,
25
+ readonly exitCode = 1,
26
+ ) {
19
27
  super(message);
20
28
  }
21
29
  }
@@ -23,7 +31,7 @@ export class BuildError extends Error {
23
31
  export interface BuildOptions {
24
32
  out: string;
25
33
  target?: Target;
26
- /** Pre-signed PUT for step.tar.gz. */
34
+ /** Pre-signed PUT for the step archive. */
27
35
  uploadUrl?: string;
28
36
  /** Print the manifest between sentinels on stdout (for the build Job's pod log). */
29
37
  sentinels?: boolean;
@@ -36,90 +44,116 @@ export interface BuildResult {
36
44
  archive: string;
37
45
  manifestFile: string;
38
46
  manifest: string;
39
- hashes: Record<string, HandlerHash>;
47
+ codeHash: string;
40
48
  harness: HarnessInfo;
41
49
  }
42
50
 
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 {
45
- const gen = join(project.dir, ".olve", "gen");
51
+ /** Writes `<dir>/.olve/gen/entry.ts`, the entry point of the step binary. */
52
+ export function generateEntry(
53
+ project: Project,
54
+ code: string,
55
+ harness: HarnessInfo,
56
+ ): string {
57
+ const gen = join(project.dir, '.olve', 'gen');
46
58
  mkdirSync(gen, { recursive: true });
47
- const registry = project.handlers.map(([name, file]) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(relPosix(gen, file))}),`).join("\n");
48
- writeFileSync(
49
- join(gen, "handlers.ts"),
50
- `// GENERATED by olve-build — do not edit.\n` +
51
- `import type { HandlerRegistry, HarnessInfo } from "@olivervea/pipelines/harness";\n\n` +
52
- `export const handlers: HandlerRegistry = {\n${registry}\n};\n\n` +
53
- `export const handlerHashes: Record<string, string> = ${JSON.stringify(hashes, null, 2)};\n\n` +
54
- `export const harness: HarnessInfo = ${JSON.stringify(harness, null, 2)};\n`,
55
- );
56
- const entry = join(gen, "entry.ts");
59
+ const entry = join(gen, 'entry.ts');
60
+ const config = `{ pipeline, codeHash: ${JSON.stringify(code)}, harness: ${JSON.stringify(harness)} }`;
57
61
  writeFileSync(
58
62
  entry,
59
63
  `// GENERATED by olve-build — do not edit. Entry point of the step binary.\n` +
60
64
  `import pipeline from ${JSON.stringify(relPosix(gen, project.pipelineFile))};\n` +
61
- `import { runHarness } from "@olivervea/pipelines/harness";\n` +
62
- `import { handlers, handlerHashes, harness } from "./handlers.ts";\n\n` +
63
- `process.exit(await runHarness({ pipeline, handlers, handlerHashes, harness }, process.argv.slice(2)));\n`,
65
+ `import { runHarness } from "@olivervea/pipelines/harness";\n\n` +
66
+ `process.exit(await runHarness(${config}, process.argv.slice(2)));\n`,
64
67
  );
65
68
  return entry;
66
69
  }
67
70
 
68
71
  export async function build(dir: string, opts: BuildOptions): Promise<BuildResult> {
69
- const log = opts.log ?? ((l: string) => process.stderr.write(l + "\n"));
70
- const stdout = opts.stdout ?? (async (t: string) => void (await Bun.write(Bun.stdout, t)));
71
- const target = opts.target ?? "bun-linux-x64";
72
+ const log = opts.log ?? ((l: string) => process.stderr.write(l + '\n'));
73
+ const stdout =
74
+ opts.stdout ?? (async (t: string) => void (await Bun.write(Bun.stdout, t)));
75
+ const target = opts.target ?? 'bun-linux-x64';
72
76
  const project = await loadProject(dir);
73
77
  const out = resolve(opts.out);
74
78
  mkdirSync(out, { recursive: true });
75
79
  const harness = harnessInfo();
76
80
 
77
81
  const t0 = performance.now();
78
- const hashes = await handlerHashes(project, harness);
79
- const entry = generateEntry(project, Object.fromEntries(Object.entries(hashes).map(([k, v]) => [k, v.handlerHash])), harness);
80
- log(`olve-build: ${project.handlers.length} handler(s) hashed in ${((performance.now() - t0) / 1000).toFixed(2)}s`);
82
+ const code = await codeHash(project, harness);
83
+ const entry = generateEntry(project, code, harness);
84
+ log(`olve-build: hashed the code in ${((performance.now() - t0) / 1000).toFixed(2)}s`);
81
85
 
82
86
  // 1. compile
83
- const binary = join(out, "step");
87
+ const binary = join(out, 'step');
84
88
  rmSync(binary, { force: true });
85
89
  const t1 = performance.now();
86
- const p = Bun.spawnSync([process.execPath, "build", "--compile", `--target=${target}`, ...BUILD_FLAGS, entry, `--outfile=${binary}`], {
87
- cwd: project.dir,
88
- stdout: "pipe",
89
- stderr: "pipe",
90
- env: { ...process.env, NO_COLOR: "1" },
91
- });
90
+ const p = Bun.spawnSync(
91
+ [
92
+ process.execPath,
93
+ 'build',
94
+ '--compile',
95
+ `--target=${target}`,
96
+ ...BUILD_FLAGS,
97
+ entry,
98
+ `--outfile=${binary}`,
99
+ ],
100
+ {
101
+ cwd: project.dir,
102
+ stdout: 'pipe',
103
+ stderr: 'pipe',
104
+ env: { ...process.env, NO_COLOR: '1' },
105
+ },
106
+ );
92
107
  if (p.exitCode !== 0 || !existsSync(binary))
93
- throw new BuildError(`bun build --compile failed (exit ${p.exitCode}):\n${p.stdout.toString()}${p.stderr.toString()}`);
94
- log(`olve-build: compiled ${target} in ${((performance.now() - t1) / 1000).toFixed(2)}s`);
108
+ throw new BuildError(
109
+ `bun build --compile failed (exit ${p.exitCode}):\n${p.stdout.toString()}${p.stderr.toString()}`,
110
+ );
111
+ log(
112
+ `olve-build: compiled ${target} in ${((performance.now() - t1) / 1000).toFixed(2)}s`,
113
+ );
95
114
 
96
- // 2. loader check: the harness launches the binary through /olve/lib/<loader>; a musl binary
97
- // (e.g. compiled on alpine, where bun-linux-x64 silently yields musl) would not start.
98
- const expected = target === "bun-linux-x64" ? GLIBC_INTERP : MUSL_INTERP;
99
- const interp = readInterpreter(binary);
100
- if (interp !== expected)
101
- throw new BuildError(`compiled binary has PT_INTERP ${interp ?? "(none)"}, expected ${expected} — compile on a glibc Bun image (oven/bun:<version>-slim), not alpine`);
115
+ // 2. loader check: on alpine, bun-linux-x64 silently yields a musl binary, which would not start.
116
+ const problem = loaderProblem(binary, target);
117
+ if (problem) throw new BuildError(problem);
102
118
 
103
119
  // 3. describe (runs the real binary: the manifest is exactly what the step pods will see)
104
- const d = Bun.spawnSync([binary, "describe"], { cwd: out, stdout: "pipe", stderr: "pipe", env: { PATH: process.env.PATH ?? "/usr/bin:/bin" } });
105
- if (d.exitCode !== 0) throw new BuildError(`describe failed (exit ${d.exitCode}):\n${d.stderr.toString()}`, d.exitCode ?? 1);
120
+ const d = Bun.spawnSync([binary, 'describe'], {
121
+ cwd: out,
122
+ stdout: 'pipe',
123
+ stderr: 'pipe',
124
+ env: { PATH: process.env.PATH ?? '/usr/bin:/bin' },
125
+ });
126
+ if (d.exitCode !== 0)
127
+ throw new BuildError(
128
+ `describe failed (exit ${d.exitCode}):\n${d.stderr.toString()}`,
129
+ d.exitCode ?? 1,
130
+ );
106
131
  const manifest = d.stdout.toString();
107
- const manifestFile = join(out, "manifest.json");
132
+ const manifestFile = join(out, 'manifest.json');
108
133
  writeFileSync(manifestFile, manifest);
109
134
 
110
- // 4. step.tar.gz (deterministic: `step` at the root, 0755, mtime 0)
111
- const archive = join(out, "step.tar.gz");
112
- await writeTarGz(archive, [{ path: "step", type: "file", mode: 0o755, source: binary }]);
135
+ // 4. step.tar.zst (deterministic: `step` at the root, 0755, mtime 0)
136
+ const archive = join(out, 'step.tar.zst');
137
+ await writeTarZst(archive, [
138
+ { path: 'step', type: 'file', mode: 0o755, source: binary },
139
+ ]);
113
140
  writeFileSync(
114
- join(out, "build.json"),
115
- canonicalJson({ target, flags: BUILD_FLAGS, harness, handlers: hashes, binarySha256: sha256File(binary), archiveSha256: sha256File(archive) }),
141
+ join(out, 'build.json'),
142
+ canonicalJson({
143
+ target,
144
+ flags: BUILD_FLAGS,
145
+ harness,
146
+ codeHash: code,
147
+ binarySha256: sha256File(binary),
148
+ archiveSha256: sha256File(archive),
149
+ }),
116
150
  );
117
151
  log(`olve-build: wrote ${archive}`);
118
152
 
119
153
  if (opts.uploadUrl) {
120
154
  await uploadFile(opts.uploadUrl, archive, log);
121
- log("olve-build: uploaded step.tar.gz");
155
+ log('olve-build: uploaded step.tar.zst');
122
156
  }
123
157
  if (opts.sentinels) await stdout(`${MANIFEST_BEGIN}\n${manifest}${MANIFEST_END}\n`);
124
- return { binary, archive, manifestFile, manifest, hashes, harness };
158
+ return { binary, archive, manifestFile, manifest, codeHash: code, harness };
125
159
  }