@olivervea/pipelines 2.0.0-beta.2 → 2.0.0-beta.20
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 +125 -62
- package/manifest.schema.json +208 -11
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +121 -51
- package/src/build/cli.ts +29 -22
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +82 -20
- package/src/build/project.ts +95 -28
- package/src/builder/pipeline.ts +344 -88
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +7 -5
- package/src/core/config-refs.ts +103 -0
- package/src/core/config.ts +224 -0
- package/src/core/external.ts +86 -0
- package/src/core/handlers.ts +61 -15
- package/src/core/models.ts +32 -15
- package/src/core/origin.ts +23 -0
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +89 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +84 -31
- package/src/ctx/sh.ts +105 -44
- package/src/ctx/types.ts +12 -4
- package/src/describe/describe.ts +701 -133
- package/src/describe/manifest-schema.ts +258 -61
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +72 -35
- package/src/harness/env.ts +45 -25
- package/src/harness/execute.ts +58 -29
- package/src/harness/index.ts +22 -9
- package/src/harness/main.ts +239 -78
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +77 -15
- package/src/index.ts +124 -11
- package/src/library/checks.ts +28 -0
- package/src/library/compose-deploy.ts +113 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +452 -108
- package/src/library/helm-deploy.ts +449 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +499 -105
- package/src/library/index.ts +48 -12
- package/src/library/models.ts +33 -9
- package/src/library/ssh.ts +67 -0
- package/src/testing/index.ts +74 -36
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +17 -7
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +86 -36
- 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": {
|
|
@@ -129,6 +165,28 @@
|
|
|
129
165
|
"minLength": 1,
|
|
130
166
|
"type": "string"
|
|
131
167
|
},
|
|
168
|
+
"retention": {
|
|
169
|
+
"additionalProperties": false,
|
|
170
|
+
"description": "Retention overrides for this pipeline (omitted when none). Each field replaces the instance setting of the same meaning.",
|
|
171
|
+
"properties": {
|
|
172
|
+
"buildGraceDays": {
|
|
173
|
+
"maximum": 9007199254740991,
|
|
174
|
+
"minimum": 0,
|
|
175
|
+
"type": "integer"
|
|
176
|
+
},
|
|
177
|
+
"keepLatestRuns": {
|
|
178
|
+
"maximum": 9007199254740991,
|
|
179
|
+
"minimum": 0,
|
|
180
|
+
"type": "integer"
|
|
181
|
+
},
|
|
182
|
+
"minRunAgeDays": {
|
|
183
|
+
"maximum": 9007199254740991,
|
|
184
|
+
"minimum": 0,
|
|
185
|
+
"type": "integer"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"type": "object"
|
|
189
|
+
},
|
|
132
190
|
"secrets": {
|
|
133
191
|
"description": "All declared secrets (steps + poll triggers), sorted by name.",
|
|
134
192
|
"items": {
|
|
@@ -297,28 +355,173 @@
|
|
|
297
355
|
"stages"
|
|
298
356
|
],
|
|
299
357
|
"type": "object"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"additionalProperties": false,
|
|
361
|
+
"properties": {
|
|
362
|
+
"cases": {
|
|
363
|
+
"additionalProperties": {
|
|
364
|
+
"items": {
|
|
365
|
+
"$ref": "#/$defs/Stage"
|
|
366
|
+
},
|
|
367
|
+
"type": "array"
|
|
368
|
+
},
|
|
369
|
+
"description": "Discriminator value → stages. Exactly one case runs; a step name may repeat across cases, nowhere else.",
|
|
370
|
+
"propertyNames": {
|
|
371
|
+
"type": "string"
|
|
372
|
+
},
|
|
373
|
+
"type": "object"
|
|
374
|
+
},
|
|
375
|
+
"discriminator": {
|
|
376
|
+
"description": "The union's literal field whose value names the case.",
|
|
377
|
+
"minLength": 1,
|
|
378
|
+
"type": "string"
|
|
379
|
+
},
|
|
380
|
+
"select": {
|
|
381
|
+
"description": "Config reference (`<entry>.<field>…`) to a discriminated union; the run's config snapshot picks the case.",
|
|
382
|
+
"minLength": 1,
|
|
383
|
+
"type": "string"
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
"required": [
|
|
387
|
+
"select",
|
|
388
|
+
"discriminator",
|
|
389
|
+
"cases"
|
|
390
|
+
],
|
|
391
|
+
"type": "object"
|
|
300
392
|
}
|
|
301
393
|
]
|
|
302
394
|
},
|
|
303
395
|
"Step": {
|
|
304
396
|
"additionalProperties": false,
|
|
305
397
|
"properties": {
|
|
398
|
+
"clusterAccess": {
|
|
399
|
+
"const": true,
|
|
400
|
+
"description": "The step pod gets the instance's step ServiceAccount token (Kubernetes:StepServiceAccount), if the instance grants one. Omitted = no token.",
|
|
401
|
+
"type": "boolean"
|
|
402
|
+
},
|
|
306
403
|
"config": {
|
|
307
404
|
"additionalProperties": {
|
|
308
405
|
"$ref": "#/$defs/Json"
|
|
309
406
|
},
|
|
310
|
-
"description": "
|
|
407
|
+
"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
408
|
"propertyNames": {
|
|
312
409
|
"type": "string"
|
|
313
410
|
},
|
|
314
411
|
"type": "object"
|
|
315
412
|
},
|
|
413
|
+
"configEntries": {
|
|
414
|
+
"description": "Config entries the step receives (sorted; omitted when none). The service delivers them in input.json under '$config'.",
|
|
415
|
+
"items": {
|
|
416
|
+
"description": "Config entry name; a key of the pipeline's config document.",
|
|
417
|
+
"pattern": "^[a-z][a-zA-Z0-9]*$",
|
|
418
|
+
"type": "string"
|
|
419
|
+
},
|
|
420
|
+
"type": "array"
|
|
421
|
+
},
|
|
316
422
|
"description": {
|
|
317
423
|
"description": "What the step does, shown in the UI (may be empty).",
|
|
318
424
|
"type": "string"
|
|
319
425
|
},
|
|
426
|
+
"externalInputs": {
|
|
427
|
+
"additionalProperties": {
|
|
428
|
+
"additionalProperties": false,
|
|
429
|
+
"properties": {
|
|
430
|
+
"description": {
|
|
431
|
+
"type": "string"
|
|
432
|
+
},
|
|
433
|
+
"from": {
|
|
434
|
+
"description": "Who or what may answer.",
|
|
435
|
+
"items": {
|
|
436
|
+
"oneOf": [
|
|
437
|
+
{
|
|
438
|
+
"additionalProperties": false,
|
|
439
|
+
"properties": {
|
|
440
|
+
"kind": {
|
|
441
|
+
"const": "person",
|
|
442
|
+
"type": "string"
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
"required": [
|
|
446
|
+
"kind"
|
|
447
|
+
],
|
|
448
|
+
"type": "object"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"additionalProperties": false,
|
|
452
|
+
"properties": {
|
|
453
|
+
"kind": {
|
|
454
|
+
"const": "callback",
|
|
455
|
+
"type": "string"
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
"required": [
|
|
459
|
+
"kind"
|
|
460
|
+
],
|
|
461
|
+
"type": "object"
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"additionalProperties": false,
|
|
465
|
+
"properties": {
|
|
466
|
+
"intervalSeconds": {
|
|
467
|
+
"maximum": 9007199254740991,
|
|
468
|
+
"minimum": 10,
|
|
469
|
+
"type": "integer"
|
|
470
|
+
},
|
|
471
|
+
"kind": {
|
|
472
|
+
"const": "poll",
|
|
473
|
+
"type": "string"
|
|
474
|
+
},
|
|
475
|
+
"url": {
|
|
476
|
+
"description": "GET while waiting; {run.id} and {run.number} are filled in.",
|
|
477
|
+
"type": "string"
|
|
478
|
+
},
|
|
479
|
+
"valuePath": {
|
|
480
|
+
"description": "Dot path to the value; '.' is the whole body.",
|
|
481
|
+
"minLength": 1,
|
|
482
|
+
"type": "string"
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
"required": [
|
|
486
|
+
"kind",
|
|
487
|
+
"url",
|
|
488
|
+
"valuePath",
|
|
489
|
+
"intervalSeconds"
|
|
490
|
+
],
|
|
491
|
+
"type": "object"
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
"minItems": 1,
|
|
496
|
+
"type": "array"
|
|
497
|
+
},
|
|
498
|
+
"model": {
|
|
499
|
+
"description": "The value is validated against this model's input view.",
|
|
500
|
+
"pattern": "^[A-Za-z][A-Za-z0-9_]*$",
|
|
501
|
+
"type": "string"
|
|
502
|
+
},
|
|
503
|
+
"title": {
|
|
504
|
+
"description": "What is asked, shown to whoever answers.",
|
|
505
|
+
"minLength": 1,
|
|
506
|
+
"type": "string"
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
"required": [
|
|
510
|
+
"model",
|
|
511
|
+
"title",
|
|
512
|
+
"description",
|
|
513
|
+
"from"
|
|
514
|
+
],
|
|
515
|
+
"type": "object"
|
|
516
|
+
},
|
|
517
|
+
"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.",
|
|
518
|
+
"propertyNames": {
|
|
519
|
+
"type": "string"
|
|
520
|
+
},
|
|
521
|
+
"type": "object"
|
|
522
|
+
},
|
|
320
523
|
"handlerHash": {
|
|
321
|
-
"description": "Implementation identity: handler bundle (contract files excluded) + harness (bun + sdkHash); for
|
|
524
|
+
"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
525
|
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
323
526
|
"type": "string"
|
|
324
527
|
},
|
|
@@ -348,14 +551,8 @@
|
|
|
348
551
|
"type": "object"
|
|
349
552
|
},
|
|
350
553
|
"kind": {
|
|
351
|
-
"description": "'custom' = handlers
|
|
352
|
-
"
|
|
353
|
-
"custom",
|
|
354
|
-
"script",
|
|
355
|
-
"gitHubSource",
|
|
356
|
-
"imageBuild",
|
|
357
|
-
"helmDeployOverSsh"
|
|
358
|
-
],
|
|
554
|
+
"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.",
|
|
555
|
+
"minLength": 1,
|
|
359
556
|
"type": "string"
|
|
360
557
|
},
|
|
361
558
|
"name": {
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olivervea/pipelines",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.20",
|
|
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/
|
|
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
|
|
3
|
+
declare module '*.sh' {
|
|
4
4
|
const path: string;
|
|
5
5
|
export default path;
|
|
6
6
|
}
|
|
7
|
-
declare module
|
|
7
|
+
declare module '*.bash' {
|
|
8
8
|
const path: string;
|
|
9
9
|
export default path;
|
|
10
10
|
}
|
package/src/build/build.ts
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
// olve-build: pipeline dir → single executable + step.tar.
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from
|
|
3
|
-
import { join, resolve } from
|
|
4
|
-
import { canonicalJson } from
|
|
5
|
-
import { sha256File } from
|
|
6
|
-
import type { HarnessInfo } from
|
|
7
|
-
import {
|
|
8
|
-
import { MANIFEST_BEGIN, MANIFEST_END, uploadFile } from
|
|
9
|
-
import { GLIBC_INTERP, MUSL_INTERP, readInterpreter } from
|
|
10
|
-
import { handlerHashes, type HandlerHash } from
|
|
11
|
-
import {
|
|
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/handler-hash.ts';
|
|
7
|
+
import { writeTarZst } 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, kindHashes, type HandlerHash } from './hashes.ts';
|
|
11
|
+
import {
|
|
12
|
+
harnessInfo,
|
|
13
|
+
loadPipeline,
|
|
14
|
+
loadProject,
|
|
15
|
+
relPosix,
|
|
16
|
+
type Project,
|
|
17
|
+
} from './project.ts';
|
|
12
18
|
|
|
13
|
-
export const BUILD_FLAGS = [
|
|
14
|
-
|
|
19
|
+
export const BUILD_FLAGS = [
|
|
20
|
+
'--no-compile-autoload-dotenv',
|
|
21
|
+
'--no-compile-autoload-bunfig',
|
|
22
|
+
'--bytecode',
|
|
23
|
+
'--format=esm',
|
|
24
|
+
] as const;
|
|
25
|
+
export type Target = 'bun-linux-x64' | 'bun-linux-x64-musl';
|
|
15
26
|
|
|
16
27
|
export class BuildError extends Error {
|
|
17
|
-
override name =
|
|
18
|
-
constructor(
|
|
28
|
+
override name = 'BuildError';
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
readonly exitCode = 1,
|
|
32
|
+
) {
|
|
19
33
|
super(message);
|
|
20
34
|
}
|
|
21
35
|
}
|
|
@@ -23,7 +37,7 @@ export class BuildError extends Error {
|
|
|
23
37
|
export interface BuildOptions {
|
|
24
38
|
out: string;
|
|
25
39
|
target?: Target;
|
|
26
|
-
/** Pre-signed PUT for step.
|
|
40
|
+
/** Pre-signed PUT for the step archive. */
|
|
27
41
|
uploadUrl?: string;
|
|
28
42
|
/** Print the manifest between sentinels on stdout (for the build Job's pod log). */
|
|
29
43
|
sentinels?: boolean;
|
|
@@ -41,84 +55,140 @@ export interface BuildResult {
|
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/** Writes `<dir>/.olve/gen/{handlers,entry}.ts`: the literal-import handler registry + binary entry. */
|
|
44
|
-
export function generateEntry(
|
|
45
|
-
|
|
58
|
+
export function generateEntry(
|
|
59
|
+
project: Project,
|
|
60
|
+
hashes: Record<string, string>,
|
|
61
|
+
harness: HarnessInfo,
|
|
62
|
+
kinds: Record<string, string> = {},
|
|
63
|
+
): string {
|
|
64
|
+
const gen = join(project.dir, '.olve', 'gen');
|
|
46
65
|
mkdirSync(gen, { recursive: true });
|
|
47
|
-
const registry = project.handlers
|
|
66
|
+
const registry = project.handlers
|
|
67
|
+
.map(
|
|
68
|
+
([name, file]) =>
|
|
69
|
+
` ${JSON.stringify(name)}: () => import(${JSON.stringify(relPosix(gen, file))}),`,
|
|
70
|
+
)
|
|
71
|
+
.join('\n');
|
|
48
72
|
writeFileSync(
|
|
49
|
-
join(gen,
|
|
73
|
+
join(gen, 'handlers.ts'),
|
|
50
74
|
`// GENERATED by olve-build — do not edit.\n` +
|
|
51
|
-
`import type { HandlerRegistry, HarnessInfo } from "@
|
|
75
|
+
`import type { HandlerRegistry, HarnessInfo } from "@olivervea/pipelines/harness";\n\n` +
|
|
52
76
|
`export const handlers: HandlerRegistry = {\n${registry}\n};\n\n` +
|
|
53
77
|
`export const handlerHashes: Record<string, string> = ${JSON.stringify(hashes, null, 2)};\n\n` +
|
|
78
|
+
`export const kindHashes: Record<string, string> = ${JSON.stringify(kinds, null, 2)};\n\n` +
|
|
54
79
|
`export const harness: HarnessInfo = ${JSON.stringify(harness, null, 2)};\n`,
|
|
55
80
|
);
|
|
56
|
-
const entry = join(gen,
|
|
81
|
+
const entry = join(gen, 'entry.ts');
|
|
57
82
|
writeFileSync(
|
|
58
83
|
entry,
|
|
59
84
|
`// GENERATED by olve-build — do not edit. Entry point of the step binary.\n` +
|
|
60
85
|
`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`,
|
|
86
|
+
`import { runHarness } from "@olivervea/pipelines/harness";\n` +
|
|
87
|
+
`import { handlers, handlerHashes, kindHashes, harness } from "./handlers.ts";\n\n` +
|
|
88
|
+
`process.exit(await runHarness({ pipeline, handlers, handlerHashes, kindHashes, harness }, process.argv.slice(2)));\n`,
|
|
64
89
|
);
|
|
65
90
|
return entry;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
export async function build(dir: string, opts: BuildOptions): Promise<BuildResult> {
|
|
69
|
-
const log = opts.log ?? ((l: string) => process.stderr.write(l +
|
|
70
|
-
const stdout =
|
|
71
|
-
|
|
72
|
-
const
|
|
94
|
+
const log = opts.log ?? ((l: string) => process.stderr.write(l + '\n'));
|
|
95
|
+
const stdout =
|
|
96
|
+
opts.stdout ?? (async (t: string) => void (await Bun.write(Bun.stdout, t)));
|
|
97
|
+
const target = opts.target ?? 'bun-linux-x64';
|
|
98
|
+
const project = await loadProject(dir);
|
|
73
99
|
const out = resolve(opts.out);
|
|
74
100
|
mkdirSync(out, { recursive: true });
|
|
75
101
|
const harness = harnessInfo();
|
|
76
102
|
|
|
77
103
|
const t0 = performance.now();
|
|
78
104
|
const hashes = await handlerHashes(project, harness);
|
|
79
|
-
const
|
|
80
|
-
|
|
105
|
+
const kinds = await kindHashes(project, await loadPipeline(project));
|
|
106
|
+
const entry = generateEntry(
|
|
107
|
+
project,
|
|
108
|
+
Object.fromEntries(Object.entries(hashes).map(([k, v]) => [k, v.handlerHash])),
|
|
109
|
+
harness,
|
|
110
|
+
kinds,
|
|
111
|
+
);
|
|
112
|
+
log(
|
|
113
|
+
`olve-build: ${project.handlers.length} handler(s) hashed in ${((performance.now() - t0) / 1000).toFixed(2)}s`,
|
|
114
|
+
);
|
|
81
115
|
|
|
82
116
|
// 1. compile
|
|
83
|
-
const binary = join(out,
|
|
117
|
+
const binary = join(out, 'step');
|
|
84
118
|
rmSync(binary, { force: true });
|
|
85
119
|
const t1 = performance.now();
|
|
86
|
-
const p = Bun.spawnSync(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
const p = Bun.spawnSync(
|
|
121
|
+
[
|
|
122
|
+
process.execPath,
|
|
123
|
+
'build',
|
|
124
|
+
'--compile',
|
|
125
|
+
`--target=${target}`,
|
|
126
|
+
...BUILD_FLAGS,
|
|
127
|
+
entry,
|
|
128
|
+
`--outfile=${binary}`,
|
|
129
|
+
],
|
|
130
|
+
{
|
|
131
|
+
cwd: project.dir,
|
|
132
|
+
stdout: 'pipe',
|
|
133
|
+
stderr: 'pipe',
|
|
134
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
135
|
+
},
|
|
136
|
+
);
|
|
92
137
|
if (p.exitCode !== 0 || !existsSync(binary))
|
|
93
|
-
throw new BuildError(
|
|
94
|
-
|
|
138
|
+
throw new BuildError(
|
|
139
|
+
`bun build --compile failed (exit ${p.exitCode}):\n${p.stdout.toString()}${p.stderr.toString()}`,
|
|
140
|
+
);
|
|
141
|
+
log(
|
|
142
|
+
`olve-build: compiled ${target} in ${((performance.now() - t1) / 1000).toFixed(2)}s`,
|
|
143
|
+
);
|
|
95
144
|
|
|
96
145
|
// 2. loader check: the harness launches the binary through /olve/lib/<loader>; a musl binary
|
|
97
146
|
// (e.g. compiled on alpine, where bun-linux-x64 silently yields musl) would not start.
|
|
98
|
-
const expected = target ===
|
|
147
|
+
const expected = target === 'bun-linux-x64' ? GLIBC_INTERP : MUSL_INTERP;
|
|
99
148
|
const interp = readInterpreter(binary);
|
|
100
149
|
if (interp !== expected)
|
|
101
|
-
throw new BuildError(
|
|
150
|
+
throw new BuildError(
|
|
151
|
+
`compiled binary has PT_INTERP ${interp ?? '(none)'}, expected ${expected} — compile on a glibc Bun image (oven/bun:<version>-slim), not alpine`,
|
|
152
|
+
);
|
|
102
153
|
|
|
103
154
|
// 3. describe (runs the real binary: the manifest is exactly what the step pods will see)
|
|
104
|
-
const d = Bun.spawnSync([binary,
|
|
105
|
-
|
|
155
|
+
const d = Bun.spawnSync([binary, 'describe'], {
|
|
156
|
+
cwd: out,
|
|
157
|
+
stdout: 'pipe',
|
|
158
|
+
stderr: 'pipe',
|
|
159
|
+
env: { PATH: process.env.PATH ?? '/usr/bin:/bin' },
|
|
160
|
+
});
|
|
161
|
+
if (d.exitCode !== 0)
|
|
162
|
+
throw new BuildError(
|
|
163
|
+
`describe failed (exit ${d.exitCode}):\n${d.stderr.toString()}`,
|
|
164
|
+
d.exitCode ?? 1,
|
|
165
|
+
);
|
|
106
166
|
const manifest = d.stdout.toString();
|
|
107
|
-
const manifestFile = join(out,
|
|
167
|
+
const manifestFile = join(out, 'manifest.json');
|
|
108
168
|
writeFileSync(manifestFile, manifest);
|
|
109
169
|
|
|
110
|
-
// 4. step.tar.
|
|
111
|
-
const archive = join(out,
|
|
112
|
-
await
|
|
170
|
+
// 4. step.tar.zst (deterministic: `step` at the root, 0755, mtime 0)
|
|
171
|
+
const archive = join(out, 'step.tar.zst');
|
|
172
|
+
await writeTarZst(archive, [
|
|
173
|
+
{ path: 'step', type: 'file', mode: 0o755, source: binary },
|
|
174
|
+
]);
|
|
113
175
|
writeFileSync(
|
|
114
|
-
join(out,
|
|
115
|
-
canonicalJson({
|
|
176
|
+
join(out, 'build.json'),
|
|
177
|
+
canonicalJson({
|
|
178
|
+
target,
|
|
179
|
+
flags: BUILD_FLAGS,
|
|
180
|
+
harness,
|
|
181
|
+
handlers: hashes,
|
|
182
|
+
kinds,
|
|
183
|
+
binarySha256: sha256File(binary),
|
|
184
|
+
archiveSha256: sha256File(archive),
|
|
185
|
+
}),
|
|
116
186
|
);
|
|
117
187
|
log(`olve-build: wrote ${archive}`);
|
|
118
188
|
|
|
119
189
|
if (opts.uploadUrl) {
|
|
120
190
|
await uploadFile(opts.uploadUrl, archive, log);
|
|
121
|
-
log(
|
|
191
|
+
log('olve-build: uploaded step.tar.zst');
|
|
122
192
|
}
|
|
123
193
|
if (opts.sentinels) await stdout(`${MANIFEST_BEGIN}\n${manifest}${MANIFEST_END}\n`);
|
|
124
194
|
return { binary, archive, manifestFile, manifest, hashes, harness };
|
package/src/build/cli.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <url>] [--sentinels] [--hash-only]
|
|
3
|
-
import { parseArgs } from
|
|
4
|
-
import { canonicalJson } from
|
|
5
|
-
import { build, BuildError, type Target } from
|
|
6
|
-
import { handlerHashes } from
|
|
7
|
-
import { harnessInfo, loadProject, ProjectError } from
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { canonicalJson } from '../util/canonical.ts';
|
|
5
|
+
import { build, BuildError, type Target } from './build.ts';
|
|
6
|
+
import { handlerHashes } from './hashes.ts';
|
|
7
|
+
import { harnessInfo, loadProject, ProjectError } from './project.ts';
|
|
8
8
|
|
|
9
|
-
const USAGE =
|
|
9
|
+
const USAGE =
|
|
10
|
+
'usage: olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <url>] [--sentinels]\n olve-build <pipelineDir> --hash-only';
|
|
10
11
|
|
|
11
12
|
async function main(): Promise<number> {
|
|
12
13
|
let parsed;
|
|
@@ -14,12 +15,12 @@ async function main(): Promise<number> {
|
|
|
14
15
|
parsed = parseArgs({
|
|
15
16
|
allowPositionals: true,
|
|
16
17
|
options: {
|
|
17
|
-
out: { type:
|
|
18
|
-
target: { type:
|
|
19
|
-
|
|
20
|
-
sentinels: { type:
|
|
21
|
-
|
|
22
|
-
help: { type:
|
|
18
|
+
out: { type: 'string' },
|
|
19
|
+
target: { type: 'string' },
|
|
20
|
+
'upload-url': { type: 'string' },
|
|
21
|
+
sentinels: { type: 'boolean', default: false },
|
|
22
|
+
'hash-only': { type: 'boolean', default: false },
|
|
23
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
23
24
|
},
|
|
24
25
|
});
|
|
25
26
|
} catch (e) {
|
|
@@ -27,23 +28,29 @@ async function main(): Promise<number> {
|
|
|
27
28
|
return 64;
|
|
28
29
|
}
|
|
29
30
|
const { values, positionals } = parsed;
|
|
30
|
-
if (values.help) return console.log(USAGE), 0;
|
|
31
|
-
if (positionals.length !== 1) return console.error(USAGE), 64;
|
|
31
|
+
if (values.help) return (console.log(USAGE), 0);
|
|
32
|
+
if (positionals.length !== 1) return (console.error(USAGE), 64);
|
|
32
33
|
const dir = positionals[0]!;
|
|
33
34
|
try {
|
|
34
|
-
if (values[
|
|
35
|
-
const h = await handlerHashes(loadProject(dir), harnessInfo());
|
|
35
|
+
if (values['hash-only']) {
|
|
36
|
+
const h = await handlerHashes(await loadProject(dir), harnessInfo());
|
|
36
37
|
await Bun.write(Bun.stdout, canonicalJson({ harness: harnessInfo(), handlers: h }));
|
|
37
38
|
return 0;
|
|
38
39
|
}
|
|
39
|
-
if (!values.out) return console.error(`--out is required\n${USAGE}`), 64;
|
|
40
|
-
const target = (values.target ??
|
|
41
|
-
if (target !==
|
|
42
|
-
|
|
40
|
+
if (!values.out) return (console.error(`--out is required\n${USAGE}`), 64);
|
|
41
|
+
const target = (values.target ?? 'bun-linux-x64') as Target;
|
|
42
|
+
if (target !== 'bun-linux-x64' && target !== 'bun-linux-x64-musl')
|
|
43
|
+
return (console.error(`unsupported target ${target}`), 64);
|
|
44
|
+
await build(dir, {
|
|
45
|
+
out: values.out,
|
|
46
|
+
target,
|
|
47
|
+
uploadUrl: values['upload-url'],
|
|
48
|
+
sentinels: values.sentinels,
|
|
49
|
+
});
|
|
43
50
|
return 0;
|
|
44
51
|
} catch (e) {
|
|
45
|
-
if (e instanceof BuildError) return console.error(e.message), e.exitCode;
|
|
46
|
-
if (e instanceof ProjectError) return console.error(e.message), 78;
|
|
52
|
+
if (e instanceof BuildError) return (console.error(e.message), e.exitCode);
|
|
53
|
+
if (e instanceof ProjectError) return (console.error(e.message), 78);
|
|
47
54
|
console.error((e as Error)?.stack ?? String(e));
|
|
48
55
|
return 1;
|
|
49
56
|
}
|