@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/src/ctx/sh.ts
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { basename, isAbsolute, join } from 'node:path';
|
|
10
|
+
import type { ExecOptions, Sh, ShOptions } from './types.ts';
|
|
11
|
+
import { PRIVATE_ENV } from '../harness/env.ts';
|
|
5
12
|
|
|
6
13
|
/** Shells looked for, in order. kaniko only has /busybox/sh; distroless has none. */
|
|
7
|
-
export const SHELL_CANDIDATES = [
|
|
14
|
+
export const SHELL_CANDIDATES = ['/bin/sh', '/busybox/sh', '/bin/bash'] as const;
|
|
8
15
|
|
|
9
16
|
export class ShellNotFoundError extends Error {
|
|
10
|
-
override name =
|
|
17
|
+
override name = 'ShellNotFoundError';
|
|
11
18
|
constructor() {
|
|
12
19
|
super(
|
|
13
|
-
`no shell found in this image (looked for ${SHELL_CANDIDATES.join(
|
|
20
|
+
`no shell found in this image (looked for ${SHELL_CANDIDATES.join(', ')}). ` +
|
|
14
21
|
`ctx.sh needs a shell: use an image that has one, or run binaries directly with ctx.exec([...]).`,
|
|
15
22
|
);
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export class CommandError extends Error {
|
|
20
|
-
override name =
|
|
21
|
-
constructor(
|
|
22
|
-
|
|
27
|
+
override name = 'CommandError';
|
|
28
|
+
constructor(
|
|
29
|
+
readonly what: string,
|
|
30
|
+
readonly exitCode: number | null,
|
|
31
|
+
readonly signalCode: string | null,
|
|
32
|
+
) {
|
|
33
|
+
super(
|
|
34
|
+
`${what} failed with ${signalCode ? `signal ${signalCode}` : `exit code ${exitCode}`}`,
|
|
35
|
+
);
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/** The command went quiet for longer than its `idleTimeoutSeconds` and was killed. */
|
|
27
40
|
export class CommandStalledError extends CommandError {
|
|
28
|
-
override name =
|
|
29
|
-
constructor(
|
|
41
|
+
override name = 'CommandStalledError';
|
|
42
|
+
constructor(
|
|
43
|
+
what: string,
|
|
44
|
+
readonly idleTimeoutSeconds: number,
|
|
45
|
+
signalCode: string | null,
|
|
46
|
+
) {
|
|
30
47
|
super(what, null, signalCode);
|
|
31
48
|
this.message = `${what} produced no output for ${idleTimeoutSeconds} s and was killed`;
|
|
32
49
|
}
|
|
@@ -42,7 +59,7 @@ export function detectShell(candidates: readonly string[] = SHELL_CANDIDATES): s
|
|
|
42
59
|
|
|
43
60
|
/** True for the value of `import x from "./x.sh" with { type: "file" }` (compiled or from source). */
|
|
44
61
|
export function isEmbeddedScript(s: string): boolean {
|
|
45
|
-
if (s.startsWith(
|
|
62
|
+
if (s.startsWith('/$bunfs/') || s.startsWith('B:/~BUN/')) return true;
|
|
46
63
|
if (!isAbsolute(s) || !/\.(sh|bash)$/.test(s) || /\s/.test(s)) return false;
|
|
47
64
|
try {
|
|
48
65
|
return statSync(s).isFile();
|
|
@@ -54,7 +71,8 @@ export function isEmbeddedScript(s: string): boolean {
|
|
|
54
71
|
/** Environment for child processes: the harness env minus harness-only credentials, plus extras. */
|
|
55
72
|
export function childEnv(extra?: Record<string, string>): Record<string, string> {
|
|
56
73
|
const env: Record<string, string> = {};
|
|
57
|
-
for (const [k, v] of Object.entries(process.env))
|
|
74
|
+
for (const [k, v] of Object.entries(process.env))
|
|
75
|
+
if (v !== undefined && !PRIVATE_ENV.includes(k)) env[k] = v;
|
|
58
76
|
return { ...env, ...extra };
|
|
59
77
|
}
|
|
60
78
|
|
|
@@ -64,7 +82,10 @@ export interface ShDeps {
|
|
|
64
82
|
shellCandidates?: readonly string[];
|
|
65
83
|
}
|
|
66
84
|
|
|
67
|
-
export function createSh(deps: ShDeps): {
|
|
85
|
+
export function createSh(deps: ShDeps): {
|
|
86
|
+
sh: Sh;
|
|
87
|
+
exec: (argv: readonly string[], opts?: ExecOptions) => Promise<void>;
|
|
88
|
+
} {
|
|
68
89
|
const copied = new Map<string, string>();
|
|
69
90
|
let counter = 0;
|
|
70
91
|
|
|
@@ -72,7 +93,7 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
72
93
|
const materialize = (script: string): string => {
|
|
73
94
|
const hit = copied.get(script);
|
|
74
95
|
if (hit) return hit;
|
|
75
|
-
const dir = join(deps.tmpDir,
|
|
96
|
+
const dir = join(deps.tmpDir, 'scripts');
|
|
76
97
|
mkdirSync(dir, { recursive: true });
|
|
77
98
|
const dest = join(dir, `${++counter}-${basename(script)}`);
|
|
78
99
|
writeFileSync(dest, readFileSync(script));
|
|
@@ -81,21 +102,34 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
81
102
|
return dest;
|
|
82
103
|
};
|
|
83
104
|
|
|
84
|
-
const argvFor = (
|
|
85
|
-
|
|
105
|
+
const argvFor = (
|
|
106
|
+
cmd: string | { script: string },
|
|
107
|
+
args: readonly string[],
|
|
108
|
+
): { argv: string[]; what: string } => {
|
|
109
|
+
const scriptPath =
|
|
110
|
+
typeof cmd === 'object' ? cmd.script : isEmbeddedScript(cmd) ? cmd : undefined;
|
|
86
111
|
if (scriptPath === undefined) {
|
|
87
112
|
const shell = detectShell(deps.shellCandidates);
|
|
88
113
|
// `sh -c cmd sh a b`: the args are $1, $2… of the command.
|
|
89
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
argv: [shell, '-c', cmd as string, 'sh', ...args],
|
|
116
|
+
what: `command \`${truncate(cmd as string)}\``,
|
|
117
|
+
};
|
|
90
118
|
}
|
|
91
119
|
const file = materialize(scriptPath);
|
|
92
|
-
const first = readFileSync(file,
|
|
120
|
+
const first = readFileSync(file, 'utf8').split('\n', 1)[0] ?? '';
|
|
93
121
|
const shebang = /^#!\s*(\S+)(?:\s+(.*))?$/.exec(first);
|
|
94
122
|
if (shebang && existsSync(shebang[1]!)) {
|
|
95
123
|
const interpArgs = shebang[2] ? [shebang[2].trim()] : [];
|
|
96
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
argv: [shebang[1]!, ...interpArgs, file, ...args],
|
|
126
|
+
what: `script ${basename(scriptPath)}`,
|
|
127
|
+
};
|
|
97
128
|
}
|
|
98
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
argv: [detectShell(deps.shellCandidates), file, ...args],
|
|
131
|
+
what: `script ${basename(scriptPath)}`,
|
|
132
|
+
};
|
|
99
133
|
};
|
|
100
134
|
|
|
101
135
|
const run = async (
|
|
@@ -104,22 +138,26 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
104
138
|
opts: { env?: Record<string, string>; cwd?: string; idleTimeoutSeconds?: number },
|
|
105
139
|
capture: boolean,
|
|
106
140
|
): Promise<string> => {
|
|
107
|
-
if (deps.signal.aborted)
|
|
108
|
-
|
|
109
|
-
|
|
141
|
+
if (deps.signal.aborted)
|
|
142
|
+
throw new Error(`${what} not started: the step was cancelled`);
|
|
143
|
+
const idleSeconds =
|
|
144
|
+
opts.idleTimeoutSeconds !== undefined && opts.idleTimeoutSeconds > 0
|
|
145
|
+
? opts.idleTimeoutSeconds
|
|
146
|
+
: undefined;
|
|
147
|
+
let proc: Bun.Subprocess<'ignore', 'pipe' | 'inherit', 'pipe' | 'inherit'>;
|
|
110
148
|
try {
|
|
111
149
|
proc = Bun.spawn(argv, {
|
|
112
150
|
cwd: opts.cwd,
|
|
113
151
|
env: childEnv(opts.env),
|
|
114
|
-
stdin:
|
|
115
|
-
stdout: capture || idleSeconds ?
|
|
116
|
-
stderr: idleSeconds ?
|
|
152
|
+
stdin: 'ignore',
|
|
153
|
+
stdout: capture || idleSeconds ? 'pipe' : 'inherit',
|
|
154
|
+
stderr: idleSeconds ? 'pipe' : 'inherit',
|
|
117
155
|
});
|
|
118
156
|
} catch (e) {
|
|
119
157
|
throw new Error(`cannot start ${what} (${argv[0]}): ${(e as Error).message}`);
|
|
120
158
|
}
|
|
121
|
-
const onAbort = () => proc.kill(
|
|
122
|
-
deps.signal.addEventListener(
|
|
159
|
+
const onAbort = () => proc.kill('SIGTERM');
|
|
160
|
+
deps.signal.addEventListener('abort', onAbort, { once: true });
|
|
123
161
|
|
|
124
162
|
// Watchdog: every chunk of output re-arms the timer; on expiry TERM, then KILL after a grace.
|
|
125
163
|
let stalled = false;
|
|
@@ -130,13 +168,16 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
130
168
|
clearTimeout(idleTimer);
|
|
131
169
|
idleTimer = setTimeout(() => {
|
|
132
170
|
stalled = true;
|
|
133
|
-
proc.kill(
|
|
134
|
-
killTimer = setTimeout(() => proc.kill(
|
|
171
|
+
proc.kill('SIGTERM');
|
|
172
|
+
killTimer = setTimeout(() => proc.kill('SIGKILL'), STALL_KILL_GRACE_MS);
|
|
135
173
|
}, idleSeconds * 1000);
|
|
136
174
|
};
|
|
137
175
|
const captured: Uint8Array[] = [];
|
|
138
176
|
const readers: { cancel(): Promise<void> }[] = [];
|
|
139
|
-
const pump = async (
|
|
177
|
+
const pump = async (
|
|
178
|
+
stream: ReadableStream<Uint8Array>,
|
|
179
|
+
sink: (chunk: Uint8Array) => void,
|
|
180
|
+
) => {
|
|
140
181
|
const reader = stream.getReader();
|
|
141
182
|
readers.push(reader);
|
|
142
183
|
try {
|
|
@@ -151,9 +192,18 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
151
192
|
}
|
|
152
193
|
};
|
|
153
194
|
const pumps: Promise<void>[] = [];
|
|
154
|
-
if (capture)
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
if (capture)
|
|
196
|
+
pumps.push(
|
|
197
|
+
pump(proc.stdout as ReadableStream<Uint8Array>, (c) => captured.push(c)),
|
|
198
|
+
);
|
|
199
|
+
else if (idleSeconds)
|
|
200
|
+
pumps.push(
|
|
201
|
+
pump(proc.stdout as ReadableStream<Uint8Array>, (c) => process.stdout.write(c)),
|
|
202
|
+
);
|
|
203
|
+
if (idleSeconds)
|
|
204
|
+
pumps.push(
|
|
205
|
+
pump(proc.stderr as ReadableStream<Uint8Array>, (c) => process.stderr.write(c)),
|
|
206
|
+
);
|
|
157
207
|
arm();
|
|
158
208
|
|
|
159
209
|
try {
|
|
@@ -161,13 +211,23 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
161
211
|
// A killed command's children may still hold the pipes open: don't wait for them after a stall.
|
|
162
212
|
if (stalled) await Promise.all(readers.map((r) => r.cancel().catch(() => {})));
|
|
163
213
|
else await Promise.all(pumps);
|
|
164
|
-
if (stalled)
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
if (stalled)
|
|
215
|
+
throw new CommandStalledError(
|
|
216
|
+
what,
|
|
217
|
+
idleSeconds!,
|
|
218
|
+
proc.signalCode ? String(proc.signalCode) : null,
|
|
219
|
+
);
|
|
220
|
+
if (code !== 0)
|
|
221
|
+
throw new CommandError(
|
|
222
|
+
what,
|
|
223
|
+
proc.signalCode ? null : code,
|
|
224
|
+
proc.signalCode ? String(proc.signalCode) : null,
|
|
225
|
+
);
|
|
226
|
+
return capture ? Buffer.concat(captured).toString('utf8') : '';
|
|
167
227
|
} finally {
|
|
168
228
|
clearTimeout(idleTimer);
|
|
169
229
|
clearTimeout(killTimer);
|
|
170
|
-
deps.signal.removeEventListener(
|
|
230
|
+
deps.signal.removeEventListener('abort', onAbort);
|
|
171
231
|
}
|
|
172
232
|
};
|
|
173
233
|
|
|
@@ -185,13 +245,13 @@ export function createSh(deps: ShDeps): { sh: Sh; exec: (argv: readonly string[]
|
|
|
185
245
|
) as Sh;
|
|
186
246
|
|
|
187
247
|
const exec = async (argv: readonly string[], opts: ExecOptions = {}) => {
|
|
188
|
-
if (argv.length === 0) throw new Error(
|
|
189
|
-
await run([...argv], `\`${truncate(argv.join(
|
|
248
|
+
if (argv.length === 0) throw new Error('ctx.exec needs at least the program');
|
|
249
|
+
await run([...argv], `\`${truncate(argv.join(' '))}\``, opts, false);
|
|
190
250
|
};
|
|
191
251
|
return { sh, exec };
|
|
192
252
|
}
|
|
193
253
|
|
|
194
254
|
function truncate(s: string, n = 120): string {
|
|
195
|
-
const one = s.replace(/\s+/g,
|
|
196
|
-
return one.length > n ? one.slice(0, n - 1) +
|
|
255
|
+
const one = s.replace(/\s+/g, ' ').trim();
|
|
256
|
+
return one.length > n ? one.slice(0, n - 1) + '…' : one;
|
|
197
257
|
}
|
package/src/ctx/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Skipped } from
|
|
2
|
-
import type { FileRef } from
|
|
3
|
-
import type { AnyConfigEntry, ConfigValues } from
|
|
4
|
-
import type { DeclaredSecrets } from
|
|
1
|
+
import type { Skipped } from '../core/handlers.ts';
|
|
2
|
+
import type { FileRef } from '../core/models.ts';
|
|
3
|
+
import type { AnyConfigEntry, ConfigValues } from '../core/config.ts';
|
|
4
|
+
import type { DeclaredSecrets } from '../core/secrets.ts';
|
|
5
5
|
|
|
6
6
|
/** Facts about the current execution, from the service (see README "Environment"). */
|
|
7
7
|
export interface RunInfo {
|