@olivervea/pipelines 2.0.0-beta.17 → 2.0.0-beta.19

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 (53) hide show
  1. package/README.md +2 -0
  2. package/manifest.schema.json +22 -0
  3. package/package.json +4 -2
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +111 -43
  6. package/src/build/cli.ts +28 -21
  7. package/src/build/elf.ts +8 -7
  8. package/src/build/hashes.ts +61 -28
  9. package/src/build/project.ts +69 -30
  10. package/src/builder/pipeline.ts +288 -102
  11. package/src/builder/refs.ts +12 -6
  12. package/src/core/brands.ts +6 -6
  13. package/src/core/config-refs.ts +46 -20
  14. package/src/core/config.ts +110 -33
  15. package/src/core/external.ts +30 -12
  16. package/src/core/handlers.ts +54 -14
  17. package/src/core/models.ts +29 -14
  18. package/src/core/origin.ts +7 -4
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +39 -23
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +111 -40
  24. package/src/ctx/index.ts +75 -34
  25. package/src/ctx/sh.ts +104 -44
  26. package/src/ctx/types.ts +4 -4
  27. package/src/describe/describe.ts +558 -194
  28. package/src/describe/manifest-schema.ts +217 -79
  29. package/src/describe/mapper-source.ts +249 -104
  30. package/src/describe/record.ts +67 -37
  31. package/src/dev/cli.ts +64 -34
  32. package/src/harness/env.ts +33 -27
  33. package/src/harness/execute.ts +55 -29
  34. package/src/harness/index.ts +21 -8
  35. package/src/harness/main.ts +197 -74
  36. package/src/harness/result.ts +38 -22
  37. package/src/harness/validate.ts +62 -19
  38. package/src/index.ts +123 -14
  39. package/src/library/checks.ts +11 -6
  40. package/src/library/compose-deploy.ts +40 -23
  41. package/src/library/github-source.ts +69 -39
  42. package/src/library/helm-deploy-ssh.ts +213 -76
  43. package/src/library/helm-deploy.ts +263 -85
  44. package/src/library/http-health.ts +7 -5
  45. package/src/library/image-build.ts +293 -117
  46. package/src/library/index.ts +47 -15
  47. package/src/library/models.ts +17 -9
  48. package/src/library/ssh.ts +31 -14
  49. package/src/testing/index.ts +63 -24
  50. package/src/util/canonical.ts +7 -4
  51. package/src/util/handler-hash.ts +15 -5
  52. package/src/util/hash.ts +5 -5
  53. package/src/util/tar.ts +74 -37
package/README.md CHANGED
@@ -253,6 +253,7 @@ export default pipeline({ name: "Olve.Pipelines", description: "…", input: M.D
253
253
  - **Describe checks the mapper twice.** It records the mapper with proxies, and it statically analyses the mapper's source. The source must be an object literal of member chains, literals, constant arrays and nested object literals. Ternaries, `&&`, `||`, `??`, `!`, comparisons, arithmetic, template strings, calls, spread and optional chaining are all rejected. Free identifiers such as module constants are allowed; they are recorded as `{const}`.
254
254
  - **Triggers.** A pipeline may call `onPush` at most once. Each `onPoll` and `onWebhook` needs a distinct `name` (`onPoll` defaults to `"poll"`). `onWebhook({ name, verify }, ({ headers, body }) => input | null)` maps a delivery to `/api/hooks/<pipeline id>/<name>`; `verify` is `hmacSha256({ secret, header, prefix? })`, `githubSignature(secret)` or `bearer(secret)`, and `null` ignores the delivery.
255
255
  - **`select` (branch on config).** `.select(TARGET.prod, { ssh: (p, prod) => p.stage(…), inCluster: (p, prod) => p.stage(…) })`: the first argument references a `z.discriminatedUnion` in a config entry (`DiscriminatorOf<U>` is its one literal field; a union with no or several literal fields is a type error). Cases are exhaustive and checked at compile time; each gets the reference narrowed to its variant (`ConfigNode<VariantOf<U, D, V>>`), recorded as `{"$config": "target.prod", "$case": "target.prod.kind=ssh"}` and usable only inside that case. The manifest records `{select, discriminator, cases}`; the service resolves it per run from the pinned config snapshot (`when` is per run input). The same step name may appear in several cases (same output model; not two different custom steps, since handlers pair by name); a step every case has is definitely available afterwards, one only some have is maybe-available. The harness picks the variant of a same-named step from `OLVE_CASES` (a JSON array of the case conditions the step sits in, set by the service).
256
+ - **Retention.** `pipeline({ …, retention: { keepLatestRuns?, minRunAgeDays?, buildGraceDays? } })` overrides the instance's retention thresholds for this pipeline's runs and binaries, field by field (non-negative integers; unset = the instance's value). Recorded on the manifest root as `retention`; the service applies the active version's.
256
257
  - **Failure handlers.** `.onFailure(handler, { steps? })` takes a step whose inputs are all `FailureContext`. When a run fails, the service runs each handler whose scope (default: any step) contains a failed step, once per failure: `run <handler>` in the handler's image with its declared secrets, from the run's pinned binary. It is untracked and best-effort — not an attempt of the run, never retried, and its own failure triggers nothing; its log is kept at `runs/<p>/<run>/_failure/<handler>/<id>/log`. `FailureContext` carries `pipeline {id, name}`, `run {id, number, address, versionStamp, trigger, url}`, `version {number, commit}`, the run's `input`, a one-line `reason`, and `failedSteps[]` (`step, attempt, address, reason, logUrl`) — only the steps in the handler's scope, never empty.
257
258
 
258
259
  ### Testing handlers
@@ -397,6 +398,7 @@ Exact format: [`manifest.schema.json`](manifest.schema.json) (JSON Schema 2020-1
397
398
  - `{kind: "poll", name, url, headers, secrets, valuePath, intervalSeconds}`, where header values may contain `$SECRET:NAME`;
398
399
  - `{kind: "webhook", name, verify: {scheme: "hmac-sha256", secret, header, prefix} | {scheme: "bearer", secret}}`. The `map-trigger` event is `{headers, body}`; a `null` mapping yields a `skipped` envelope (the delivery is ignored).
399
400
  - **`failureHandlers`** is a list of `{step: Step, steps: [names] | null}`. Each handler's inputs are all FailureContext (`{"ref": "failure"}`).
401
+ - **`retention`** (when present) is `{keepLatestRuns?, minRunAgeDays?, buildGraceDays?}`, non-negative integers that replace the instance's thresholds for this pipeline. Omitted when the definition sets none.
400
402
  - **`secrets`** lists every secret used by steps, polls and webhooks, sorted, as `{name, description}`.
401
403
  - **`config`** lists every config entry steps declare, sorted by name, as `{name, description, required, schema}`. `schema` is the `z.input` view (defaults listed); it validates the entry's value in the config document. `required` is false when the schema accepts a missing value. A `secretRef` field is marked `"x-olve-kind": "secretRef"`, with the secret's name as `properties.secret.const`. Omitted when there are no entries.
402
404
  - **Step `configEntries`** names the entries the step receives (sorted; omitted when none). It is part of the step's content, like `secrets`. The step key `config` is the step's `settings`.
@@ -165,6 +165,28 @@
165
165
  "minLength": 1,
166
166
  "type": "string"
167
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
+ },
168
190
  "secrets": {
169
191
  "description": "All declared secrets (steps + poll triggers), sorted by name.",
170
192
  "items": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olivervea/pipelines",
3
- "version": "2.0.0-beta.17",
3
+ "version": "2.0.0-beta.19",
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",
@@ -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,35 @@
1
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 { harnessInfo, loadPipeline, loadProject, relPosix, type Project } from "./project.ts";
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 = ["--no-compile-autoload-dotenv", "--no-compile-autoload-bunfig", "--bytecode", "--format=esm"] as const;
14
- export type Target = "bun-linux-x64" | "bun-linux-x64-musl";
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 = "BuildError";
18
- constructor(message: string, readonly exitCode = 1) {
28
+ override name = 'BuildError';
29
+ constructor(
30
+ message: string,
31
+ readonly exitCode = 1,
32
+ ) {
19
33
  super(message);
20
34
  }
21
35
  }
@@ -41,12 +55,22 @@ 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(project: Project, hashes: Record<string, string>, harness: HarnessInfo, kinds: Record<string, string> = {}): string {
45
- const gen = join(project.dir, ".olve", "gen");
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.map(([name, file]) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(relPosix(gen, file))}),`).join("\n");
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, "handlers.ts"),
73
+ join(gen, 'handlers.ts'),
50
74
  `// GENERATED by olve-build — do not edit.\n` +
51
75
  `import type { HandlerRegistry, HarnessInfo } from "@olivervea/pipelines/harness";\n\n` +
52
76
  `export const handlers: HandlerRegistry = {\n${registry}\n};\n\n` +
@@ -54,7 +78,7 @@ export function generateEntry(project: Project, hashes: Record<string, string>,
54
78
  `export const kindHashes: Record<string, string> = ${JSON.stringify(kinds, null, 2)};\n\n` +
55
79
  `export const harness: HarnessInfo = ${JSON.stringify(harness, null, 2)};\n`,
56
80
  );
57
- const entry = join(gen, "entry.ts");
81
+ const entry = join(gen, 'entry.ts');
58
82
  writeFileSync(
59
83
  entry,
60
84
  `// GENERATED by olve-build — do not edit. Entry point of the step binary.\n` +
@@ -67,9 +91,10 @@ export function generateEntry(project: Project, hashes: Record<string, string>,
67
91
  }
68
92
 
69
93
  export async function build(dir: string, opts: BuildOptions): Promise<BuildResult> {
70
- const log = opts.log ?? ((l: string) => process.stderr.write(l + "\n"));
71
- const stdout = opts.stdout ?? (async (t: string) => void (await Bun.write(Bun.stdout, t)));
72
- const target = opts.target ?? "bun-linux-x64";
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';
73
98
  const project = await loadProject(dir);
74
99
  const out = resolve(opts.out);
75
100
  mkdirSync(out, { recursive: true });
@@ -78,49 +103,92 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
78
103
  const t0 = performance.now();
79
104
  const hashes = await handlerHashes(project, harness);
80
105
  const kinds = await kindHashes(project, await loadPipeline(project));
81
- const entry = generateEntry(project, Object.fromEntries(Object.entries(hashes).map(([k, v]) => [k, v.handlerHash])), harness, kinds);
82
- log(`olve-build: ${project.handlers.length} handler(s) hashed in ${((performance.now() - t0) / 1000).toFixed(2)}s`);
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
+ );
83
115
 
84
116
  // 1. compile
85
- const binary = join(out, "step");
117
+ const binary = join(out, 'step');
86
118
  rmSync(binary, { force: true });
87
119
  const t1 = performance.now();
88
- const p = Bun.spawnSync([process.execPath, "build", "--compile", `--target=${target}`, ...BUILD_FLAGS, entry, `--outfile=${binary}`], {
89
- cwd: project.dir,
90
- stdout: "pipe",
91
- stderr: "pipe",
92
- env: { ...process.env, NO_COLOR: "1" },
93
- });
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
+ );
94
137
  if (p.exitCode !== 0 || !existsSync(binary))
95
- throw new BuildError(`bun build --compile failed (exit ${p.exitCode}):\n${p.stdout.toString()}${p.stderr.toString()}`);
96
- log(`olve-build: compiled ${target} in ${((performance.now() - t1) / 1000).toFixed(2)}s`);
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
+ );
97
144
 
98
145
  // 2. loader check: the harness launches the binary through /olve/lib/<loader>; a musl binary
99
146
  // (e.g. compiled on alpine, where bun-linux-x64 silently yields musl) would not start.
100
- const expected = target === "bun-linux-x64" ? GLIBC_INTERP : MUSL_INTERP;
147
+ const expected = target === 'bun-linux-x64' ? GLIBC_INTERP : MUSL_INTERP;
101
148
  const interp = readInterpreter(binary);
102
149
  if (interp !== expected)
103
- throw new BuildError(`compiled binary has PT_INTERP ${interp ?? "(none)"}, expected ${expected} — compile on a glibc Bun image (oven/bun:<version>-slim), not alpine`);
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
+ );
104
153
 
105
154
  // 3. describe (runs the real binary: the manifest is exactly what the step pods will see)
106
- const d = Bun.spawnSync([binary, "describe"], { cwd: out, stdout: "pipe", stderr: "pipe", env: { PATH: process.env.PATH ?? "/usr/bin:/bin" } });
107
- if (d.exitCode !== 0) throw new BuildError(`describe failed (exit ${d.exitCode}):\n${d.stderr.toString()}`, d.exitCode ?? 1);
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
+ );
108
166
  const manifest = d.stdout.toString();
109
- const manifestFile = join(out, "manifest.json");
167
+ const manifestFile = join(out, 'manifest.json');
110
168
  writeFileSync(manifestFile, manifest);
111
169
 
112
170
  // 4. step.tar.zst (deterministic: `step` at the root, 0755, mtime 0)
113
- const archive = join(out, "step.tar.zst");
114
- await writeTarZst(archive, [{ path: "step", type: "file", mode: 0o755, source: binary }]);
171
+ const archive = join(out, 'step.tar.zst');
172
+ await writeTarZst(archive, [
173
+ { path: 'step', type: 'file', mode: 0o755, source: binary },
174
+ ]);
115
175
  writeFileSync(
116
- join(out, "build.json"),
117
- canonicalJson({ target, flags: BUILD_FLAGS, harness, handlers: hashes, kinds, binarySha256: sha256File(binary), archiveSha256: sha256File(archive) }),
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
+ }),
118
186
  );
119
187
  log(`olve-build: wrote ${archive}`);
120
188
 
121
189
  if (opts.uploadUrl) {
122
190
  await uploadFile(opts.uploadUrl, archive, log);
123
- log("olve-build: uploaded step.tar.zst");
191
+ log('olve-build: uploaded step.tar.zst');
124
192
  }
125
193
  if (opts.sentinels) await stdout(`${MANIFEST_BEGIN}\n${manifest}${MANIFEST_END}\n`);
126
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 "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";
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 = "usage: olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <url>] [--sentinels]\n olve-build <pipelineDir> --hash-only";
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: "string" },
18
- target: { type: "string" },
19
- "upload-url": { type: "string" },
20
- sentinels: { type: "boolean", default: false },
21
- "hash-only": { type: "boolean", default: false },
22
- help: { type: "boolean", short: "h", default: false },
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["hash-only"]) {
35
+ if (values['hash-only']) {
35
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 ?? "bun-linux-x64") as Target;
41
- if (target !== "bun-linux-x64" && target !== "bun-linux-x64-musl") return console.error(`unsupported target ${target}`), 64;
42
- await build(dir, { out: values.out, target, uploadUrl: values["upload-url"], sentinels: values.sentinels });
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
  }
package/src/build/elf.ts CHANGED
@@ -1,22 +1,23 @@
1
- import { closeSync, openSync, readSync } from "node:fs";
1
+ import { closeSync, openSync, readSync } from 'node:fs';
2
2
 
3
- export const GLIBC_INTERP = "/lib64/ld-linux-x86-64.so.2";
4
- export const MUSL_INTERP = "/lib/ld-musl-x86-64.so.1";
3
+ export const GLIBC_INTERP = '/lib64/ld-linux-x86-64.so.2';
4
+ export const MUSL_INTERP = '/lib/ld-musl-x86-64.so.1';
5
5
 
6
6
  function read(fd: number, pos: number, len: number): Buffer {
7
7
  const b = Buffer.alloc(len);
8
8
  const n = readSync(fd, b, 0, len, pos);
9
- if (n !== len) throw new Error("truncated ELF file");
9
+ if (n !== len) throw new Error('truncated ELF file');
10
10
  return b;
11
11
  }
12
12
 
13
13
  /** PT_INTERP of an x86-64 ELF executable, or null if it has none (static). */
14
14
  export function readInterpreter(path: string): string | null {
15
- const fd = openSync(path, "r");
15
+ const fd = openSync(path, 'r');
16
16
  try {
17
17
  const h = read(fd, 0, 64);
18
18
  if (h.readUInt32BE(0) !== 0x7f454c46) throw new Error(`${path} is not an ELF file`);
19
- if (h[4] !== 2 || h[5] !== 1) throw new Error(`${path} is not a 64-bit little-endian ELF file`);
19
+ if (h[4] !== 2 || h[5] !== 1)
20
+ throw new Error(`${path} is not a 64-bit little-endian ELF file`);
20
21
  const phoff = Number(h.readBigUInt64LE(0x20));
21
22
  const phentsize = h.readUInt16LE(0x36);
22
23
  const phnum = h.readUInt16LE(0x38);
@@ -25,7 +26,7 @@ export function readInterpreter(path: string): string | null {
25
26
  if (ph.readUInt32LE(0) === 3 /* PT_INTERP */) {
26
27
  const off = Number(ph.readBigUInt64LE(0x08));
27
28
  const size = Number(ph.readBigUInt64LE(0x20));
28
- return read(fd, off, size).toString("latin1").replace(/\0+$/, "");
29
+ return read(fd, off, size).toString('latin1').replace(/\0+$/, '');
29
30
  }
30
31
  }
31
32
  return null;
@@ -2,15 +2,15 @@
2
2
  // (tree-shaken → function-level granularity; embedded scripts are bundle outputs too), then hash the
3
3
  // outputs. Contract changes are versioned through the manifest's model/step entries, SDK changes
4
4
  // through `harness`; both are combined in composeHandlerHash.
5
- import { dirname, join, relative, resolve } from "node:path";
6
- import type { BunPlugin } from "bun";
7
- import { isImplementation } from "../core/handlers.ts";
8
- import { originsOf } from "../core/origin.ts";
9
- import { pipelineData } from "../builder/pipeline.ts";
10
- import { allSteps } from "../harness/main.ts";
11
- import { composeHandlerHash, type HarnessInfo } from "../util/handler-hash.ts";
12
- import { sha256Hex } from "../util/hash.ts";
13
- import type { Project } from "./project.ts";
5
+ import { dirname, join, relative, resolve } from 'node:path';
6
+ import type { BunPlugin } from 'bun';
7
+ import { isImplementation } from '../core/handlers.ts';
8
+ import { originsOf } from '../core/origin.ts';
9
+ import { pipelineData } from '../builder/pipeline.ts';
10
+ import { allSteps } from '../harness/main.ts';
11
+ import { composeHandlerHash, type HarnessInfo } from '../util/handler-hash.ts';
12
+ import { sha256Hex } from '../util/hash.ts';
13
+ import type { Project } from './project.ts';
14
14
 
15
15
  export interface HandlerHash {
16
16
  /** Hash of the bundle outputs alone. */
@@ -24,49 +24,75 @@ export interface HandlerHash {
24
24
  function externals(project: Project): BunPlugin {
25
25
  const contracts = new Set(project.contractFiles);
26
26
  return {
27
- name: "olve-handler-hash-externals",
27
+ name: 'olve-handler-hash-externals',
28
28
  setup(b) {
29
- b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({ path: args.path, external: true }));
29
+ b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({
30
+ path: args.path,
31
+ external: true,
32
+ }));
30
33
  b.onResolve({ filter: /^\.{1,2}\// }, (args) => {
31
34
  const base = resolve(dirname(args.importer), args.path);
32
- for (const cand of [base, `${base}.ts`, join(base, "index.ts")]) if (contracts.has(cand)) return { path: args.path, external: true };
35
+ for (const cand of [base, `${base}.ts`, join(base, 'index.ts')])
36
+ if (contracts.has(cand)) return { path: args.path, external: true };
33
37
  return undefined;
34
38
  });
35
39
  },
36
40
  };
37
41
  }
38
42
 
39
- export async function bundleHash(project: Project, entry: string): Promise<{ hash: string; outputs: string[] }> {
40
- const inside = !relative(project.dir, entry).startsWith("..");
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('..');
41
48
  const r = await Bun.build({
42
49
  entrypoints: [entry],
43
- target: "bun",
44
- format: "esm",
50
+ target: 'bun',
51
+ format: 'esm',
45
52
  root: inside ? project.dir : dirname(entry),
46
- naming: { entry: "[dir]/[name].[ext]", chunk: "[dir]/[name].[ext]", asset: "[dir]/[name].[ext]" },
53
+ naming: {
54
+ entry: '[dir]/[name].[ext]',
55
+ chunk: '[dir]/[name].[ext]',
56
+ asset: '[dir]/[name].[ext]',
57
+ },
47
58
  plugins: [externals(project)],
48
59
  throw: false,
49
60
  });
50
- if (!r.success) throw new AggregateError(r.logs, `bundling ${entry} failed:\n${r.logs.map(String).join("\n")}`);
61
+ if (!r.success)
62
+ throw new AggregateError(
63
+ r.logs,
64
+ `bundling ${entry} failed:\n${r.logs.map(String).join('\n')}`,
65
+ );
51
66
  const parts: string[] = [];
52
67
  const outputs: string[] = [];
53
- for (const o of [...r.outputs].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
54
- const name = o.path.replace(/^\.\//, "");
68
+ for (const o of [...r.outputs].sort((a, b) =>
69
+ a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
70
+ )) {
71
+ const name = o.path.replace(/^\.\//, '');
55
72
  outputs.push(name);
56
73
  const bytes = new Uint8Array(await o.arrayBuffer());
57
74
  // Bun prefixes each module with a `// <path>` comment relative to the *process cwd*; drop those
58
75
  // so the hash depends only on code, not on where the repo or the build tool runs.
59
- const content = name.endsWith(".js") ? new TextDecoder().decode(bytes).replace(/^\/\/ \S+\n/gm, "") : bytes;
76
+ const content = name.endsWith('.js')
77
+ ? new TextDecoder().decode(bytes).replace(/^\/\/ \S+\n/gm, '')
78
+ : bytes;
60
79
  parts.push(`${name}\0${sha256Hex(content)}`);
61
80
  }
62
- return { hash: "sha256:" + sha256Hex(parts.join("\n")), outputs };
81
+ return { hash: 'sha256:' + sha256Hex(parts.join('\n')), outputs };
63
82
  }
64
83
 
65
- export async function handlerHashes(project: Project, harness: HarnessInfo): Promise<Record<string, HandlerHash>> {
84
+ export async function handlerHashes(
85
+ project: Project,
86
+ harness: HarnessInfo,
87
+ ): Promise<Record<string, HandlerHash>> {
66
88
  const out: Record<string, HandlerHash> = {};
67
89
  for (const [name, file] of project.handlers) {
68
90
  const b = await bundleHash(project, file);
69
- out[name] = { bundleHash: b.hash, handlerHash: composeHandlerHash(b.hash, harness), outputs: b.outputs };
91
+ out[name] = {
92
+ bundleHash: b.hash,
93
+ handlerHash: composeHandlerHash(b.hash, harness),
94
+ outputs: b.outputs,
95
+ };
70
96
  }
71
97
  return out;
72
98
  }
@@ -75,22 +101,29 @@ export async function handlerHashes(project: Project, harness: HarnessInfo): Pro
75
101
  * Kind → hash of the modules that implement the steps of that kind which carry their implementation
76
102
  * (each module bundled like a handler: the SDK and contract files external).
77
103
  */
78
- export async function kindHashes(project: Project, pipeline: unknown): Promise<Record<string, string>> {
104
+ export async function kindHashes(
105
+ project: Project,
106
+ pipeline: unknown,
107
+ ): Promise<Record<string, string>> {
79
108
  const origins = new Map<string, Set<string>>();
80
109
  for (const s of allSteps(pipelineData(pipeline))) {
81
110
  if (!isImplementation(s)) continue;
82
111
  const files = originsOf(s).filter((f) => !project.contractFiles.includes(f));
83
112
  if (files.length === 0)
84
- throw new Error(`step '${s.name}' carries its implementation but is defined in ${project.contractFiles.map((f) => relative(project.dir, f)).join(" or ")}; define and implement it in a module of its own (or a package)`);
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
+ );
85
116
  const set = origins.get(s.kind) ?? new Set<string>();
86
117
  for (const f of files) set.add(f);
87
118
  origins.set(s.kind, set);
88
119
  }
89
120
  const out: Record<string, string> = {};
90
- for (const [kind, files] of [...origins].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
121
+ for (const [kind, files] of [...origins].sort(([a], [b]) =>
122
+ a < b ? -1 : a > b ? 1 : 0,
123
+ )) {
91
124
  const parts: string[] = [];
92
125
  for (const f of [...files].sort()) parts.push((await bundleHash(project, f)).hash);
93
- out[kind] = "sha256:" + sha256Hex(parts.join("\n"));
126
+ out[kind] = 'sha256:' + sha256Hex(parts.join('\n'));
94
127
  }
95
128
  return out;
96
129
  }