@olivervea/pipelines 2.0.0-beta.3 → 2.0.0-beta.31

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 (65) hide show
  1. package/README.md +165 -78
  2. package/manifest.schema.json +300 -16
  3. package/package.json +6 -4
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +98 -64
  6. package/src/build/cli.ts +33 -23
  7. package/src/build/elf.ts +16 -7
  8. package/src/build/hashes.ts +40 -51
  9. package/src/build/project.ts +55 -52
  10. package/src/builder/pipeline.ts +348 -89
  11. package/src/builder/refs.ts +15 -5
  12. package/src/core/brands.ts +6 -5
  13. package/src/core/config-refs.ts +103 -0
  14. package/src/core/config.ts +224 -0
  15. package/src/core/external.ts +86 -0
  16. package/src/core/handlers.ts +62 -15
  17. package/src/core/matrix.ts +150 -0
  18. package/src/core/models.ts +37 -15
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +113 -38
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +119 -42
  24. package/src/ctx/index.ts +110 -39
  25. package/src/ctx/links.ts +30 -0
  26. package/src/ctx/sh.ts +106 -44
  27. package/src/ctx/types.ts +24 -4
  28. package/src/describe/config-schema.ts +98 -0
  29. package/src/describe/context.ts +187 -0
  30. package/src/describe/describe.ts +63 -355
  31. package/src/describe/failure-handlers.ts +34 -0
  32. package/src/describe/manifest-schema.ts +304 -61
  33. package/src/describe/mapper-source.ts +252 -105
  34. package/src/describe/record.ts +67 -37
  35. package/src/describe/schemas.ts +71 -0
  36. package/src/describe/stages.ts +166 -0
  37. package/src/describe/step.ts +544 -0
  38. package/src/describe/triggers.ts +72 -0
  39. package/src/dev/cli.ts +62 -35
  40. package/src/harness/env.ts +47 -25
  41. package/src/harness/execute.ts +74 -30
  42. package/src/harness/exit.ts +1 -1
  43. package/src/harness/index.ts +19 -8
  44. package/src/harness/main.ts +245 -99
  45. package/src/harness/mask.ts +98 -0
  46. package/src/harness/result.ts +45 -24
  47. package/src/harness/validate.ts +82 -15
  48. package/src/index.ts +139 -10
  49. package/src/library/checks.ts +11 -6
  50. package/src/library/compose-deploy.ts +115 -0
  51. package/src/library/github-source.ts +100 -47
  52. package/src/library/helm-deploy-ssh.ts +426 -112
  53. package/src/library/helm-deploy.ts +529 -0
  54. package/src/library/http-health.ts +7 -5
  55. package/src/library/image-build.ts +510 -162
  56. package/src/library/index.ts +48 -14
  57. package/src/library/models.ts +35 -14
  58. package/src/library/ssh.ts +44 -18
  59. package/src/testing/index.ts +100 -35
  60. package/src/util/canonical.ts +7 -4
  61. package/src/util/code-hash.ts +17 -0
  62. package/src/util/hash.ts +5 -5
  63. package/src/util/tar.ts +83 -38
  64. package/src/library/script.ts +0 -85
  65. 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 { closeSync, createReadStream, createWriteStream, lstatSync, openSync, readdirSync, readlinkSync, readSync, renameSync, rmSync, writeSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { pipeline } from "node:stream/promises";
7
- import { createGzip } from "node:zlib";
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: "file"; mode: number; source: string }
11
- | { path: string; type: "bytes"; mode: number; data: Uint8Array }
12
- | { path: string; type: "dir"; mode: number }
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, "0") + "\0";
29
+ return n.toString(8).padStart(len - 1, '0') + '\0';
19
30
  }
20
31
 
21
- function header(name: string, size: number, mode: number, typeflag: string, linkname = ""): Uint8Array {
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 === "string" ? enc.encode(s) : 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(" ", 148, 8); // checksum placeholder
50
+ put(' ', 148, 8); // checksum placeholder
34
51
  put(typeflag, 156, 1);
35
52
  put(linkname, 157, 100);
36
- put("ustar\0", 257, 6);
37
- put("00", 263, 2);
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) + " ", 148, 8);
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) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
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, "w");
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 === "dir" && !e.path.endsWith("/") ? e.path + "/" : e.path;
68
- const link = e.type === "symlink" ? e.target : "";
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 === "file") size = lstatSync(e.source).size;
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("path", name));
74
- if (enc.encode(link).length > 100) pax.push(paxRecord("linkpath", link));
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("././@PaxHeader", body.length, 0o644, "x"));
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 === "symlink" ? 0o777 : e.mode;
83
- const flag = e.type === "dir" ? "5" : e.type === "symlink" ? "2" : "0";
84
- write(header(shortName, size, mode, flag, link.length > 100 ? link.slice(0, 99) : link));
85
- if (e.type === "bytes") {
86
- write(e.data);
87
- pad(size);
88
- } else if (e.type === "file") {
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 = ""): TarEntry[] {
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()) out.push({ path: r, type: "symlink", target: readlinkSync(a) });
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: "dir", mode: 0o755 });
140
+ out.push({ path: r, type: 'dir', mode: 0o755 });
123
141
  walk(a, r);
124
- } else if (st.isFile()) out.push({ path: r, type: "file", mode: st.mode & 0o111 ? 0o755 : 0o644, source: a });
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(out: string, entries: readonly TarEntry[], level = 6): Promise<void> {
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());
@@ -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
- }
@@ -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
- }