@olivervea/pipelines 2.0.0-beta.20 → 2.0.0-beta.22
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 +22 -35
- package/manifest.schema.json +10 -10
- package/package.json +1 -1
- package/src/build/build.ts +14 -45
- package/src/build/cli.ts +6 -3
- package/src/build/hashes.ts +19 -92
- package/src/build/project.ts +19 -62
- package/src/builder/pipeline.ts +2 -2
- package/src/core/brands.ts +0 -1
- package/src/core/handlers.ts +3 -11
- package/src/core/steps.ts +6 -9
- package/src/describe/describe.ts +11 -59
- package/src/describe/manifest-schema.ts +6 -8
- package/src/dev/cli.ts +2 -12
- package/src/harness/exit.ts +1 -1
- package/src/harness/index.ts +1 -2
- package/src/harness/main.ts +9 -34
- package/src/testing/index.ts +2 -2
- package/src/util/code-hash.ts +17 -0
- package/src/core/origin.ts +0 -23
- package/src/util/handler-hash.ts +0 -32
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ bunx olve-build . --out .olve/out
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
- The package ships TypeScript sources (Bun runs them directly; `harness.sdkHash` hashes them), so the
|
|
50
|
-
same version gives the same `sdkHash` and
|
|
50
|
+
same version gives the same `sdkHash` and `codeHash` from npm or from `file:../src/sdk`.
|
|
51
51
|
- Inside this repo, `.pipelines/` and `.pipelines-test/` use `"@olivervea/pipelines": "file:../src/sdk"`.
|
|
52
52
|
|
|
53
53
|
### Releases
|
|
@@ -59,17 +59,9 @@ skips when the version exists, or when the pipeline secret `NPM_TOKEN` is unset.
|
|
|
59
59
|
|
|
60
60
|
## Authoring
|
|
61
61
|
|
|
62
|
-
A pipeline directory (the binding path, `.pipelines/` by default)
|
|
62
|
+
A pipeline directory (the binding path, `.pipelines/` by default) is an ordinary Bun package. `main` in `package.json` is required and names the entry point, which default-exports the pipeline; every other name is yours. A step carries its implementation, `implement(step({ … }), fn)`, and is imported and staged like any value.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
models.ts data models (contract file)
|
|
66
|
-
pipeline.ts steps + `export default pipeline(…)` (contract file)
|
|
67
|
-
handlers/*.ts one per custom step: `export default implement(step, fn)` (pairs by the step, not the file name)
|
|
68
|
-
handlers/lib/… shared helpers (not handlers)
|
|
69
|
-
scripts/*.sh embedded with `import s from "../scripts/x.sh" with { type: "file" }`
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Contract files hold contracts only. Describe evaluates them without loading handlers, and they are kept out of every handler's hash.
|
|
64
|
+
Describe evaluates the entry point and everything it imports, so top-level code must be deterministic: no env, time or network. Scripts are embedded with `import s from "./x.sh" with { type: "file" }`.
|
|
73
65
|
|
|
74
66
|
### Models, secrets, steps
|
|
75
67
|
|
|
@@ -97,6 +89,7 @@ export const deployBeta = step({
|
|
|
97
89
|
|
|
98
90
|
Rules:
|
|
99
91
|
|
|
92
|
+
- `step()` is the contract. A pipeline stages `implement(step, fn)`; a staged step without an implementation is a describe error.
|
|
100
93
|
- Models are **nominal**: two models with the same shape are not interchangeable.
|
|
101
94
|
- There is **one schema per model name**. Two different schemas registered under the same name is a describe error.
|
|
102
95
|
- **Outputs are validated strictly**: unknown keys are rejected at any depth, not stripped.
|
|
@@ -176,13 +169,13 @@ Library steps carry their own implementation and image; they take typed settings
|
|
|
176
169
|
| `sshSession(ctx, { host, key })` | Installs openssh-client when the image has no ssh (apk or apt-get), writes the key to `<tmpDir>/ssh/id` (trailing newline, mode 600; host key not verified) and returns `{ run(remote), text(remote), copy(local, remote), shell(remote) }`; `shell` is the quoted ssh command for pipes (`ctx.sh(`printf '%s' "$V" \| ${s.shell("kubectl apply -f -")}`)`). `helmDeployOverSsh` uses it. |
|
|
177
170
|
|
|
178
171
|
```ts
|
|
179
|
-
import { httpHealthCheck, implement } from "@olivervea/pipelines";
|
|
180
|
-
export
|
|
172
|
+
import { httpHealthCheck, implement, step } from "@olivervea/pipelines";
|
|
173
|
+
export const smoke = implement(step({ name: "smoke", description: "", image: "alpine:3.22" }), async (_, ctx) => { await httpHealthCheck(ctx, "http://app.ns.svc.cluster.local/api/health", { attempts: 10 }); });
|
|
181
174
|
```
|
|
182
175
|
|
|
183
176
|
### Reusable steps
|
|
184
177
|
|
|
185
|
-
A step several pipelines use is a function returning `implement(step(…), handler)`, staged directly
|
|
178
|
+
A step several pipelines use is a function returning `implement(step(…), handler)`, staged directly. The library steps are written this way, so a module in a repo or a package of its own works the same.
|
|
186
179
|
|
|
187
180
|
```ts
|
|
188
181
|
import { implement, resolveConfigRefs, serviceUrl, step, HelmRelease, type Configurable, type LiteralName } from "@olivervea/pipelines";
|
|
@@ -195,20 +188,17 @@ export function smokeTest<const N extends string>(def: { name: LiteralName<N>; d
|
|
|
195
188
|
}
|
|
196
189
|
```
|
|
197
190
|
|
|
198
|
-
- **`kind`**: a label,
|
|
191
|
+
- **`kind`**: a label, `custom` unless the step names one. Prefix a package's kinds (`acme/smokeTest`).
|
|
199
192
|
- **`settings`**: what the step was set up with, recorded as the step's manifest `config` (so part of its version). JSON values and config references; the step receives every entry its settings reference.
|
|
200
|
-
-
|
|
201
|
-
- `implement` refuses a step that already carries an implementation; a `handlers/*.ts` file for such a step is a describe error.
|
|
193
|
+
- `implement` refuses a step that already carries an implementation.
|
|
202
194
|
|
|
203
195
|
### Handlers
|
|
204
196
|
|
|
205
197
|
```ts
|
|
206
|
-
|
|
207
|
-
import
|
|
208
|
-
import { deployBeta } from "../pipeline";
|
|
209
|
-
import deployScript from "../scripts/deploy.sh" with { type: "file" };
|
|
198
|
+
import { implement, step } from "@olivervea/pipelines";
|
|
199
|
+
import deployScript from "./scripts/deploy.sh" with { type: "file" };
|
|
210
200
|
|
|
211
|
-
export
|
|
201
|
+
export const deployBeta = implement(step({ name: "deploy-beta", /* … */ }), async ({ artifact }, ctx) => {
|
|
212
202
|
await ctx.sh(deployScript, { env: { IMAGE_TAR: await ctx.files.fetch(artifact.image), CHART_DIR: await ctx.files.fetch(artifact.chart) } });
|
|
213
203
|
await ctx.retry({ attempts: 5, delaySeconds: 5 }, () => ctx.http.expectOk("https://pipelines-beta.ovea.pro/api/health"));
|
|
214
204
|
return { release: "olve-pipelines", version: artifact.version, url: "https://pipelines-beta.ovea.pro" }; // or ctx.skip("reason")
|
|
@@ -260,13 +250,13 @@ export default pipeline({ name: "Olve.Pipelines", description: "…", input: M.D
|
|
|
260
250
|
|
|
261
251
|
```ts
|
|
262
252
|
import { testHandler, testFileRef } from "@olivervea/pipelines/testing";
|
|
263
|
-
import deployBeta from "../
|
|
253
|
+
import { deployBeta } from "../steps/deploy-beta";
|
|
264
254
|
|
|
265
255
|
const r = await testHandler(deployBeta, { artifact }, { secrets: { SSH_PRIVATE_KEY: "k" }, config: { target: { host: "h" } }, ctx: { http: fakeHttp }, run: { number: 412 } });
|
|
266
256
|
expect(r).toMatchObject({ status: "succeeded", output: { release: "olve-pipelines" } }); // r.filesDir, r.logs, r.exitCode
|
|
267
257
|
```
|
|
268
258
|
|
|
269
|
-
`testHandler` uses the same validation path as a real run. Invalid input gives `status: "failed"` with `kind: "input"`; an invalid return value gives `kind: "output"`. Any part of `ctx` can be replaced; whatever you don't replace is the real implementation. `testHandler` takes any implementation: a
|
|
259
|
+
`testHandler` uses the same validation path as a real run. Invalid input gives `status: "failed"` with `kind: "input"`; an invalid return value gives `kind: "output"`. Any part of `ctx` can be replaced; whatever you don't replace is the real implementation. `testHandler` takes any implementation: a custom step, a library step or a reusable step.
|
|
270
260
|
|
|
271
261
|
## Harness contract (service ⇄ step binary)
|
|
272
262
|
|
|
@@ -346,7 +336,7 @@ For `map-trigger`, `output` is the pipeline input after it has been parsed by th
|
|
|
346
336
|
| 65 | `input.json` missing, not JSON, or invalid for the step (or event), including its `$config` | failed / `input` |
|
|
347
337
|
| 66 | the return value failed strict validation, or an own file ref is missing or changed | failed / `output` |
|
|
348
338
|
| 74 | the `result.tar.gz` upload failed after retries. Used only when the step itself succeeded; otherwise the step's own code wins. | yes (printed) |
|
|
349
|
-
| 78 | the pipeline is invalid (describe problems, or a
|
|
339
|
+
| 78 | the pipeline is invalid (describe problems, or a step without an implementation) | none |
|
|
350
340
|
| 130 / 143 | the handler failed after SIGINT / SIGTERM (cancellation, `activeDeadlineSeconds`) | failed / `handler` |
|
|
351
341
|
|
|
352
342
|
### Result upload (`OLVE_RESULT_URL`)
|
|
@@ -377,7 +367,7 @@ Exact format: [`manifest.schema.json`](manifest.schema.json) (JSON Schema 2020-1
|
|
|
377
367
|
- **`apiVersion`** is `"2.0"`.
|
|
378
368
|
- **`harness`** is `{ bun, sdk, sdkHash }`.
|
|
379
369
|
- **`models[name]`** is `{ output, input? }`.
|
|
380
|
-
- **Step `kind`** is `custom`
|
|
370
|
+
- **Step `kind`** is `custom` by default, or what a reusable step names (`imageBuild`, `composeDeploy`, a package's `acme/…`). To the service it is a label: any non-empty kind is accepted, so a new library or package step needs no service change.
|
|
381
371
|
- `output` is the `z.output` view, with `additionalProperties: false` on every object. It validates outputs, the pool and trigger results.
|
|
382
372
|
- `input` is the `z.input` view (defaults optional). Use it for trigger forms and manual input. It is emitted only when it differs from `output` beyond `additionalProperties`.
|
|
383
373
|
- **Step `input`** maps each input key to a mapping:
|
|
@@ -388,10 +378,7 @@ Exact format: [`manifest.schema.json`](manifest.schema.json) (JSON Schema 2020-1
|
|
|
388
378
|
An object whose only key is `ref` (with a string value) or `const` is a tag. Describe never emits a nested object with exactly that key set. Model-matched inputs are emitted already resolved (`{"ref": "build"}`).
|
|
389
379
|
- **Step `externalInputs`** (when present) maps each external input key to `{model, title, description, from}`, `from` being `{kind: "person"}`, `{kind: "callback"}` or `{kind: "poll", url, valuePath, intervalSeconds}`. Those keys are not in `input` or `inputTypes`; the service merges the answers into `input.json`.
|
|
390
380
|
- **`inputTypes`** declares each input key: `{model, optional}`, or `{schema: {output, input?}}` for a plain schema.
|
|
391
|
-
- **`
|
|
392
|
-
- Custom steps: `sha256(canonical {bundle, harness})`. `bundle` hashes the handler bundled on its own, with `pipeline.ts`, `models.ts` and `@olivervea/pipelines` external. It is tree-shaken and includes embedded scripts, so it changes only when that handler's code (or code it uses) changes.
|
|
393
|
-
- Steps that carry their implementation: `sha256(canonical {library: kind, config, harness, code})`, `code` being the hash of the modules that define and implement steps of that kind (see [Reusable steps](#reusable-steps)).
|
|
394
|
-
- Because `harness` is part of both hashes, a Bun or SDK bump changes every handler.
|
|
381
|
+
- **`codeHash`** is the build's identity: `sha256(canonical {bundle, harness})`. `bundle` hashes the entry point bundled with everything it imports, with `@olivervea/pipelines` external. It is tree-shaken and includes embedded scripts and dependencies, so any change to code in use changes it, and with it the manifest, which makes a new pipeline version. A step version covers what the step declares: image, `inputTypes`/model versions, `output` model version, `secrets`, `configEntries` (and those entries' schemas), `input` mappings and `config`.
|
|
395
382
|
- **`stages`** is an array whose items are either `{steps: [Step…]}` (parallel, with a barrier) or `{when: {field: [values]}, stages: [...]}`.
|
|
396
383
|
- **`triggers`**:
|
|
397
384
|
- `{kind: "push", name: "push"}`;
|
|
@@ -407,7 +394,7 @@ Describe runs these checks; reconcile should re-run the manifest-level ones:
|
|
|
407
394
|
|
|
408
395
|
- step name format and reserved names (`input`, `failure`), and duplicate step names;
|
|
409
396
|
- one schema per model name, and JSON-representable models;
|
|
410
|
-
-
|
|
397
|
+
- a staged step without an implementation, and an empty step kind;
|
|
411
398
|
- availability, ambiguity, maybe-available as required, `optional` without a producer, plain schema without a mapper;
|
|
412
399
|
- mapper source analysis and recording, unknown fields in refs, mapper keys that are missing or extra;
|
|
413
400
|
- `when` on an unknown or non-literal field, or with an invalid value;
|
|
@@ -420,14 +407,14 @@ Describe runs these checks; reconcile should re-run the manifest-level ones:
|
|
|
420
407
|
|
|
421
408
|
```
|
|
422
409
|
olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <presigned PUT>] [--sentinels]
|
|
423
|
-
olve-build <pipelineDir> --hash-only # print
|
|
410
|
+
olve-build <pipelineDir> --hash-only # print the code hash (diagnostics)
|
|
424
411
|
```
|
|
425
412
|
|
|
426
|
-
1.
|
|
427
|
-
2. Generate `<pipelineDir>/.olve/gen/
|
|
413
|
+
1. Read the entry point from `main` in `package.json` (a missing `main`, or a `main` whose file is missing, is an error) and hash the build (see `codeHash`).
|
|
414
|
+
2. Generate `<pipelineDir>/.olve/gen/entry.ts`, which imports the pipeline and calls `runHarness` with it, the code hash and the harness info. Add `.olve/` to `.gitignore`.
|
|
428
415
|
3. Run `bun build --compile --target=bun-linux-x64 --no-compile-autoload-dotenv --no-compile-autoload-bunfig --bytecode --format=esm` → `<out>/step`. The output is never minified, because mapper source analysis needs real source.
|
|
429
416
|
4. Check that `PT_INTERP` is `/lib64/ld-linux-x86-64.so.2`. On alpine, Bun silently produces a musl binary; the check fails it.
|
|
430
|
-
5. Run `<out>/step describe` → `<out>/manifest.json`. A describe problem is a build failure with exit 78. **A
|
|
417
|
+
5. Run `<out>/step describe` → `<out>/manifest.json`. A describe problem is a build failure with exit 78. **A step with no implementation is an error.**
|
|
431
418
|
6. Write `<out>/step.tar.zst` (a deterministic tar, `step` at the root, mode 0755, zstd level 3: it unpacks in the step pod far faster than gzip; harness-base also still unpacks the gzip archives of older SDKs; from 2.0.0-beta.7 the SDK needs an instance whose harness-base unpacks zstd, i.e. one deployed on or after 2026-10-25) and `<out>/build.json` (diagnostics).
|
|
432
419
|
7. With `--upload-url`, PUT `step.tar.zst`, using the same retry rules as the result upload. With `--sentinels`, print the manifest between the manifest sentinels.
|
|
433
420
|
|
package/manifest.schema.json
CHANGED
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"const": "2.0",
|
|
48
48
|
"type": "string"
|
|
49
49
|
},
|
|
50
|
+
"codeHash": {
|
|
51
|
+
"description": "Hash of the whole build: the pipeline and its code bundled, plus the harness. Any code change makes a new pipeline version.",
|
|
52
|
+
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
50
55
|
"config": {
|
|
51
56
|
"description": "Config entries used by steps, sorted by name (omitted when none). Fields of kind secretRef ({secret: NAME}) name a secret.",
|
|
52
57
|
"items": {
|
|
@@ -121,7 +126,7 @@
|
|
|
121
126
|
},
|
|
122
127
|
"harness": {
|
|
123
128
|
"additionalProperties": false,
|
|
124
|
-
"description": "Harness identity. Already folded into
|
|
129
|
+
"description": "Harness identity. Already folded into codeHash.",
|
|
125
130
|
"properties": {
|
|
126
131
|
"bun": {
|
|
127
132
|
"description": "Bun runtime version compiled into the binary.",
|
|
@@ -227,6 +232,7 @@
|
|
|
227
232
|
"name",
|
|
228
233
|
"input",
|
|
229
234
|
"harness",
|
|
235
|
+
"codeHash",
|
|
230
236
|
"models",
|
|
231
237
|
"secrets",
|
|
232
238
|
"triggers",
|
|
@@ -404,7 +410,7 @@
|
|
|
404
410
|
"additionalProperties": {
|
|
405
411
|
"$ref": "#/$defs/Json"
|
|
406
412
|
},
|
|
407
|
-
"description": "The step's settings (display only
|
|
413
|
+
"description": "The step's settings (display only). A config reference is recorded as {\"$config\": \"<entry>.<field>…\"}; the entry is in configEntries.",
|
|
408
414
|
"propertyNames": {
|
|
409
415
|
"type": "string"
|
|
410
416
|
},
|
|
@@ -520,11 +526,6 @@
|
|
|
520
526
|
},
|
|
521
527
|
"type": "object"
|
|
522
528
|
},
|
|
523
|
-
"handlerHash": {
|
|
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.",
|
|
525
|
-
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
526
|
-
"type": "string"
|
|
527
|
-
},
|
|
528
529
|
"image": {
|
|
529
530
|
"description": "Container image the step runs in.",
|
|
530
531
|
"minLength": 1,
|
|
@@ -551,7 +552,7 @@
|
|
|
551
552
|
"type": "object"
|
|
552
553
|
},
|
|
553
554
|
"kind": {
|
|
554
|
-
"description": "
|
|
555
|
+
"description": "A label for the step's kind: 'custom' unless the step names one.",
|
|
555
556
|
"minLength": 1,
|
|
556
557
|
"type": "string"
|
|
557
558
|
},
|
|
@@ -601,8 +602,7 @@
|
|
|
601
602
|
"inputTypes",
|
|
602
603
|
"input",
|
|
603
604
|
"output",
|
|
604
|
-
"secrets"
|
|
605
|
-
"handlerHash"
|
|
605
|
+
"secrets"
|
|
606
606
|
],
|
|
607
607
|
"type": "object"
|
|
608
608
|
},
|
package/package.json
CHANGED
package/src/build/build.ts
CHANGED
|
@@ -3,18 +3,12 @@ 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
|
-
import type { HarnessInfo } from '../util/
|
|
6
|
+
import type { HarnessInfo } from '../util/code-hash.ts';
|
|
7
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 {
|
|
11
|
-
import {
|
|
12
|
-
harnessInfo,
|
|
13
|
-
loadPipeline,
|
|
14
|
-
loadProject,
|
|
15
|
-
relPosix,
|
|
16
|
-
type Project,
|
|
17
|
-
} from './project.ts';
|
|
10
|
+
import { codeHash } from './hashes.ts';
|
|
11
|
+
import { harnessInfo, loadProject, relPosix, type Project } from './project.ts';
|
|
18
12
|
|
|
19
13
|
export const BUILD_FLAGS = [
|
|
20
14
|
'--no-compile-autoload-dotenv',
|
|
@@ -50,42 +44,26 @@ export interface BuildResult {
|
|
|
50
44
|
archive: string;
|
|
51
45
|
manifestFile: string;
|
|
52
46
|
manifest: string;
|
|
53
|
-
|
|
47
|
+
codeHash: string;
|
|
54
48
|
harness: HarnessInfo;
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
/** Writes `<dir>/.olve/gen/
|
|
51
|
+
/** Writes `<dir>/.olve/gen/entry.ts`, the entry point of the step binary. */
|
|
58
52
|
export function generateEntry(
|
|
59
53
|
project: Project,
|
|
60
|
-
|
|
54
|
+
code: string,
|
|
61
55
|
harness: HarnessInfo,
|
|
62
|
-
kinds: Record<string, string> = {},
|
|
63
56
|
): string {
|
|
64
57
|
const gen = join(project.dir, '.olve', 'gen');
|
|
65
58
|
mkdirSync(gen, { recursive: true });
|
|
66
|
-
const registry = project.handlers
|
|
67
|
-
.map(
|
|
68
|
-
([name, file]) =>
|
|
69
|
-
` ${JSON.stringify(name)}: () => import(${JSON.stringify(relPosix(gen, file))}),`,
|
|
70
|
-
)
|
|
71
|
-
.join('\n');
|
|
72
|
-
writeFileSync(
|
|
73
|
-
join(gen, 'handlers.ts'),
|
|
74
|
-
`// GENERATED by olve-build — do not edit.\n` +
|
|
75
|
-
`import type { HandlerRegistry, HarnessInfo } from "@olivervea/pipelines/harness";\n\n` +
|
|
76
|
-
`export const handlers: HandlerRegistry = {\n${registry}\n};\n\n` +
|
|
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` +
|
|
79
|
-
`export const harness: HarnessInfo = ${JSON.stringify(harness, null, 2)};\n`,
|
|
80
|
-
);
|
|
81
59
|
const entry = join(gen, 'entry.ts');
|
|
60
|
+
const config = `{ pipeline, codeHash: ${JSON.stringify(code)}, harness: ${JSON.stringify(harness)} }`;
|
|
82
61
|
writeFileSync(
|
|
83
62
|
entry,
|
|
84
63
|
`// GENERATED by olve-build — do not edit. Entry point of the step binary.\n` +
|
|
85
64
|
`import pipeline from ${JSON.stringify(relPosix(gen, project.pipelineFile))};\n` +
|
|
86
|
-
`import { runHarness } from "@olivervea/pipelines/harness";\n` +
|
|
87
|
-
`
|
|
88
|
-
`process.exit(await runHarness({ pipeline, handlers, handlerHashes, kindHashes, 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`,
|
|
89
67
|
);
|
|
90
68
|
return entry;
|
|
91
69
|
}
|
|
@@ -101,17 +79,9 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
|
|
|
101
79
|
const harness = harnessInfo();
|
|
102
80
|
|
|
103
81
|
const t0 = performance.now();
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
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
|
-
);
|
|
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`);
|
|
115
85
|
|
|
116
86
|
// 1. compile
|
|
117
87
|
const binary = join(out, 'step');
|
|
@@ -178,8 +148,7 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
|
|
|
178
148
|
target,
|
|
179
149
|
flags: BUILD_FLAGS,
|
|
180
150
|
harness,
|
|
181
|
-
|
|
182
|
-
kinds,
|
|
151
|
+
codeHash: code,
|
|
183
152
|
binarySha256: sha256File(binary),
|
|
184
153
|
archiveSha256: sha256File(archive),
|
|
185
154
|
}),
|
|
@@ -191,5 +160,5 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
|
|
|
191
160
|
log('olve-build: uploaded step.tar.zst');
|
|
192
161
|
}
|
|
193
162
|
if (opts.sentinels) await stdout(`${MANIFEST_BEGIN}\n${manifest}${MANIFEST_END}\n`);
|
|
194
|
-
return { binary, archive, manifestFile, manifest,
|
|
163
|
+
return { binary, archive, manifestFile, manifest, codeHash: code, harness };
|
|
195
164
|
}
|
package/src/build/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { canonicalJson } from '../util/canonical.ts';
|
|
5
5
|
import { build, BuildError, type Target } from './build.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { codeHash } from './hashes.ts';
|
|
7
7
|
import { harnessInfo, loadProject, ProjectError } from './project.ts';
|
|
8
8
|
|
|
9
9
|
const USAGE =
|
|
@@ -33,8 +33,11 @@ async function main(): Promise<number> {
|
|
|
33
33
|
const dir = positionals[0]!;
|
|
34
34
|
try {
|
|
35
35
|
if (values['hash-only']) {
|
|
36
|
-
const
|
|
37
|
-
await Bun.write(
|
|
36
|
+
const code = await codeHash(await loadProject(dir), harnessInfo());
|
|
37
|
+
await Bun.write(
|
|
38
|
+
Bun.stdout,
|
|
39
|
+
canonicalJson({ harness: harnessInfo(), codeHash: code }),
|
|
40
|
+
);
|
|
38
41
|
return 0;
|
|
39
42
|
}
|
|
40
43
|
if (!values.out) return (console.error(`--out is required\n${USAGE}`), 64);
|
package/src/build/hashes.ts
CHANGED
|
@@ -1,61 +1,33 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// outputs. Contract changes are versioned through the manifest's model/step entries, SDK changes
|
|
4
|
-
// through `harness`; both are combined in composeHandlerHash.
|
|
5
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
1
|
+
// The build's code hash: everything the pipeline runs, bundled once with the SDK external (the
|
|
2
|
+
// harness identity stands for it).
|
|
6
3
|
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';
|
|
11
|
-
import { composeHandlerHash, type HarnessInfo } from '../util/handler-hash.ts';
|
|
12
4
|
import { sha256Hex } from '../util/hash.ts';
|
|
5
|
+
import { composeCodeHash, type HarnessInfo } from '../util/code-hash.ts';
|
|
13
6
|
import type { Project } from './project.ts';
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
8
|
+
const sdkExternal: BunPlugin = {
|
|
9
|
+
name: 'olve-code-hash-external',
|
|
10
|
+
setup(b) {
|
|
11
|
+
b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({
|
|
12
|
+
path: args.path,
|
|
13
|
+
external: true,
|
|
14
|
+
}));
|
|
15
|
+
},
|
|
16
|
+
};
|
|
23
17
|
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
return {
|
|
27
|
-
name: 'olve-handler-hash-externals',
|
|
28
|
-
setup(b) {
|
|
29
|
-
b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({
|
|
30
|
-
path: args.path,
|
|
31
|
-
external: true,
|
|
32
|
-
}));
|
|
33
|
-
b.onResolve({ filter: /^\.{1,2}\// }, (args) => {
|
|
34
|
-
const base = resolve(dirname(args.importer), args.path);
|
|
35
|
-
for (const cand of [base, `${base}.ts`, join(base, 'index.ts')])
|
|
36
|
-
if (contracts.has(cand)) return { path: args.path, external: true };
|
|
37
|
-
return undefined;
|
|
38
|
-
});
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function bundleHash(
|
|
44
|
-
project: Project,
|
|
45
|
-
entry: string,
|
|
46
|
-
): Promise<{ hash: string; outputs: string[] }> {
|
|
47
|
-
const inside = !relative(project.dir, entry).startsWith('..');
|
|
18
|
+
async function bundleHash(project: Project): Promise<string> {
|
|
19
|
+
const entry = project.pipelineFile;
|
|
48
20
|
const r = await Bun.build({
|
|
49
21
|
entrypoints: [entry],
|
|
50
22
|
target: 'bun',
|
|
51
23
|
format: 'esm',
|
|
52
|
-
root:
|
|
24
|
+
root: project.dir,
|
|
53
25
|
naming: {
|
|
54
26
|
entry: '[dir]/[name].[ext]',
|
|
55
27
|
chunk: '[dir]/[name].[ext]',
|
|
56
28
|
asset: '[dir]/[name].[ext]',
|
|
57
29
|
},
|
|
58
|
-
plugins: [
|
|
30
|
+
plugins: [sdkExternal],
|
|
59
31
|
throw: false,
|
|
60
32
|
});
|
|
61
33
|
if (!r.success)
|
|
@@ -64,12 +36,10 @@ export async function bundleHash(
|
|
|
64
36
|
`bundling ${entry} failed:\n${r.logs.map(String).join('\n')}`,
|
|
65
37
|
);
|
|
66
38
|
const parts: string[] = [];
|
|
67
|
-
const outputs: string[] = [];
|
|
68
39
|
for (const o of [...r.outputs].sort((a, b) =>
|
|
69
40
|
a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
|
|
70
41
|
)) {
|
|
71
42
|
const name = o.path.replace(/^\.\//, '');
|
|
72
|
-
outputs.push(name);
|
|
73
43
|
const bytes = new Uint8Array(await o.arrayBuffer());
|
|
74
44
|
// Bun prefixes each module with a `// <path>` comment relative to the *process cwd*; drop those
|
|
75
45
|
// so the hash depends only on code, not on where the repo or the build tool runs.
|
|
@@ -78,52 +48,9 @@ export async function bundleHash(
|
|
|
78
48
|
: bytes;
|
|
79
49
|
parts.push(`${name}\0${sha256Hex(content)}`);
|
|
80
50
|
}
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function handlerHashes(
|
|
85
|
-
project: Project,
|
|
86
|
-
harness: HarnessInfo,
|
|
87
|
-
): Promise<Record<string, HandlerHash>> {
|
|
88
|
-
const out: Record<string, HandlerHash> = {};
|
|
89
|
-
for (const [name, file] of project.handlers) {
|
|
90
|
-
const b = await bundleHash(project, file);
|
|
91
|
-
out[name] = {
|
|
92
|
-
bundleHash: b.hash,
|
|
93
|
-
handlerHash: composeHandlerHash(b.hash, harness),
|
|
94
|
-
outputs: b.outputs,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
return out;
|
|
51
|
+
return 'sha256:' + sha256Hex(parts.join('\n'));
|
|
98
52
|
}
|
|
99
53
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* (each module bundled like a handler: the SDK and contract files external).
|
|
103
|
-
*/
|
|
104
|
-
export async function kindHashes(
|
|
105
|
-
project: Project,
|
|
106
|
-
pipeline: unknown,
|
|
107
|
-
): Promise<Record<string, string>> {
|
|
108
|
-
const origins = new Map<string, Set<string>>();
|
|
109
|
-
for (const s of allSteps(pipelineData(pipeline))) {
|
|
110
|
-
if (!isImplementation(s)) continue;
|
|
111
|
-
const files = originsOf(s).filter((f) => !project.contractFiles.includes(f));
|
|
112
|
-
if (files.length === 0)
|
|
113
|
-
throw new Error(
|
|
114
|
-
`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)`,
|
|
115
|
-
);
|
|
116
|
-
const set = origins.get(s.kind) ?? new Set<string>();
|
|
117
|
-
for (const f of files) set.add(f);
|
|
118
|
-
origins.set(s.kind, set);
|
|
119
|
-
}
|
|
120
|
-
const out: Record<string, string> = {};
|
|
121
|
-
for (const [kind, files] of [...origins].sort(([a], [b]) =>
|
|
122
|
-
a < b ? -1 : a > b ? 1 : 0,
|
|
123
|
-
)) {
|
|
124
|
-
const parts: string[] = [];
|
|
125
|
-
for (const f of [...files].sort()) parts.push((await bundleHash(project, f)).hash);
|
|
126
|
-
out[kind] = 'sha256:' + sha256Hex(parts.join('\n'));
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
54
|
+
export async function codeHash(project: Project, harness: HarnessInfo): Promise<string> {
|
|
55
|
+
return composeCodeHash(await bundleHash(project), harness);
|
|
129
56
|
}
|
package/src/build/project.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
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';
|
|
5
4
|
import { sha256Hex } from '../util/hash.ts';
|
|
6
|
-
import type { HarnessInfo } from '../util/
|
|
5
|
+
import type { HarnessInfo } from '../util/code-hash.ts';
|
|
7
6
|
|
|
8
7
|
export const SDK_ROOT = resolve(import.meta.dir, '../..');
|
|
9
8
|
|
|
10
9
|
export interface Project {
|
|
11
10
|
dir: string;
|
|
12
11
|
pipelineFile: string;
|
|
13
|
-
/** Contract files (excluded from handler bundles): pipeline.ts, models.ts. */
|
|
14
|
-
contractFiles: string[];
|
|
15
|
-
/** step name → absolute handler file, sorted by step name. The step is the one the file's `implement(step, …)` names. */
|
|
16
|
-
handlers: [string, string][];
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
export class ProjectError extends Error {
|
|
@@ -24,35 +19,28 @@ export async function loadProject(dirArg: string): Promise<Project> {
|
|
|
24
19
|
const dir = resolve(dirArg);
|
|
25
20
|
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
26
21
|
throw new ProjectError(`${dirArg} is not a directory`);
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
return { dir, pipelineFile: entryOf(dirArg, dir) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The file `main` in the package's package.json names. */
|
|
26
|
+
function entryOf(dirArg: string, dir: string): string {
|
|
27
|
+
const pkg = join(dir, 'package.json');
|
|
28
|
+
const main = existsSync(pkg)
|
|
29
|
+
? (JSON.parse(readFileSync(pkg, 'utf8')) as { main?: string }).main
|
|
30
|
+
: undefined;
|
|
31
|
+
if (!main)
|
|
29
32
|
throw new ProjectError(
|
|
30
|
-
`${join(dirArg, '
|
|
33
|
+
`${join(dirArg, 'package.json')} must name the entry point in \`main\` (a file that default-exports pipeline({...}))`,
|
|
31
34
|
);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.filter(
|
|
39
|
-
(f) =>
|
|
40
|
-
f.endsWith('.ts') &&
|
|
41
|
-
!f.endsWith('.test.ts') &&
|
|
42
|
-
!f.endsWith('.d.ts') &&
|
|
43
|
-
statSync(join(hdir, f)).isFile(),
|
|
44
|
-
)
|
|
45
|
-
.sort()
|
|
46
|
-
: [];
|
|
47
|
-
return {
|
|
48
|
-
dir,
|
|
49
|
-
pipelineFile,
|
|
50
|
-
contractFiles,
|
|
51
|
-
handlers: await resolveHandlers(hdir, files),
|
|
52
|
-
};
|
|
35
|
+
const entry = join(dir, main);
|
|
36
|
+
if (!existsSync(entry))
|
|
37
|
+
throw new ProjectError(
|
|
38
|
+
`${join(dirArg, main)} not found (it must default-export pipeline({...}))`,
|
|
39
|
+
);
|
|
40
|
+
return entry;
|
|
53
41
|
}
|
|
54
42
|
|
|
55
|
-
/** The default export of
|
|
43
|
+
/** The default export of the entry point. */
|
|
56
44
|
export async function loadPipeline(project: Project): Promise<unknown> {
|
|
57
45
|
const mod = (await import(project.pipelineFile)) as { default?: unknown };
|
|
58
46
|
if (!mod.default)
|
|
@@ -62,37 +50,6 @@ export async function loadPipeline(project: Project): Promise<unknown> {
|
|
|
62
50
|
return mod.default;
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
/** Pairs each handler file with the step its default export implements (file names don't matter). */
|
|
66
|
-
async function resolveHandlers(
|
|
67
|
-
hdir: string,
|
|
68
|
-
files: string[],
|
|
69
|
-
): Promise<[string, string][]> {
|
|
70
|
-
const byStep = new Map<string, string>();
|
|
71
|
-
for (const f of files) {
|
|
72
|
-
const file = join(hdir, f);
|
|
73
|
-
let impl: unknown;
|
|
74
|
-
try {
|
|
75
|
-
impl = ((await import(file)) as { default?: unknown }).default;
|
|
76
|
-
} catch (e) {
|
|
77
|
-
throw new ProjectError(
|
|
78
|
-
`handlers/${f} failed to load: ${(e as Error)?.message ?? String(e)}`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
if (!isImplementation(impl))
|
|
82
|
-
throw new ProjectError(
|
|
83
|
-
`handlers/${f} must default-export implement(<step>, …); put shared code in a subfolder such as handlers/lib/`,
|
|
84
|
-
);
|
|
85
|
-
const step = (impl.step as { name: string }).name;
|
|
86
|
-
const other = byStep.get(step);
|
|
87
|
-
if (other)
|
|
88
|
-
throw new ProjectError(
|
|
89
|
-
`handlers/${other.slice(hdir.length + 1)} and handlers/${f} both implement step '${step}'`,
|
|
90
|
-
);
|
|
91
|
-
byStep.set(step, file);
|
|
92
|
-
}
|
|
93
|
-
return [...byStep].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
53
|
function listFiles(dir: string): string[] {
|
|
97
54
|
const out: string[] = [];
|
|
98
55
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
package/src/builder/pipeline.ts
CHANGED
|
@@ -314,7 +314,7 @@ export function pipelineData(p: unknown): PipelineData {
|
|
|
314
314
|
: undefined;
|
|
315
315
|
if (!d)
|
|
316
316
|
throw new TypeError(
|
|
317
|
-
'not a pipeline (
|
|
317
|
+
'not a pipeline (the entry point must default-export pipeline({...})...)',
|
|
318
318
|
);
|
|
319
319
|
return d as PipelineData;
|
|
320
320
|
}
|
|
@@ -553,7 +553,7 @@ export class Pipeline<In extends AnyModel, A extends AvailMap> {
|
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
/** Start a pipeline definition.
|
|
556
|
+
/** Start a pipeline definition. The entry point must default-export the result. */
|
|
557
557
|
export function pipeline<In extends AnyModel>(
|
|
558
558
|
def: PipelineDef<In>,
|
|
559
559
|
): Pipeline<In, InitialAvail<In>> {
|
package/src/core/brands.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// a dev runner and the user's installed package — still recognise each other's objects.
|
|
3
3
|
export const STEP = Symbol.for('olve.pipelines.step');
|
|
4
4
|
export const IMPLEMENTATION = Symbol.for('olve.pipelines.implementation');
|
|
5
|
-
export const ORIGIN = Symbol.for('olve.pipelines.origin');
|
|
6
5
|
export const SKIPPED = Symbol.for('olve.pipelines.skipped');
|
|
7
6
|
export const PIPELINE = Symbol.for('olve.pipelines.pipeline');
|
|
8
7
|
export const REF_PATH = Symbol.for('olve.pipelines.refPath');
|
package/src/core/handlers.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IMPLEMENTATION,
|
|
3
|
-
ORIGIN,
|
|
4
3
|
SKIPPED,
|
|
5
4
|
type ImplementationBrand,
|
|
6
5
|
type SkippedBrand,
|
|
7
6
|
} from './brands.ts';
|
|
8
7
|
import type { Ctx } from '../ctx/types.ts';
|
|
9
|
-
import { callerFile, originsOf } from './origin.ts';
|
|
10
8
|
import type { AnyStep, ConfigOf, OutputValue, SecretsOf, StepInput } from './steps.ts';
|
|
11
9
|
import type { Awaitable } from './types.ts';
|
|
12
10
|
|
|
@@ -32,7 +30,7 @@ export type Handler<S extends AnyStep> = (
|
|
|
32
30
|
ctx: Ctx<SecretsOf<S>, ConfigOf<S>>,
|
|
33
31
|
) => Awaitable<HandlerResult<S>>;
|
|
34
32
|
|
|
35
|
-
/** A step with its handler. It is itself a step: stage it
|
|
33
|
+
/** A step with its handler. It is itself a step: stage it like any value. */
|
|
36
34
|
export type Implementation<S extends AnyStep = AnyStep> = S & {
|
|
37
35
|
readonly [ImplementationBrand]: true;
|
|
38
36
|
/** The step as defined. */
|
|
@@ -50,8 +48,8 @@ type NoExtraFields<R, T> = R extends Skipped | void
|
|
|
50
48
|
};
|
|
51
49
|
|
|
52
50
|
/**
|
|
53
|
-
* Implement a step. Input, ctx and return types come from the step definition alone.
|
|
54
|
-
*
|
|
51
|
+
* Implement a step. Input, ctx and return types come from the step definition alone. Stage the
|
|
52
|
+
* result directly; a reusable step gives itself a `kind`.
|
|
55
53
|
*/
|
|
56
54
|
export function implement<S extends AnyStep, R extends HandlerResult<S>>(
|
|
57
55
|
step: S &
|
|
@@ -67,15 +65,9 @@ export function implement<S extends AnyStep, R extends HandlerResult<S>>(
|
|
|
67
65
|
throw new TypeError(
|
|
68
66
|
`step '${(step as AnyStep).name}' already carries its implementation`,
|
|
69
67
|
);
|
|
70
|
-
const origins = [
|
|
71
|
-
...new Set(
|
|
72
|
-
[...originsOf(step), callerFile()].filter((f): f is string => f !== undefined),
|
|
73
|
-
),
|
|
74
|
-
];
|
|
75
68
|
return Object.freeze({
|
|
76
69
|
...step,
|
|
77
70
|
[IMPLEMENTATION]: true,
|
|
78
|
-
[ORIGIN]: origins,
|
|
79
71
|
step,
|
|
80
72
|
handler: handler as unknown as Handler<S>,
|
|
81
73
|
}) as unknown as Implementation<S>;
|
package/src/core/steps.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import { callerFile } from './origin.ts';
|
|
2
|
+
import { STEP, type StepBrand } from './brands.ts';
|
|
4
3
|
import type { AnyModel, Infer, Optional } from './models.ts';
|
|
5
4
|
import type { External } from './external.ts';
|
|
6
5
|
import type { AnyConfigEntry } from './config.ts';
|
|
@@ -29,8 +28,7 @@ export type InferEntry<E> =
|
|
|
29
28
|
export type StepInput<I> = Simplify<{ -readonly [K in keyof I]: InferEntry<I[K]> }>;
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
* A label for what a step is, shown in the UI. `custom`
|
|
33
|
-
* `handlers/`; a step that carries its implementation (`implement(step, …)` staged directly) names its
|
|
31
|
+
* A label for what a step is, shown in the UI. `custom` is the default; a reusable step names its
|
|
34
32
|
* kind, e.g. `gitHubSource`. Packages prefix theirs (`acme/deploy`).
|
|
35
33
|
*/
|
|
36
34
|
export type StepKind = string;
|
|
@@ -113,7 +111,7 @@ export interface StepDef<
|
|
|
113
111
|
settings?: Readonly<Record<string, unknown>>;
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
function makeStep(def: Record<string, unknown
|
|
114
|
+
function makeStep(def: Record<string, unknown>): AnyStep {
|
|
117
115
|
const config = new Map<string, AnyConfigEntry>();
|
|
118
116
|
for (const e of (def.config as readonly AnyConfigEntry[] | undefined) ?? [])
|
|
119
117
|
if (!config.has(e.name)) config.set(e.name, e);
|
|
@@ -132,7 +130,6 @@ function makeStep(def: Record<string, unknown>, origin: string | undefined): Any
|
|
|
132
130
|
...(def.retries !== undefined ? { retries: def.retries } : {}),
|
|
133
131
|
...(def.clusterAccess === true ? { clusterAccess: true } : {}),
|
|
134
132
|
...(def.settings !== undefined ? { settings: def.settings } : {}),
|
|
135
|
-
[ORIGIN]: origin === undefined ? [] : [origin],
|
|
136
133
|
};
|
|
137
134
|
return Object.freeze(s) as unknown as AnyStep;
|
|
138
135
|
}
|
|
@@ -144,8 +141,8 @@ export function isStep(v: unknown): v is AnyStep {
|
|
|
144
141
|
}
|
|
145
142
|
|
|
146
143
|
/**
|
|
147
|
-
* Define a step: a contract (name, image, inputs, output, secrets).
|
|
148
|
-
*
|
|
144
|
+
* Define a step: a contract (name, image, inputs, output, secrets). `implement(step, fn)` gives it
|
|
145
|
+
* its handler; that is what a pipeline stages.
|
|
149
146
|
*/
|
|
150
147
|
export function step<
|
|
151
148
|
const N extends string,
|
|
@@ -154,5 +151,5 @@ export function step<
|
|
|
154
151
|
const Sec extends readonly Secret[] = [],
|
|
155
152
|
const Cfg extends readonly AnyConfigEntry[] = [],
|
|
156
153
|
>(def: StepDef<N, I, O, Sec, Cfg>): Step<N, I, O, Sec[number]['name'], Cfg[number]> {
|
|
157
|
-
return makeStep(def as never
|
|
154
|
+
return makeStep(def as never) as never;
|
|
158
155
|
}
|
package/src/describe/describe.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// `describe`: evaluates the
|
|
2
|
-
// every describe-time check. All problems are collected and reported together.
|
|
1
|
+
// `describe`: evaluates the pipeline into the manifest and runs every describe-time check. All problems are collected and reported together.
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import { CONFIG_NAME, isConfigEntry, SECRET_REF_KIND } from '../core/config.ts';
|
|
5
4
|
import { CONFIG_CASE_KEY, CONFIG_REF_KEY } from '../core/config-refs.ts';
|
|
@@ -18,7 +17,7 @@ import {
|
|
|
18
17
|
} from '../builder/pipeline.ts';
|
|
19
18
|
import { FailureContext } from '../library/models.ts';
|
|
20
19
|
import { canonicalize, canonicalJson } from '../util/canonical.ts';
|
|
21
|
-
import {
|
|
20
|
+
import type { HarnessInfo } from '../util/code-hash.ts';
|
|
22
21
|
import { analyzeMapperSource } from './mapper-source.ts';
|
|
23
22
|
import { MapperError, recordMapper, type Mapping } from './record.ts';
|
|
24
23
|
import {
|
|
@@ -41,12 +40,8 @@ export class DescribeError extends Error {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export interface DescribeOptions {
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
/** Step name → handler hash (from the build; includes the harness identity). */
|
|
47
|
-
handlerHashes: Readonly<Record<string, string>>;
|
|
48
|
-
/** Kind → hash of the code that implements the steps of that kind which carry their implementation (from the build). */
|
|
49
|
-
kindHashes?: Readonly<Record<string, string>>;
|
|
43
|
+
/** Hash of the build (from the build; includes the harness identity). */
|
|
44
|
+
codeHash?: string;
|
|
50
45
|
harness: HarnessInfo;
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -129,7 +124,6 @@ function literalValues(field: JsonSchema | undefined): unknown[] | undefined {
|
|
|
129
124
|
export function describePipeline(pipeline: unknown, opts: DescribeOptions): ManifestT {
|
|
130
125
|
const data = pipelineData(pipeline);
|
|
131
126
|
const problems: string[] = [...data.problems];
|
|
132
|
-
const handlerNames = new Set(opts.handlerNames);
|
|
133
127
|
|
|
134
128
|
// ── models ──
|
|
135
129
|
const models = new Map<string, { schemas: ModelSchemasT; key: string }>();
|
|
@@ -232,20 +226,10 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
232
226
|
const prev = placements.get(s.name) ?? [];
|
|
233
227
|
if (prev.some((p) => !exclusive(p.cases, cases)))
|
|
234
228
|
problems.push(`duplicate step name '${s.name}'`);
|
|
235
|
-
else if (prev.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
240
|
-
)
|
|
241
|
-
problems.push(
|
|
242
|
-
`step '${s.name}': two different steps with handler files share the name across select cases; handlers pair by name, so use one step or different names`,
|
|
243
|
-
);
|
|
244
|
-
if (prev.some((p) => (p.step.output?.name ?? null) !== (s.output?.name ?? null)))
|
|
245
|
-
problems.push(
|
|
246
|
-
`step '${s.name}': its select cases must give it the same output model`,
|
|
247
|
-
);
|
|
248
|
-
}
|
|
229
|
+
else if (prev.some((p) => (p.step.output?.name ?? null) !== (s.output?.name ?? null)))
|
|
230
|
+
problems.push(
|
|
231
|
+
`step '${s.name}': its select cases must give it the same output model`,
|
|
232
|
+
);
|
|
249
233
|
placements.set(s.name, [...prev, { step: s, cases }]);
|
|
250
234
|
if (!steps.has(s.name)) steps.set(s.name, s);
|
|
251
235
|
};
|
|
@@ -439,15 +423,10 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
439
423
|
`${at}: config '${name}' references secret '${ref}' (secretRef), so the step must declare it in secrets`,
|
|
440
424
|
);
|
|
441
425
|
}
|
|
442
|
-
let handlerHash: string;
|
|
443
426
|
if (settingsProblem !== undefined)
|
|
444
427
|
problems.push(`${at}: invalid ${s.kind} settings (${settingsProblem})`);
|
|
445
428
|
if (typeof s.kind !== 'string' || s.kind === '')
|
|
446
429
|
problems.push(`${at}: kind must be a non-empty string`);
|
|
447
|
-
if (carried && s.kind === 'custom')
|
|
448
|
-
problems.push(
|
|
449
|
-
`${at} carries its implementation, so it needs a kind (step({ kind: "…", … })); a step with kind custom has its handler in handlers/`,
|
|
450
|
-
);
|
|
451
430
|
for (const { ref, case: kase } of refs) {
|
|
452
431
|
const [entry, ...path] = ref.split('.') as [string, ...string[]];
|
|
453
432
|
if (kase !== undefined && !cases.some((c) => c.cond === kase))
|
|
@@ -463,20 +442,8 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
463
442
|
`${at}: config reference '${ref}': config '${entry}' has no field '${path.join('.')}'`,
|
|
464
443
|
);
|
|
465
444
|
}
|
|
466
|
-
if (carried)
|
|
467
|
-
|
|
468
|
-
if (opts.kindHashes && !code)
|
|
469
|
-
problems.push(`${at}: no hash was computed for the code of kind '${s.kind}'`);
|
|
470
|
-
handlerHash = libraryHandlerHash(s.kind, settings, opts.harness, code);
|
|
471
|
-
} else {
|
|
472
|
-
if (!handlerNames.has(s.name))
|
|
473
|
-
problems.push(
|
|
474
|
-
`${at} has no handler (add a handlers/*.ts default-exporting implement(${camel(s.name)}, …))`,
|
|
475
|
-
);
|
|
476
|
-
handlerHash = opts.handlerHashes[s.name] ?? '';
|
|
477
|
-
if (handlerNames.has(s.name) && !handlerHash)
|
|
478
|
-
problems.push(`${at}: no handler hash was computed`);
|
|
479
|
-
}
|
|
445
|
+
if (!carried)
|
|
446
|
+
problems.push(`${at} has no implementation (stage implement(step({...}), fn))`);
|
|
480
447
|
if (typeof s.description !== 'string')
|
|
481
448
|
problems.push(`${at}: description must be a string (it may be empty)`);
|
|
482
449
|
if (
|
|
@@ -496,7 +463,6 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
496
463
|
input,
|
|
497
464
|
output: s.output ? useModel(s.output, `${at} output`) : null,
|
|
498
465
|
secrets: secretNames,
|
|
499
|
-
handlerHash: handlerHash || 'sha256:' + '0'.repeat(64),
|
|
500
466
|
...(s.timeoutSeconds !== undefined ? { timeoutSeconds: s.timeoutSeconds } : {}),
|
|
501
467
|
...(s.retries !== undefined ? { retries: s.retries } : {}),
|
|
502
468
|
...(settings !== undefined
|
|
@@ -737,17 +703,6 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
737
703
|
})
|
|
738
704
|
.filter((x) => x !== undefined);
|
|
739
705
|
|
|
740
|
-
// ── handlers without a step ──
|
|
741
|
-
for (const h of handlerNames) {
|
|
742
|
-
const s = steps.get(h);
|
|
743
|
-
if (!s)
|
|
744
|
-
problems.push(`a handler implements step '${h}', which is not in the pipeline`);
|
|
745
|
-
else if (isImplementation(s))
|
|
746
|
-
problems.push(
|
|
747
|
-
`a handler implements '${h}', a ${s.kind} step that carries its own implementation`,
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
706
|
// ── triggers ──
|
|
752
707
|
const triggerNames = new Set<string>();
|
|
753
708
|
const triggers: TriggerT[] = data.triggers.map((t) => {
|
|
@@ -818,6 +773,7 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
818
773
|
input: inputModel.name,
|
|
819
774
|
...(Object.keys(retention).length ? { retention } : {}),
|
|
820
775
|
harness: opts.harness,
|
|
776
|
+
codeHash: opts.codeHash ?? 'sha256:' + '0'.repeat(64),
|
|
821
777
|
models: Object.fromEntries([...models].map(([k, v]) => [k, v.schemas])),
|
|
822
778
|
secrets: [...secrets]
|
|
823
779
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
@@ -948,7 +904,3 @@ interface CaseOf {
|
|
|
948
904
|
value: string;
|
|
949
905
|
cond: string;
|
|
950
906
|
}
|
|
951
|
-
|
|
952
|
-
function camel(name: string): string {
|
|
953
|
-
return name.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
|
954
|
-
}
|
|
@@ -94,9 +94,7 @@ export const StepDescriptor = z
|
|
|
94
94
|
kind: z
|
|
95
95
|
.string()
|
|
96
96
|
.min(1)
|
|
97
|
-
.describe(
|
|
98
|
-
"'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.",
|
|
99
|
-
),
|
|
97
|
+
.describe("A label for the step's kind: 'custom' unless the step names one."),
|
|
100
98
|
image: z.string().min(1).describe('Container image the step runs in.'),
|
|
101
99
|
inputTypes: z
|
|
102
100
|
.record(z.string(), InputType)
|
|
@@ -114,16 +112,13 @@ export const StepDescriptor = z
|
|
|
114
112
|
.describe(
|
|
115
113
|
'Declared secrets (sorted). Mount exactly these, as env vars of the same name.',
|
|
116
114
|
),
|
|
117
|
-
handlerHash: hash.describe(
|
|
118
|
-
'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.',
|
|
119
|
-
),
|
|
120
115
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
121
116
|
retries: z.number().int().nonnegative().optional(),
|
|
122
117
|
config: z
|
|
123
118
|
.record(z.string(), Json)
|
|
124
119
|
.optional()
|
|
125
120
|
.describe(
|
|
126
|
-
'The step\'s settings (display only
|
|
121
|
+
'The step\'s settings (display only). A config reference is recorded as {"$config": "<entry>.<field>…"}; the entry is in configEntries.',
|
|
127
122
|
),
|
|
128
123
|
configEntries: z
|
|
129
124
|
.array(configName)
|
|
@@ -276,7 +271,10 @@ export const Manifest = z
|
|
|
276
271
|
sdk: z.string().describe('@olivervea/pipelines package version.'),
|
|
277
272
|
sdkHash: hash.describe('Hash of the SDK runtime sources + zod version.'),
|
|
278
273
|
})
|
|
279
|
-
.describe('Harness identity. Already folded into
|
|
274
|
+
.describe('Harness identity. Already folded into codeHash.'),
|
|
275
|
+
codeHash: hash.describe(
|
|
276
|
+
'Hash of the whole build: the pipeline and its code bundled, plus the harness. Any code change makes a new pipeline version.',
|
|
277
|
+
),
|
|
280
278
|
models: z
|
|
281
279
|
.record(modelName, ModelSchemas)
|
|
282
280
|
.describe('Every model referenced by the pipeline (input, step inputs/outputs).'),
|
package/src/dev/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { join, resolve } from 'node:path';
|
|
|
15
15
|
import { parseArgs } from 'node:util';
|
|
16
16
|
import { runHarness, type HarnessConfig } from '../harness/main.ts';
|
|
17
17
|
import { EXIT } from '../harness/exit.ts';
|
|
18
|
-
import {
|
|
18
|
+
import { codeHash } from '../build/hashes.ts';
|
|
19
19
|
import {
|
|
20
20
|
harnessInfo,
|
|
21
21
|
loadPipeline,
|
|
@@ -34,19 +34,9 @@ export async function devConfig(
|
|
|
34
34
|
const project = await loadProject(dir);
|
|
35
35
|
const pipeline = await loadPipeline(project);
|
|
36
36
|
const harness = harnessInfo();
|
|
37
|
-
const hashes = withHashes ? await handlerHashes(project, harness) : {};
|
|
38
37
|
return {
|
|
39
38
|
pipeline,
|
|
40
|
-
|
|
41
|
-
project.handlers.map(([name, file]) => [
|
|
42
|
-
name,
|
|
43
|
-
() => import(file) as Promise<{ default: unknown }>,
|
|
44
|
-
]),
|
|
45
|
-
),
|
|
46
|
-
handlerHashes: Object.fromEntries(
|
|
47
|
-
Object.entries(hashes).map(([k, v]) => [k, v.handlerHash]),
|
|
48
|
-
),
|
|
49
|
-
...(withHashes ? { kindHashes: await kindHashes(project, pipeline) } : {}),
|
|
39
|
+
...(withHashes ? { codeHash: await codeHash(project, harness) } : {}),
|
|
50
40
|
harness,
|
|
51
41
|
};
|
|
52
42
|
}
|
package/src/harness/exit.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const EXIT = {
|
|
|
11
11
|
outputInvalid: 66,
|
|
12
12
|
/** Uploading result.tar.gz to OLVE_RESULT_URL failed (after retries). The envelope was still printed. */
|
|
13
13
|
uploadFailed: 74,
|
|
14
|
-
/** The pipeline is invalid: describe problems,
|
|
14
|
+
/** The pipeline is invalid: describe problems, a step without an implementation. */
|
|
15
15
|
configInvalid: 78,
|
|
16
16
|
/** Cancelled by SIGINT / SIGTERM (128 + signal). */
|
|
17
17
|
sigint: 130,
|
package/src/harness/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ export {
|
|
|
5
5
|
allSteps,
|
|
6
6
|
resolveStepFunction,
|
|
7
7
|
type HarnessConfig,
|
|
8
|
-
type HandlerRegistry,
|
|
9
8
|
type HarnessIO,
|
|
10
9
|
} from './main.ts';
|
|
11
10
|
export { EXIT } from './exit.ts';
|
|
@@ -19,4 +18,4 @@ export {
|
|
|
19
18
|
export { RESULT_BEGIN, RESULT_END, MANIFEST_BEGIN, MANIFEST_END } from './result.ts';
|
|
20
19
|
export { describePipeline, DescribeError, manifestText } from '../describe/describe.ts';
|
|
21
20
|
export type { ManifestT as Manifest } from '../describe/manifest-schema.ts';
|
|
22
|
-
export type { HarnessInfo } from '../util/
|
|
21
|
+
export type { HarnessInfo } from '../util/code-hash.ts';
|
package/src/harness/main.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { WebhookEvent } from '../core/webhooks.ts';
|
|
|
11
11
|
import { pipelineData, type PipelineData, type StageNode } from '../builder/pipeline.ts';
|
|
12
12
|
import { createCtx, formatLog } from '../ctx/index.ts';
|
|
13
13
|
import { DescribeError, describePipeline, manifestText } from '../describe/describe.ts';
|
|
14
|
-
import type { HarnessInfo } from '../util/
|
|
14
|
+
import type { HarnessInfo } from '../util/code-hash.ts';
|
|
15
15
|
import { callbackUrlsFromEnv, ENV, paths, runInfoFromEnv } from './env.ts';
|
|
16
16
|
import { EXIT } from './exit.ts';
|
|
17
17
|
import {
|
|
@@ -31,18 +31,11 @@ import {
|
|
|
31
31
|
} from './result.ts';
|
|
32
32
|
import { formatIssues, parseStrict } from './validate.ts';
|
|
33
33
|
|
|
34
|
-
/** step name → lazy import of `handlers/<name>.ts`. Literal import() paths so Bun bundles them. */
|
|
35
|
-
export type HandlerRegistry = Readonly<
|
|
36
|
-
Record<string, () => Promise<{ default: unknown }>>
|
|
37
|
-
>;
|
|
38
|
-
|
|
39
34
|
export interface HarnessConfig {
|
|
40
|
-
/** Default export of
|
|
35
|
+
/** Default export of the entry point. */
|
|
41
36
|
pipeline: unknown;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/** Kind → hash of the code of the steps of that kind that carry their implementation. */
|
|
45
|
-
kindHashes?: Readonly<Record<string, string>>;
|
|
37
|
+
/** Hash of the build (from the build; includes the harness identity). */
|
|
38
|
+
codeHash?: string;
|
|
46
39
|
harness: HarnessInfo;
|
|
47
40
|
}
|
|
48
41
|
|
|
@@ -130,26 +123,10 @@ export function cancellation(): { signal: AbortSignal; dispose(): void } {
|
|
|
130
123
|
};
|
|
131
124
|
}
|
|
132
125
|
|
|
133
|
-
/**
|
|
134
|
-
export
|
|
135
|
-
step: AnyStep,
|
|
136
|
-
handlers: HandlerRegistry,
|
|
137
|
-
): Promise<StepFunction | string> {
|
|
126
|
+
/** The function `step` carries, or what is wrong. */
|
|
127
|
+
export function resolveStepFunction(step: AnyStep): StepFunction | string {
|
|
138
128
|
if (isImplementation(step)) return step.handler as unknown as StepFunction;
|
|
139
|
-
|
|
140
|
-
if (!load) return `step '${step.name}' has no handler`;
|
|
141
|
-
let mod: { default: unknown };
|
|
142
|
-
try {
|
|
143
|
-
mod = await load();
|
|
144
|
-
} catch (e) {
|
|
145
|
-
return `the handler of step '${step.name}' failed to load: ${(e as Error)?.stack ?? String(e)}`;
|
|
146
|
-
}
|
|
147
|
-
const impl = mod.default;
|
|
148
|
-
if (!isImplementation(impl))
|
|
149
|
-
return `the handler of step '${step.name}' must default-export implement(<step '${step.name}'>, …)`;
|
|
150
|
-
if (impl.step !== step)
|
|
151
|
-
return `the handler of step '${step.name}' implements a different step object (it must be implement(<the '${step.name}' step from pipeline.ts>, …))`;
|
|
152
|
-
return impl.handler as unknown as StepFunction;
|
|
129
|
+
return `step '${step.name}' has no implementation (stage implement(step({...}), fn))`;
|
|
153
130
|
}
|
|
154
131
|
|
|
155
132
|
/** Writes output.json, prints the sentinel block and uploads result.tar.gz when OLVE_RESULT_URL is set. */
|
|
@@ -190,9 +167,7 @@ async function describeMode(
|
|
|
190
167
|
try {
|
|
191
168
|
const text = manifestText(
|
|
192
169
|
describePipeline(cfg.pipeline, {
|
|
193
|
-
|
|
194
|
-
handlerHashes: cfg.handlerHashes,
|
|
195
|
-
kindHashes: cfg.kindHashes,
|
|
170
|
+
codeHash: cfg.codeHash,
|
|
196
171
|
harness: cfg.harness,
|
|
197
172
|
}),
|
|
198
173
|
);
|
|
@@ -216,7 +191,7 @@ async function runMode(
|
|
|
216
191
|
const data = pipelineData(cfg.pipeline);
|
|
217
192
|
const step = findStep(data, name, selectedCases());
|
|
218
193
|
if (typeof step === 'string') return (io.stderr(step), EXIT.usage);
|
|
219
|
-
const fn =
|
|
194
|
+
const fn = resolveStepFunction(step);
|
|
220
195
|
if (typeof fn === 'string') return (io.stderr(fn), EXIT.configInvalid);
|
|
221
196
|
|
|
222
197
|
const p = paths();
|
package/src/testing/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ export type TestResult<S extends AnyStep> = {
|
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Runs
|
|
49
|
+
* Runs a step that carries its implementation with `input`.
|
|
50
50
|
* Input is validated like `input.json`, the result like a real run: an invalid output gives
|
|
51
51
|
* `{ status: "failed", error: { kind: "output" } }`, a throwing handler `kind: "handler"`.
|
|
52
52
|
*/
|
|
@@ -57,7 +57,7 @@ export async function testHandler<S extends AnyStep>(
|
|
|
57
57
|
): Promise<TestResult<S>> {
|
|
58
58
|
if (!isImplementation(target))
|
|
59
59
|
throw new TypeError(
|
|
60
|
-
|
|
60
|
+
'testHandler expects implement(…): a step that carries its implementation',
|
|
61
61
|
);
|
|
62
62
|
const step: AnyStep = target.step;
|
|
63
63
|
const fn = target.handler as unknown as StepFunction;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { canonicalJson } from './canonical.ts';
|
|
2
|
+
import { sha256Hex } from './hash.ts';
|
|
3
|
+
|
|
4
|
+
/** Identity of the harness compiled into a binary. */
|
|
5
|
+
export interface HarnessInfo {
|
|
6
|
+
/** Bun version (runtime compiled into the binary). */
|
|
7
|
+
bun: string;
|
|
8
|
+
/** @olivervea/pipelines package version. */
|
|
9
|
+
sdk: string;
|
|
10
|
+
/** `sha256:` hash over the SDK runtime sources and the zod version. */
|
|
11
|
+
sdkHash: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The hash of a whole build: its code bundled with the SDK external, combined with the harness. */
|
|
15
|
+
export function composeCodeHash(bundleHash: string, harness: HarnessInfo): string {
|
|
16
|
+
return 'sha256:' + sha256Hex(canonicalJson({ bundle: bundleHash, harness }));
|
|
17
|
+
}
|
package/src/core/origin.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { ORIGIN } from './brands.ts';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The file that called the function calling this one, from the stack. A tail call drops its caller's
|
|
5
|
-
* frame, so this can name a module further up; the build only uses it to choose what to hash.
|
|
6
|
-
*/
|
|
7
|
-
export function callerFile(): string | undefined {
|
|
8
|
-
const files = (new Error().stack ?? '')
|
|
9
|
-
.split('\n')
|
|
10
|
-
.slice(1)
|
|
11
|
-
.map((f) => f.match(FRAME)?.[1]);
|
|
12
|
-
const own = files[0];
|
|
13
|
-
const callee = files[1];
|
|
14
|
-
const file = files.slice(2).find((f) => f && f !== own && f !== callee) ?? undefined;
|
|
15
|
-
return file?.startsWith('file://') ? new URL(file).pathname : file;
|
|
16
|
-
}
|
|
17
|
-
const FRAME = /\(?((?:file:\/\/)?\/[^()]*?):\d+(?::\d+)?\)?\s*$/;
|
|
18
|
-
|
|
19
|
-
/** The modules that defined and implemented a step (where `step` and `implement` were called). */
|
|
20
|
-
export function originsOf(v: unknown): string[] {
|
|
21
|
-
const o = (v as Record<symbol, unknown>)[ORIGIN];
|
|
22
|
-
return Array.isArray(o) ? o.filter((x): x is string => typeof x === 'string') : [];
|
|
23
|
-
}
|
package/src/util/handler-hash.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { canonicalJson } from './canonical.ts';
|
|
2
|
-
import { sha256Hex } from './hash.ts';
|
|
3
|
-
|
|
4
|
-
/** Identity of the harness compiled into a binary. A Bun or SDK bump legitimately bumps every handler. */
|
|
5
|
-
export interface HarnessInfo {
|
|
6
|
-
/** Bun version (runtime compiled into the binary). */
|
|
7
|
-
bun: string;
|
|
8
|
-
/** @olivervea/pipelines package version. */
|
|
9
|
-
sdk: string;
|
|
10
|
-
/** `sha256:` hash over the SDK runtime sources and the zod version. */
|
|
11
|
-
sdkHash: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Custom step: hash of the handler bundle (contract files + SDK external) combined with the harness. */
|
|
15
|
-
export function composeHandlerHash(bundleHash: string, harness: HarnessInfo): string {
|
|
16
|
-
return 'sha256:' + sha256Hex(canonicalJson({ bundle: bundleHash, harness }));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** A step that carries its implementation: kind + canonical settings + harness, and the hash of the code that implements the kind. */
|
|
20
|
-
export function libraryHandlerHash(
|
|
21
|
-
kind: string,
|
|
22
|
-
config: unknown,
|
|
23
|
-
harness: HarnessInfo,
|
|
24
|
-
code?: string,
|
|
25
|
-
): string {
|
|
26
|
-
return (
|
|
27
|
-
'sha256:' +
|
|
28
|
-
sha256Hex(
|
|
29
|
-
canonicalJson({ library: kind, config, harness, ...(code ? { code } : {}) }),
|
|
30
|
-
)
|
|
31
|
-
);
|
|
32
|
-
}
|