@olivervea/pipelines 2.0.0-beta.3 → 2.0.0-beta.30
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 +163 -78
- package/manifest.schema.json +300 -16
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +98 -64
- package/src/build/cli.ts +33 -23
- package/src/build/elf.ts +16 -7
- package/src/build/hashes.ts +40 -51
- package/src/build/project.ts +55 -52
- package/src/builder/pipeline.ts +348 -89
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +6 -5
- package/src/core/config-refs.ts +103 -0
- package/src/core/config.ts +224 -0
- package/src/core/external.ts +86 -0
- package/src/core/handlers.ts +62 -15
- package/src/core/matrix.ts +150 -0
- package/src/core/models.ts +37 -15
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +113 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +119 -42
- package/src/ctx/index.ts +103 -38
- package/src/ctx/links.ts +30 -0
- package/src/ctx/sh.ts +104 -44
- package/src/ctx/types.ts +24 -4
- package/src/describe/config-schema.ts +98 -0
- package/src/describe/context.ts +187 -0
- package/src/describe/describe.ts +63 -355
- package/src/describe/failure-handlers.ts +34 -0
- package/src/describe/manifest-schema.ts +304 -61
- package/src/describe/mapper-source.ts +252 -105
- package/src/describe/record.ts +67 -37
- package/src/describe/schemas.ts +71 -0
- package/src/describe/stages.ts +166 -0
- package/src/describe/step.ts +544 -0
- package/src/describe/triggers.ts +72 -0
- package/src/dev/cli.ts +62 -35
- package/src/harness/env.ts +47 -25
- package/src/harness/execute.ts +74 -30
- package/src/harness/exit.ts +1 -1
- package/src/harness/index.ts +19 -8
- package/src/harness/main.ts +234 -98
- package/src/harness/result.ts +45 -24
- package/src/harness/validate.ts +82 -15
- package/src/index.ts +139 -10
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +115 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +426 -112
- package/src/library/helm-deploy.ts +529 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +510 -162
- package/src/library/index.ts +48 -14
- package/src/library/models.ts +35 -14
- package/src/library/ssh.ts +44 -18
- package/src/testing/index.ts +100 -35
- package/src/util/canonical.ts +7 -4
- package/src/util/code-hash.ts +17 -0
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +83 -38
- package/src/library/script.ts +0 -85
- package/src/util/handler-hash.ts +0 -22
package/src/util/tar.ts
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
// Deterministic tar writer (ustar + PAX long names), streaming from disk. Entries are sorted,
|
|
2
2
|
// with mtime 0, uid/gid 0, no user names — so equal content gives byte-identical archives (and
|
|
3
3
|
// sha256s). Extraction uses Bun.Archive.
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
createReadStream,
|
|
7
|
+
createWriteStream,
|
|
8
|
+
lstatSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
readSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeSync,
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { pipeline } from 'node:stream/promises';
|
|
19
|
+
import { createGzip } from 'node:zlib';
|
|
8
20
|
|
|
9
21
|
export type TarEntry =
|
|
10
|
-
| { path: string; type:
|
|
11
|
-
| { path: string; type:
|
|
12
|
-
| { path: string; type:
|
|
13
|
-
| { path: string; type: "symlink"; target: string };
|
|
22
|
+
| { path: string; type: 'file'; mode: number; source: string }
|
|
23
|
+
| { path: string; type: 'dir'; mode: number }
|
|
24
|
+
| { path: string; type: 'symlink'; target: string };
|
|
14
25
|
|
|
15
26
|
const enc = new TextEncoder();
|
|
16
27
|
|
|
17
28
|
function octal(n: number, len: number): string {
|
|
18
|
-
return n.toString(8).padStart(len - 1,
|
|
29
|
+
return n.toString(8).padStart(len - 1, '0') + '\0';
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
function header(
|
|
32
|
+
function header(
|
|
33
|
+
name: string,
|
|
34
|
+
size: number,
|
|
35
|
+
mode: number,
|
|
36
|
+
typeflag: string,
|
|
37
|
+
linkname = '',
|
|
38
|
+
): Uint8Array {
|
|
22
39
|
const h = new Uint8Array(512);
|
|
23
40
|
const put = (s: string | Uint8Array, off: number, len: number) => {
|
|
24
|
-
const b = typeof s ===
|
|
41
|
+
const b = typeof s === 'string' ? enc.encode(s) : s;
|
|
25
42
|
h.set(b.subarray(0, len), off);
|
|
26
43
|
};
|
|
27
44
|
put(name, 0, 100);
|
|
@@ -30,14 +47,14 @@ function header(name: string, size: number, mode: number, typeflag: string, link
|
|
|
30
47
|
put(octal(0, 8), 116, 8);
|
|
31
48
|
put(octal(size, 12), 124, 12);
|
|
32
49
|
put(octal(0, 12), 136, 12);
|
|
33
|
-
put(
|
|
50
|
+
put(' ', 148, 8); // checksum placeholder
|
|
34
51
|
put(typeflag, 156, 1);
|
|
35
52
|
put(linkname, 157, 100);
|
|
36
|
-
put(
|
|
37
|
-
put(
|
|
53
|
+
put('ustar\0', 257, 6);
|
|
54
|
+
put('00', 263, 2);
|
|
38
55
|
let sum = 0;
|
|
39
56
|
for (const b of h) sum += b;
|
|
40
|
-
put(octal(sum, 7) +
|
|
57
|
+
put(octal(sum, 7) + ' ', 148, 8);
|
|
41
58
|
return h;
|
|
42
59
|
}
|
|
43
60
|
|
|
@@ -51,9 +68,11 @@ function paxRecord(key: string, value: string): string {
|
|
|
51
68
|
|
|
52
69
|
/** Writes `entries` (sorted by path here) as an uncompressed tar to `out`. */
|
|
53
70
|
export function writeTar(out: string, entries: readonly TarEntry[]): void {
|
|
54
|
-
const sorted = [...entries].sort((a, b) =>
|
|
71
|
+
const sorted = [...entries].sort((a, b) =>
|
|
72
|
+
a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
|
|
73
|
+
);
|
|
55
74
|
const tmp = `${out}.partial`;
|
|
56
|
-
const fd = openSync(tmp,
|
|
75
|
+
const fd = openSync(tmp, 'w');
|
|
57
76
|
const write = (b: Uint8Array) => {
|
|
58
77
|
let off = 0;
|
|
59
78
|
while (off < b.length) off += writeSync(fd, b, off, b.length - off);
|
|
@@ -64,29 +83,27 @@ export function writeTar(out: string, entries: readonly TarEntry[]): void {
|
|
|
64
83
|
};
|
|
65
84
|
try {
|
|
66
85
|
for (const e of sorted) {
|
|
67
|
-
const name = e.type ===
|
|
68
|
-
const link = e.type ===
|
|
86
|
+
const name = e.type === 'dir' && !e.path.endsWith('/') ? e.path + '/' : e.path;
|
|
87
|
+
const link = e.type === 'symlink' ? e.target : '';
|
|
69
88
|
let size = 0;
|
|
70
|
-
if (e.type ===
|
|
71
|
-
else if (e.type === "bytes") size = e.data.length;
|
|
89
|
+
if (e.type === 'file') size = lstatSync(e.source).size;
|
|
72
90
|
const pax: string[] = [];
|
|
73
|
-
if (enc.encode(name).length > 100) pax.push(paxRecord(
|
|
74
|
-
if (enc.encode(link).length > 100) pax.push(paxRecord(
|
|
91
|
+
if (enc.encode(name).length > 100) pax.push(paxRecord('path', name));
|
|
92
|
+
if (enc.encode(link).length > 100) pax.push(paxRecord('linkpath', link));
|
|
75
93
|
if (pax.length) {
|
|
76
|
-
const body = enc.encode(pax.join(
|
|
77
|
-
write(header(
|
|
94
|
+
const body = enc.encode(pax.join(''));
|
|
95
|
+
write(header('././@PaxHeader', body.length, 0o644, 'x'));
|
|
78
96
|
write(body);
|
|
79
97
|
pad(body.length);
|
|
80
98
|
}
|
|
81
99
|
const shortName = enc.encode(name).length > 100 ? name.slice(0, 99) : name;
|
|
82
|
-
const mode = e.type ===
|
|
83
|
-
const flag = e.type ===
|
|
84
|
-
write(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const src = openSync(e.source, "r");
|
|
100
|
+
const mode = e.type === 'symlink' ? 0o777 : e.mode;
|
|
101
|
+
const flag = e.type === 'dir' ? '5' : e.type === 'symlink' ? '2' : '0';
|
|
102
|
+
write(
|
|
103
|
+
header(shortName, size, mode, flag, link.length > 100 ? link.slice(0, 99) : link),
|
|
104
|
+
);
|
|
105
|
+
if (e.type === 'file') {
|
|
106
|
+
const src = openSync(e.source, 'r');
|
|
90
107
|
try {
|
|
91
108
|
const buf = new Uint8Array(1 << 20);
|
|
92
109
|
let left = size;
|
|
@@ -110,18 +127,25 @@ export function writeTar(out: string, entries: readonly TarEntry[]): void {
|
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
/** Entries for the contents of `dir` (recursively), with paths prefixed by `prefix` ("" = none). */
|
|
113
|
-
export function dirEntries(dir: string, prefix =
|
|
130
|
+
export function dirEntries(dir: string, prefix = ''): TarEntry[] {
|
|
114
131
|
const out: TarEntry[] = [];
|
|
115
132
|
const walk = (abs: string, rel: string) => {
|
|
116
133
|
for (const name of readdirSync(abs).sort()) {
|
|
117
134
|
const a = join(abs, name);
|
|
118
135
|
const r = rel ? `${rel}/${name}` : name;
|
|
119
136
|
const st = lstatSync(a);
|
|
120
|
-
if (st.isSymbolicLink())
|
|
137
|
+
if (st.isSymbolicLink())
|
|
138
|
+
out.push({ path: r, type: 'symlink', target: readlinkSync(a) });
|
|
121
139
|
else if (st.isDirectory()) {
|
|
122
|
-
out.push({ path: r, type:
|
|
140
|
+
out.push({ path: r, type: 'dir', mode: 0o755 });
|
|
123
141
|
walk(a, r);
|
|
124
|
-
} else if (st.isFile())
|
|
142
|
+
} else if (st.isFile())
|
|
143
|
+
out.push({
|
|
144
|
+
path: r,
|
|
145
|
+
type: 'file',
|
|
146
|
+
mode: st.mode & 0o111 ? 0o755 : 0o644,
|
|
147
|
+
source: a,
|
|
148
|
+
});
|
|
125
149
|
// sockets, fifos, devices are skipped
|
|
126
150
|
}
|
|
127
151
|
};
|
|
@@ -137,7 +161,11 @@ export async function gzipFile(src: string, out: string, level = 6): Promise<voi
|
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
/** Writes a deterministic `.tar.gz`. */
|
|
140
|
-
export async function writeTarGz(
|
|
164
|
+
export async function writeTarGz(
|
|
165
|
+
out: string,
|
|
166
|
+
entries: readonly TarEntry[],
|
|
167
|
+
level = 6,
|
|
168
|
+
): Promise<void> {
|
|
141
169
|
const tar = `${out}.tar-tmp`;
|
|
142
170
|
try {
|
|
143
171
|
writeTar(tar, entries);
|
|
@@ -147,6 +175,23 @@ export async function writeTarGz(out: string, entries: readonly TarEntry[], leve
|
|
|
147
175
|
}
|
|
148
176
|
}
|
|
149
177
|
|
|
178
|
+
/** Writes a deterministic zstd-compressed tar (level 3: fast to write, and unpacks far faster than gzip). */
|
|
179
|
+
export async function writeTarZst(
|
|
180
|
+
out: string,
|
|
181
|
+
entries: readonly TarEntry[],
|
|
182
|
+
level = 3,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const tar = `${out}.tar-tmp`;
|
|
185
|
+
const tmp = `${out}.partial`;
|
|
186
|
+
try {
|
|
187
|
+
writeTar(tar, entries);
|
|
188
|
+
await Bun.write(tmp, await Bun.zstdCompress(await Bun.file(tar).bytes(), { level }));
|
|
189
|
+
renameSync(tmp, out);
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(tar, { force: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
150
195
|
/** Extracts a tar or tar.gz file into `dir`. */
|
|
151
196
|
export async function extractTar(archive: string, dir: string): Promise<void> {
|
|
152
197
|
const a = new Bun.Archive(await Bun.file(archive).bytes());
|
package/src/library/script.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { Ctx, RunInfo } from "../ctx/types.ts";
|
|
4
|
-
import type { AnyModel } from "../core/models.ts";
|
|
5
|
-
import type { Secret } from "../core/secrets.ts";
|
|
6
|
-
import { makeStep, type InputSpec, type LibraryStep, type Step, type StepDef, type StepInput } from "../core/steps.ts";
|
|
7
|
-
import { sha256Hex } from "../util/hash.ts";
|
|
8
|
-
|
|
9
|
-
/** A config value that may depend on the run: a constant or a function of the step's inputs and run info. */
|
|
10
|
-
export type RunValue<I, T = string> = T | ((input: StepInput<I>, run: RunInfo) => T);
|
|
11
|
-
|
|
12
|
-
export interface ScriptDef<N extends string, I extends InputSpec, O extends AnyModel | null, Sec extends readonly Secret[]>
|
|
13
|
-
extends StepDef<N, I, O, Sec> {
|
|
14
|
-
/** An embedded script: `import s from "./scripts/x.sh" with { type: "file" }`. */
|
|
15
|
-
script: string;
|
|
16
|
-
/** Extra environment; values may be functions of the inputs (evaluated in the step, not at describe). */
|
|
17
|
-
env?: Record<string, RunValue<I>>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** `deployTo` → `INPUT_DEPLOY_TO`. */
|
|
21
|
-
export function inputEnvName(key: string): string {
|
|
22
|
-
return "INPUT_" + key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Runs a shell script (the zero-TypeScript path). Contract for the script:
|
|
27
|
-
* - `$OLVE_INPUT`: path of the validated input as JSON; each input key is also `INPUT_<KEY>`
|
|
28
|
-
* (strings raw, everything else JSON).
|
|
29
|
-
* - `$OLVE_OUTPUT`: if the step has an output model, write the output JSON there. Objects of the
|
|
30
|
-
* form `{"$file": "<path>"}` / `{"$dir": "<path>"}` are turned into file references.
|
|
31
|
-
* - Declared secrets are in the environment under their names.
|
|
32
|
-
*/
|
|
33
|
-
export function script<
|
|
34
|
-
const N extends string,
|
|
35
|
-
const I extends InputSpec = {},
|
|
36
|
-
O extends AnyModel | null = null,
|
|
37
|
-
const Sec extends readonly Secret[] = [],
|
|
38
|
-
>(def: ScriptDef<N, I, O, Sec>): Step<N, I, O, Sec[number]["name"]> & LibraryStep {
|
|
39
|
-
const env = def.env ?? {};
|
|
40
|
-
return makeStep("script", def as never, {
|
|
41
|
-
manifestConfig: () => ({
|
|
42
|
-
scriptSha256: "sha256:" + sha256Hex(readFileSync(def.script)),
|
|
43
|
-
env: Object.fromEntries(Object.entries(env).filter(([, v]) => typeof v === "string")),
|
|
44
|
-
computedEnv: Object.keys(env).filter((k) => typeof env[k] === "function").sort(),
|
|
45
|
-
}),
|
|
46
|
-
async run(input: Record<string, unknown>, ctx: Ctx) {
|
|
47
|
-
mkdirSync(ctx.tmpDir, { recursive: true });
|
|
48
|
-
const dir = mkdtempSync(join(ctx.tmpDir, "script-"));
|
|
49
|
-
const inputPath = join(dir, "input.json");
|
|
50
|
-
const outputPath = join(dir, "output.json");
|
|
51
|
-
writeFileSync(inputPath, JSON.stringify(input));
|
|
52
|
-
const vars: Record<string, string> = { OLVE_INPUT: inputPath };
|
|
53
|
-
for (const [k, v] of Object.entries(input)) {
|
|
54
|
-
if (v !== undefined) vars[inputEnvName(k)] = typeof v === "string" ? v : JSON.stringify(v);
|
|
55
|
-
}
|
|
56
|
-
for (const [k, v] of Object.entries(env)) {
|
|
57
|
-
vars[k] = typeof v === "function" ? (v as (i: unknown, r: RunInfo) => string)(input, ctx.run) : v;
|
|
58
|
-
}
|
|
59
|
-
if (def.output) vars.OLVE_OUTPUT = outputPath;
|
|
60
|
-
await ctx.sh({ script: def.script }, { env: vars });
|
|
61
|
-
if (!def.output) return undefined;
|
|
62
|
-
let raw: unknown;
|
|
63
|
-
try {
|
|
64
|
-
raw = JSON.parse(readFileSync(outputPath, "utf8"));
|
|
65
|
-
} catch (e) {
|
|
66
|
-
throw new Error(`script did not write valid JSON to $OLVE_OUTPUT: ${(e as Error).message}`);
|
|
67
|
-
}
|
|
68
|
-
return resolveFilePlaceholders(raw, ctx);
|
|
69
|
-
},
|
|
70
|
-
}) as never;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function resolveFilePlaceholders(v: unknown, ctx: Ctx): Promise<unknown> {
|
|
74
|
-
if (Array.isArray(v)) return Promise.all(v.map((x) => resolveFilePlaceholders(x, ctx)));
|
|
75
|
-
if (typeof v === "object" && v !== null) {
|
|
76
|
-
const o = v as Record<string, unknown>;
|
|
77
|
-
const keys = Object.keys(o);
|
|
78
|
-
if (keys.length === 1 && typeof o.$file === "string") return ctx.output.file(o.$file);
|
|
79
|
-
if (keys.length === 1 && typeof o.$dir === "string") return ctx.output.dir(o.$dir);
|
|
80
|
-
const out: Record<string, unknown> = {};
|
|
81
|
-
for (const k of keys) out[k] = await resolveFilePlaceholders(o[k], ctx);
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
return v;
|
|
85
|
-
}
|
package/src/util/handler-hash.ts
DELETED
|
@@ -1,22 +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
|
-
/** Library step: kind + canonical config + harness. */
|
|
20
|
-
export function libraryHandlerHash(kind: string, config: unknown, harness: HarnessInfo): string {
|
|
21
|
-
return "sha256:" + sha256Hex(canonicalJson({ library: kind, config, harness }));
|
|
22
|
-
}
|