@olivervea/pipelines 2.0.0-beta.25 → 2.0.0-beta.26
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 +1 -1
- package/src/build/build.ts +4 -9
- package/src/build/elf.ts +8 -0
- package/src/ctx/files.ts +8 -2
- package/src/ctx/index.ts +17 -11
- package/src/describe/describe.ts +3 -1
- package/src/describe/mapper-source.ts +4 -2
- package/src/harness/index.ts +0 -1
- package/src/harness/main.ts +0 -6
- package/src/harness/result.ts +7 -2
- package/src/util/tar.ts +1 -6
package/package.json
CHANGED
package/src/build/build.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { sha256File } from '../util/hash.ts';
|
|
|
6
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
|
-
import {
|
|
9
|
+
import { loaderProblem } from './elf.ts';
|
|
10
10
|
import { codeHash } from './hashes.ts';
|
|
11
11
|
import { harnessInfo, loadProject, relPosix, type Project } from './project.ts';
|
|
12
12
|
|
|
@@ -112,14 +112,9 @@ export async function build(dir: string, opts: BuildOptions): Promise<BuildResul
|
|
|
112
112
|
`olve-build: compiled ${target} in ${((performance.now() - t1) / 1000).toFixed(2)}s`,
|
|
113
113
|
);
|
|
114
114
|
|
|
115
|
-
// 2. loader check:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const interp = readInterpreter(binary);
|
|
119
|
-
if (interp !== expected)
|
|
120
|
-
throw new BuildError(
|
|
121
|
-
`compiled binary has PT_INTERP ${interp ?? '(none)'}, expected ${expected} — compile on a glibc Bun image (oven/bun:<version>-slim), not alpine`,
|
|
122
|
-
);
|
|
115
|
+
// 2. loader check: on alpine, bun-linux-x64 silently yields a musl binary, which would not start.
|
|
116
|
+
const problem = loaderProblem(binary, target);
|
|
117
|
+
if (problem) throw new BuildError(problem);
|
|
123
118
|
|
|
124
119
|
// 3. describe (runs the real binary: the manifest is exactly what the step pods will see)
|
|
125
120
|
const d = Bun.spawnSync([binary, 'describe'], {
|
package/src/build/elf.ts
CHANGED
|
@@ -34,3 +34,11 @@ export function readInterpreter(path: string): string | null {
|
|
|
34
34
|
closeSync(fd);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/** Why `binary` won't start through `target`'s loader (the harness runs it via /olve/lib/<loader>), or undefined. */
|
|
39
|
+
export function loaderProblem(binary: string, target: string): string | undefined {
|
|
40
|
+
const expected = target === 'bun-linux-x64' ? GLIBC_INTERP : MUSL_INTERP;
|
|
41
|
+
const interp = readInterpreter(binary);
|
|
42
|
+
if (interp === expected) return undefined;
|
|
43
|
+
return `compiled binary has PT_INTERP ${interp ?? '(none)'}, expected ${expected} — compile on a glibc Bun image (oven/bun:<version>-slim), not alpine`;
|
|
44
|
+
}
|
package/src/ctx/files.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface FilesDeps {
|
|
|
37
37
|
root: string;
|
|
38
38
|
signal: AbortSignal;
|
|
39
39
|
log: (message: string, fields?: Record<string, unknown>) => void;
|
|
40
|
+
/** Backoff between download retries (default: real sleep). */
|
|
41
|
+
sleep?: (ms: number) => Promise<void>;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function checkName(name: string, what: string): void {
|
|
@@ -122,6 +124,7 @@ async function download(
|
|
|
122
124
|
headers: Record<string, string>,
|
|
123
125
|
signal: AbortSignal,
|
|
124
126
|
log: FilesDeps['log'],
|
|
127
|
+
sleep: (ms: number) => Promise<void>,
|
|
125
128
|
): Promise<void> {
|
|
126
129
|
const attempts = 4;
|
|
127
130
|
for (let i = 1; ; i++) {
|
|
@@ -143,13 +146,14 @@ async function download(
|
|
|
143
146
|
attempt: i,
|
|
144
147
|
error: (e as Error).message,
|
|
145
148
|
});
|
|
146
|
-
await
|
|
149
|
+
await sleep(1000 * 2 ** (i - 1));
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
export function createFetch(deps: FilesDeps) {
|
|
152
155
|
const cache = new Map<string, string>();
|
|
156
|
+
const sleep = deps.sleep ?? ((ms: number) => Bun.sleep(ms));
|
|
153
157
|
return async (input: FileRef): Promise<string> => {
|
|
154
158
|
const parsed = fileRefSchema.safeParse(input);
|
|
155
159
|
if (!parsed.success)
|
|
@@ -181,7 +185,8 @@ export function createFetch(deps: FilesDeps) {
|
|
|
181
185
|
const baseUrl = process.env[ENV.filesBaseUrl];
|
|
182
186
|
if (own && existsSync(join(deps.workFiles, obj)))
|
|
183
187
|
copyFileSync(join(deps.workFiles, obj), objFile);
|
|
184
|
-
else if (urls[key])
|
|
188
|
+
else if (urls[key])
|
|
189
|
+
await download(urls[key], objFile, {}, deps.signal, deps.log, sleep);
|
|
185
190
|
else if (
|
|
186
191
|
localDir &&
|
|
187
192
|
existsSync(
|
|
@@ -201,6 +206,7 @@ export function createFetch(deps: FilesDeps) {
|
|
|
201
206
|
token ? { authorization: `Bearer ${token}` } : {},
|
|
202
207
|
deps.signal,
|
|
203
208
|
deps.log,
|
|
209
|
+
sleep,
|
|
204
210
|
);
|
|
205
211
|
} else {
|
|
206
212
|
throw new Error(
|
package/src/ctx/index.ts
CHANGED
|
@@ -89,9 +89,25 @@ export function createHttp(signal: AbortSignal): Http {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** Waits `ms`, or rejects at once when the step is cancelled. */
|
|
93
|
+
function cancellableSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
94
|
+
return new Promise<void>((res, rej) => {
|
|
95
|
+
const t = setTimeout(() => {
|
|
96
|
+
signal.removeEventListener('abort', onAbort);
|
|
97
|
+
res();
|
|
98
|
+
}, ms);
|
|
99
|
+
const onAbort = () => {
|
|
100
|
+
clearTimeout(t);
|
|
101
|
+
rej(new Error('retry cancelled: the step was cancelled'));
|
|
102
|
+
};
|
|
103
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
export function createRetry(
|
|
93
108
|
signal: AbortSignal,
|
|
94
109
|
log: (m: string, f?: Record<string, unknown>) => void,
|
|
110
|
+
sleep: (ms: number, signal: AbortSignal) => Promise<void> = cancellableSleep,
|
|
95
111
|
) {
|
|
96
112
|
return async <T>(
|
|
97
113
|
opts: RetryOptions,
|
|
@@ -110,17 +126,7 @@ export function createRetry(
|
|
|
110
126
|
of: opts.attempts,
|
|
111
127
|
error: (e as Error)?.message ?? String(e),
|
|
112
128
|
});
|
|
113
|
-
await
|
|
114
|
-
const t = setTimeout(() => {
|
|
115
|
-
signal.removeEventListener('abort', onAbort);
|
|
116
|
-
res();
|
|
117
|
-
}, delay);
|
|
118
|
-
const onAbort = () => {
|
|
119
|
-
clearTimeout(t);
|
|
120
|
-
rej(new Error('retry cancelled: the step was cancelled'));
|
|
121
|
-
};
|
|
122
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
123
|
-
});
|
|
129
|
+
await sleep(delay, signal);
|
|
124
130
|
delay *= opts.backoff ?? 1;
|
|
125
131
|
}
|
|
126
132
|
}
|
package/src/describe/describe.ts
CHANGED
|
@@ -806,7 +806,7 @@ export function describePipeline(pipeline: unknown, opts: DescribeOptions): Mani
|
|
|
806
806
|
export const manifestText = (m: ManifestT): string => canonicalJson(m);
|
|
807
807
|
|
|
808
808
|
/**
|
|
809
|
-
* The schemas at `path` inside an entry's schema: every union branch that has the field, except where
|
|
809
|
+
* The schemas at `path` inside an entry's schema (through properties and records): every union branch that has the field, except where
|
|
810
810
|
* `kase` (`<entry>.<path>.<discriminator>=<value>`) narrows a union to one branch.
|
|
811
811
|
*/
|
|
812
812
|
export function subSchemas(
|
|
@@ -848,6 +848,8 @@ export function subSchemas(
|
|
|
848
848
|
const props = o.properties as Record<string, JsonSchema> | undefined;
|
|
849
849
|
if (props && Object.hasOwn(props, rest[0]!))
|
|
850
850
|
return walk(props[rest[0]!], rest.slice(1), `${here}.${rest[0]}`);
|
|
851
|
+
if (o.additionalProperties && typeof o.additionalProperties === 'object')
|
|
852
|
+
return walk(o.additionalProperties, rest.slice(1), `${here}.${rest[0]}`);
|
|
851
853
|
return [];
|
|
852
854
|
};
|
|
853
855
|
return walk(schema, path, entry);
|
|
@@ -131,8 +131,8 @@ function tokenize(src: string): Tok[] {
|
|
|
131
131
|
i = e + 2;
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
if (c === '"' || c === "'") {
|
|
134
|
+
// Bun prints a string constant holding a newline or both quote kinds as a template literal.
|
|
135
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
136
136
|
let j = i + 1;
|
|
137
137
|
let v = '';
|
|
138
138
|
for (;;) {
|
|
@@ -140,6 +140,8 @@ function tokenize(src: string): Tok[] {
|
|
|
140
140
|
throw new MapperError('mapper source has an unterminated string');
|
|
141
141
|
const d = src[j]!;
|
|
142
142
|
if (d === c) break;
|
|
143
|
+
if (c === '`' && d === '$' && src[j + 1] === '{')
|
|
144
|
+
throw new MapperError('mapper uses a template string');
|
|
143
145
|
if (d === '\\') {
|
|
144
146
|
v += src.slice(j, j + 2);
|
|
145
147
|
j += 2;
|
package/src/harness/index.ts
CHANGED
package/src/harness/main.ts
CHANGED
|
@@ -56,12 +56,6 @@ export const defaultIO: HarnessIO = {
|
|
|
56
56
|
const USAGE =
|
|
57
57
|
'usage: step describe [--sentinels] | step run <step> | step map-trigger <trigger>';
|
|
58
58
|
|
|
59
|
-
export function allSteps(data: PipelineData): AnyStep[] {
|
|
60
|
-
const out: AnyStep[] = [];
|
|
61
|
-
for (const { step } of stepsWithCases(data)) if (!out.includes(step)) out.push(step);
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
59
|
/** Every step with the `select` case conditions (`<ref>.<discriminator>=<value>`) it sits in. */
|
|
66
60
|
export function stepsWithCases(data: PipelineData): { step: AnyStep; cases: string[] }[] {
|
|
67
61
|
const out: { step: AnyStep; cases: string[] }[] = [];
|
package/src/harness/result.ts
CHANGED
|
@@ -53,10 +53,15 @@ export async function uploadFile(
|
|
|
53
53
|
url: string,
|
|
54
54
|
file: string,
|
|
55
55
|
log: (line: string) => void,
|
|
56
|
-
opts: {
|
|
56
|
+
opts: {
|
|
57
|
+
attempts?: number;
|
|
58
|
+
baseDelayMs?: number;
|
|
59
|
+
sleep?: (ms: number) => Promise<void>;
|
|
60
|
+
} = {},
|
|
57
61
|
): Promise<void> {
|
|
58
62
|
const attempts = opts.attempts ?? 5;
|
|
59
63
|
const base = opts.baseDelayMs ?? 1000;
|
|
64
|
+
const sleep = opts.sleep ?? ((ms: number) => Bun.sleep(ms));
|
|
60
65
|
for (let i = 1; ; i++) {
|
|
61
66
|
let status = 0;
|
|
62
67
|
let detail: string;
|
|
@@ -73,7 +78,7 @@ export async function uploadFile(
|
|
|
73
78
|
if (!transient || i >= attempts)
|
|
74
79
|
throw new Error(`upload to ${redactUrl(url)} failed: ${detail}`);
|
|
75
80
|
log(`upload to ${redactUrl(url)} failed (${detail}); retry ${i}/${attempts - 1}`);
|
|
76
|
-
await
|
|
81
|
+
await sleep(base * 2 ** (i - 1));
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
package/src/util/tar.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { createGzip } from 'node:zlib';
|
|
|
20
20
|
|
|
21
21
|
export type TarEntry =
|
|
22
22
|
| { path: string; type: 'file'; mode: number; source: string }
|
|
23
|
-
| { path: string; type: 'bytes'; mode: number; data: Uint8Array }
|
|
24
23
|
| { path: string; type: 'dir'; mode: number }
|
|
25
24
|
| { path: string; type: 'symlink'; target: string };
|
|
26
25
|
|
|
@@ -88,7 +87,6 @@ export function writeTar(out: string, entries: readonly TarEntry[]): void {
|
|
|
88
87
|
const link = e.type === 'symlink' ? e.target : '';
|
|
89
88
|
let size = 0;
|
|
90
89
|
if (e.type === 'file') size = lstatSync(e.source).size;
|
|
91
|
-
else if (e.type === 'bytes') size = e.data.length;
|
|
92
90
|
const pax: string[] = [];
|
|
93
91
|
if (enc.encode(name).length > 100) pax.push(paxRecord('path', name));
|
|
94
92
|
if (enc.encode(link).length > 100) pax.push(paxRecord('linkpath', link));
|
|
@@ -104,10 +102,7 @@ export function writeTar(out: string, entries: readonly TarEntry[]): void {
|
|
|
104
102
|
write(
|
|
105
103
|
header(shortName, size, mode, flag, link.length > 100 ? link.slice(0, 99) : link),
|
|
106
104
|
);
|
|
107
|
-
if (e.type === '
|
|
108
|
-
write(e.data);
|
|
109
|
-
pad(size);
|
|
110
|
-
} else if (e.type === 'file') {
|
|
105
|
+
if (e.type === 'file') {
|
|
111
106
|
const src = openSync(e.source, 'r');
|
|
112
107
|
try {
|
|
113
108
|
const buf = new Uint8Array(1 << 20);
|