@olivervea/pipelines 2.0.0-beta.18 → 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.
- package/package.json +4 -2
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +111 -43
- package/src/build/cli.ts +28 -21
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +61 -28
- package/src/build/project.ts +69 -30
- package/src/builder/pipeline.ts +274 -102
- package/src/builder/refs.ts +12 -6
- package/src/core/brands.ts +6 -6
- package/src/core/config-refs.ts +46 -20
- package/src/core/config.ts +110 -33
- package/src/core/external.ts +30 -12
- package/src/core/handlers.ts +54 -14
- package/src/core/models.ts +29 -14
- package/src/core/origin.ts +7 -4
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +39 -23
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +75 -34
- package/src/ctx/sh.ts +104 -44
- package/src/ctx/types.ts +4 -4
- package/src/describe/describe.ts +550 -196
- package/src/describe/manifest-schema.ts +210 -80
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +64 -34
- package/src/harness/env.ts +33 -27
- package/src/harness/execute.ts +55 -29
- package/src/harness/index.ts +21 -8
- package/src/harness/main.ts +197 -74
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +62 -19
- package/src/index.ts +123 -14
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +40 -23
- package/src/library/github-source.ts +69 -39
- package/src/library/helm-deploy-ssh.ts +213 -76
- package/src/library/helm-deploy.ts +263 -85
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +293 -117
- package/src/library/index.ts +47 -15
- package/src/library/models.ts +17 -9
- package/src/library/ssh.ts +31 -14
- package/src/testing/index.ts +63 -24
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +15 -5
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +74 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olivervea/pipelines",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
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
|
|
3
|
+
declare module '*.sh' {
|
|
4
4
|
const path: string;
|
|
5
5
|
export default path;
|
|
6
6
|
}
|
|
7
|
-
declare module
|
|
7
|
+
declare module '*.bash' {
|
|
8
8
|
const path: string;
|
|
9
9
|
export default path;
|
|
10
10
|
}
|
package/src/build/build.ts
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
// olve-build: pipeline dir → single executable + step.tar.zst + manifest.json.
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from
|
|
3
|
-
import { join, resolve } from
|
|
4
|
-
import { canonicalJson } from
|
|
5
|
-
import { sha256File } from
|
|
6
|
-
import type { HarnessInfo } from
|
|
7
|
-
import { writeTarZst } from
|
|
8
|
-
import { MANIFEST_BEGIN, MANIFEST_END, uploadFile } from
|
|
9
|
-
import { GLIBC_INTERP, MUSL_INTERP, readInterpreter } from
|
|
10
|
-
import { handlerHashes, kindHashes, type HandlerHash } from
|
|
11
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { canonicalJson } from '../util/canonical.ts';
|
|
5
|
+
import { sha256File } from '../util/hash.ts';
|
|
6
|
+
import type { HarnessInfo } from '../util/handler-hash.ts';
|
|
7
|
+
import { writeTarZst } from '../util/tar.ts';
|
|
8
|
+
import { MANIFEST_BEGIN, MANIFEST_END, uploadFile } from '../harness/result.ts';
|
|
9
|
+
import { GLIBC_INTERP, MUSL_INTERP, readInterpreter } from './elf.ts';
|
|
10
|
+
import { handlerHashes, kindHashes, type HandlerHash } from './hashes.ts';
|
|
11
|
+
import {
|
|
12
|
+
harnessInfo,
|
|
13
|
+
loadPipeline,
|
|
14
|
+
loadProject,
|
|
15
|
+
relPosix,
|
|
16
|
+
type Project,
|
|
17
|
+
} from './project.ts';
|
|
12
18
|
|
|
13
|
-
export const BUILD_FLAGS = [
|
|
14
|
-
|
|
19
|
+
export const BUILD_FLAGS = [
|
|
20
|
+
'--no-compile-autoload-dotenv',
|
|
21
|
+
'--no-compile-autoload-bunfig',
|
|
22
|
+
'--bytecode',
|
|
23
|
+
'--format=esm',
|
|
24
|
+
] as const;
|
|
25
|
+
export type Target = 'bun-linux-x64' | 'bun-linux-x64-musl';
|
|
15
26
|
|
|
16
27
|
export class BuildError extends Error {
|
|
17
|
-
override name =
|
|
18
|
-
constructor(
|
|
28
|
+
override name = 'BuildError';
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
readonly exitCode = 1,
|
|
32
|
+
) {
|
|
19
33
|
super(message);
|
|
20
34
|
}
|
|
21
35
|
}
|
|
@@ -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(
|
|
45
|
-
|
|
58
|
+
export function generateEntry(
|
|
59
|
+
project: Project,
|
|
60
|
+
hashes: Record<string, string>,
|
|
61
|
+
harness: HarnessInfo,
|
|
62
|
+
kinds: Record<string, string> = {},
|
|
63
|
+
): string {
|
|
64
|
+
const gen = join(project.dir, '.olve', 'gen');
|
|
46
65
|
mkdirSync(gen, { recursive: true });
|
|
47
|
-
const registry = project.handlers
|
|
66
|
+
const registry = project.handlers
|
|
67
|
+
.map(
|
|
68
|
+
([name, file]) =>
|
|
69
|
+
` ${JSON.stringify(name)}: () => import(${JSON.stringify(relPosix(gen, file))}),`,
|
|
70
|
+
)
|
|
71
|
+
.join('\n');
|
|
48
72
|
writeFileSync(
|
|
49
|
-
join(gen,
|
|
73
|
+
join(gen, 'handlers.ts'),
|
|
50
74
|
`// GENERATED by olve-build — do not edit.\n` +
|
|
51
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,
|
|
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 +
|
|
71
|
-
const stdout =
|
|
72
|
-
|
|
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(
|
|
82
|
-
|
|
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,
|
|
117
|
+
const binary = join(out, 'step');
|
|
86
118
|
rmSync(binary, { force: true });
|
|
87
119
|
const t1 = performance.now();
|
|
88
|
-
const p = Bun.spawnSync(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
96
|
-
|
|
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 ===
|
|
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(
|
|
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,
|
|
107
|
-
|
|
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,
|
|
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,
|
|
114
|
-
await writeTarZst(archive, [
|
|
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,
|
|
117
|
-
canonicalJson({
|
|
176
|
+
join(out, 'build.json'),
|
|
177
|
+
canonicalJson({
|
|
178
|
+
target,
|
|
179
|
+
flags: BUILD_FLAGS,
|
|
180
|
+
harness,
|
|
181
|
+
handlers: hashes,
|
|
182
|
+
kinds,
|
|
183
|
+
binarySha256: sha256File(binary),
|
|
184
|
+
archiveSha256: sha256File(archive),
|
|
185
|
+
}),
|
|
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(
|
|
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
|
|
4
|
-
import { canonicalJson } from
|
|
5
|
-
import { build, BuildError, type Target } from
|
|
6
|
-
import { handlerHashes } from
|
|
7
|
-
import { harnessInfo, loadProject, ProjectError } from
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { canonicalJson } from '../util/canonical.ts';
|
|
5
|
+
import { build, BuildError, type Target } from './build.ts';
|
|
6
|
+
import { handlerHashes } from './hashes.ts';
|
|
7
|
+
import { harnessInfo, loadProject, ProjectError } from './project.ts';
|
|
8
8
|
|
|
9
|
-
const USAGE =
|
|
9
|
+
const USAGE =
|
|
10
|
+
'usage: olve-build <pipelineDir> --out <dir> [--target bun-linux-x64|bun-linux-x64-musl] [--upload-url <url>] [--sentinels]\n olve-build <pipelineDir> --hash-only';
|
|
10
11
|
|
|
11
12
|
async function main(): Promise<number> {
|
|
12
13
|
let parsed;
|
|
@@ -14,12 +15,12 @@ async function main(): Promise<number> {
|
|
|
14
15
|
parsed = parseArgs({
|
|
15
16
|
allowPositionals: true,
|
|
16
17
|
options: {
|
|
17
|
-
out: { type:
|
|
18
|
-
target: { type:
|
|
19
|
-
|
|
20
|
-
sentinels: { type:
|
|
21
|
-
|
|
22
|
-
help: { type:
|
|
18
|
+
out: { type: 'string' },
|
|
19
|
+
target: { type: 'string' },
|
|
20
|
+
'upload-url': { type: 'string' },
|
|
21
|
+
sentinels: { type: 'boolean', default: false },
|
|
22
|
+
'hash-only': { type: 'boolean', default: false },
|
|
23
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
23
24
|
},
|
|
24
25
|
});
|
|
25
26
|
} catch (e) {
|
|
@@ -27,23 +28,29 @@ async function main(): Promise<number> {
|
|
|
27
28
|
return 64;
|
|
28
29
|
}
|
|
29
30
|
const { values, positionals } = parsed;
|
|
30
|
-
if (values.help) return console.log(USAGE), 0;
|
|
31
|
-
if (positionals.length !== 1) return console.error(USAGE), 64;
|
|
31
|
+
if (values.help) return (console.log(USAGE), 0);
|
|
32
|
+
if (positionals.length !== 1) return (console.error(USAGE), 64);
|
|
32
33
|
const dir = positionals[0]!;
|
|
33
34
|
try {
|
|
34
|
-
if (values[
|
|
35
|
+
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 ??
|
|
41
|
-
if (target !==
|
|
42
|
-
|
|
40
|
+
if (!values.out) return (console.error(`--out is required\n${USAGE}`), 64);
|
|
41
|
+
const target = (values.target ?? 'bun-linux-x64') as Target;
|
|
42
|
+
if (target !== 'bun-linux-x64' && target !== 'bun-linux-x64-musl')
|
|
43
|
+
return (console.error(`unsupported target ${target}`), 64);
|
|
44
|
+
await build(dir, {
|
|
45
|
+
out: values.out,
|
|
46
|
+
target,
|
|
47
|
+
uploadUrl: values['upload-url'],
|
|
48
|
+
sentinels: values.sentinels,
|
|
49
|
+
});
|
|
43
50
|
return 0;
|
|
44
51
|
} catch (e) {
|
|
45
|
-
if (e instanceof BuildError) return console.error(e.message), e.exitCode;
|
|
46
|
-
if (e instanceof ProjectError) return console.error(e.message), 78;
|
|
52
|
+
if (e instanceof BuildError) return (console.error(e.message), e.exitCode);
|
|
53
|
+
if (e instanceof ProjectError) return (console.error(e.message), 78);
|
|
47
54
|
console.error((e as Error)?.stack ?? String(e));
|
|
48
55
|
return 1;
|
|
49
56
|
}
|
package/src/build/elf.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { closeSync, openSync, readSync } from
|
|
1
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
export const GLIBC_INTERP =
|
|
4
|
-
export const MUSL_INTERP =
|
|
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(
|
|
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,
|
|
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)
|
|
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(
|
|
29
|
+
return read(fd, off, size).toString('latin1').replace(/\0+$/, '');
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
return null;
|
package/src/build/hashes.ts
CHANGED
|
@@ -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
|
|
6
|
-
import type { BunPlugin } from
|
|
7
|
-
import { isImplementation } from
|
|
8
|
-
import { originsOf } from
|
|
9
|
-
import { pipelineData } from
|
|
10
|
-
import { allSteps } from
|
|
11
|
-
import { composeHandlerHash, type HarnessInfo } from
|
|
12
|
-
import { sha256Hex } from
|
|
13
|
-
import type { Project } from
|
|
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:
|
|
27
|
+
name: 'olve-handler-hash-externals',
|
|
28
28
|
setup(b) {
|
|
29
|
-
b.onResolve({ filter: /^@olivervea\/pipelines(\/.*)?$/ }, (args) => ({
|
|
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,
|
|
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(
|
|
40
|
-
|
|
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:
|
|
44
|
-
format:
|
|
50
|
+
target: 'bun',
|
|
51
|
+
format: 'esm',
|
|
45
52
|
root: inside ? project.dir : dirname(entry),
|
|
46
|
-
naming: {
|
|
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)
|
|
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) =>
|
|
54
|
-
|
|
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(
|
|
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:
|
|
81
|
+
return { hash: 'sha256:' + sha256Hex(parts.join('\n')), outputs };
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
export async function handlerHashes(
|
|
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] = {
|
|
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(
|
|
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(
|
|
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]) =>
|
|
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] =
|
|
126
|
+
out[kind] = 'sha256:' + sha256Hex(parts.join('\n'));
|
|
94
127
|
}
|
|
95
128
|
return out;
|
|
96
129
|
}
|