@olivervea/pipelines 0.0.0-stage → 2.0.0-beta.10

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -2
  3. package/manifest.schema.json +674 -0
  4. package/package.json +60 -4
  5. package/src/assets.d.ts +10 -0
  6. package/src/build/build.ts +127 -0
  7. package/src/build/cli.ts +52 -0
  8. package/src/build/elf.ts +35 -0
  9. package/src/build/hashes.ts +96 -0
  10. package/src/build/project.ts +107 -0
  11. package/src/builder/pipeline.ts +373 -0
  12. package/src/builder/refs.ts +26 -0
  13. package/src/core/brands.ts +15 -0
  14. package/src/core/config-refs.ts +77 -0
  15. package/src/core/config.ts +147 -0
  16. package/src/core/handlers.ts +50 -0
  17. package/src/core/models.ts +69 -0
  18. package/src/core/origin.ts +20 -0
  19. package/src/core/secrets.ts +13 -0
  20. package/src/core/steps.ts +138 -0
  21. package/src/core/types.ts +6 -0
  22. package/src/core/webhooks.ts +30 -0
  23. package/src/ctx/files.ts +161 -0
  24. package/src/ctx/index.ts +122 -0
  25. package/src/ctx/sh.ts +197 -0
  26. package/src/ctx/types.ts +116 -0
  27. package/src/describe/describe.ts +565 -0
  28. package/src/describe/manifest-schema.ts +179 -0
  29. package/src/describe/mapper-source.ts +238 -0
  30. package/src/describe/record.ts +107 -0
  31. package/src/dev/cli.ts +87 -0
  32. package/src/harness/env.ts +58 -0
  33. package/src/harness/execute.ts +97 -0
  34. package/src/harness/exit.ts +19 -0
  35. package/src/harness/index.ts +9 -0
  36. package/src/harness/main.ts +270 -0
  37. package/src/harness/result.ts +75 -0
  38. package/src/harness/validate.ts +92 -0
  39. package/src/index.ts +15 -0
  40. package/src/library/checks.ts +23 -0
  41. package/src/library/compose-deploy.ts +96 -0
  42. package/src/library/github-source.ts +100 -0
  43. package/src/library/helm-deploy-ssh.ts +358 -0
  44. package/src/library/helm-deploy.ts +271 -0
  45. package/src/library/http-health.ts +34 -0
  46. package/src/library/image-build.ts +418 -0
  47. package/src/library/index.ts +18 -0
  48. package/src/library/models.ts +79 -0
  49. package/src/library/ssh.ts +50 -0
  50. package/src/testing/index.ts +82 -0
  51. package/src/util/canonical.ts +19 -0
  52. package/src/util/handler-hash.ts +22 -0
  53. package/src/util/hash.ts +22 -0
  54. package/src/util/tar.ts +167 -0
@@ -0,0 +1,19 @@
1
+ // Canonical JSON: object keys sorted recursively (by UTF-16 code units), arrays kept in order
2
+ // (order is meaningful: stages, steps, enum values), `undefined` members dropped. Stable for hashing.
3
+ export function canonicalize(v: unknown): unknown {
4
+ if (Array.isArray(v)) return v.map(canonicalize);
5
+ if (v !== null && typeof v === "object") {
6
+ const out: Record<string, unknown> = {};
7
+ for (const k of Object.keys(v).sort()) {
8
+ const x = (v as Record<string, unknown>)[k];
9
+ if (x !== undefined) out[k] = canonicalize(x);
10
+ }
11
+ return out;
12
+ }
13
+ if (typeof v === "number" && !Number.isFinite(v)) throw new Error(`non-finite number in canonical JSON: ${v}`);
14
+ if (typeof v === "bigint" || typeof v === "function" || typeof v === "symbol") throw new Error(`${typeof v} is not JSON`);
15
+ return v;
16
+ }
17
+
18
+ /** Canonical JSON text, 2-space indented, with a trailing newline. */
19
+ export const canonicalJson = (v: unknown): string => JSON.stringify(canonicalize(v), null, 2) + "\n";
@@ -0,0 +1,22 @@
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
+ /** A step that carries its implementation: kind + canonical settings + harness, and the hash of the code that implements the kind. */
20
+ export function libraryHandlerHash(kind: string, config: unknown, harness: HarnessInfo, code?: string): string {
21
+ return "sha256:" + sha256Hex(canonicalJson({ library: kind, config, harness, ...(code ? { code } : {}) }));
22
+ }
@@ -0,0 +1,22 @@
1
+ import { openSync, readSync, closeSync } from "node:fs";
2
+
3
+ export function sha256Hex(data: string | Uint8Array | ArrayBuffer): string {
4
+ return new Bun.CryptoHasher("sha256").update(data).digest("hex");
5
+ }
6
+
7
+ /** Streaming SHA-256 of a file (constant memory). */
8
+ export function sha256File(path: string): string {
9
+ const h = new Bun.CryptoHasher("sha256");
10
+ const fd = openSync(path, "r");
11
+ try {
12
+ const buf = new Uint8Array(1 << 20);
13
+ for (;;) {
14
+ const n = readSync(fd, buf, 0, buf.length, null);
15
+ if (n <= 0) break;
16
+ h.update(buf.subarray(0, n));
17
+ }
18
+ } finally {
19
+ closeSync(fd);
20
+ }
21
+ return h.digest("hex");
22
+ }
@@ -0,0 +1,167 @@
1
+ // Deterministic tar writer (ustar + PAX long names), streaming from disk. Entries are sorted,
2
+ // with mtime 0, uid/gid 0, no user names — so equal content gives byte-identical archives (and
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";
8
+
9
+ 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 };
14
+
15
+ const enc = new TextEncoder();
16
+
17
+ function octal(n: number, len: number): string {
18
+ return n.toString(8).padStart(len - 1, "0") + "\0";
19
+ }
20
+
21
+ function header(name: string, size: number, mode: number, typeflag: string, linkname = ""): Uint8Array {
22
+ const h = new Uint8Array(512);
23
+ const put = (s: string | Uint8Array, off: number, len: number) => {
24
+ const b = typeof s === "string" ? enc.encode(s) : s;
25
+ h.set(b.subarray(0, len), off);
26
+ };
27
+ put(name, 0, 100);
28
+ put(octal(mode & 0o7777, 8), 100, 8);
29
+ put(octal(0, 8), 108, 8);
30
+ put(octal(0, 8), 116, 8);
31
+ put(octal(size, 12), 124, 12);
32
+ put(octal(0, 12), 136, 12);
33
+ put(" ", 148, 8); // checksum placeholder
34
+ put(typeflag, 156, 1);
35
+ put(linkname, 157, 100);
36
+ put("ustar\0", 257, 6);
37
+ put("00", 263, 2);
38
+ let sum = 0;
39
+ for (const b of h) sum += b;
40
+ put(octal(sum, 7) + " ", 148, 8);
41
+ return h;
42
+ }
43
+
44
+ function paxRecord(key: string, value: string): string {
45
+ const body = ` ${key}=${value}\n`;
46
+ let len = enc.encode(body).length;
47
+ let total = len + String(len).length;
48
+ if (String(total).length !== String(len).length) total = len + String(total).length;
49
+ return `${total}${body}`;
50
+ }
51
+
52
+ /** Writes `entries` (sorted by path here) as an uncompressed tar to `out`. */
53
+ 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));
55
+ const tmp = `${out}.partial`;
56
+ const fd = openSync(tmp, "w");
57
+ const write = (b: Uint8Array) => {
58
+ let off = 0;
59
+ while (off < b.length) off += writeSync(fd, b, off, b.length - off);
60
+ };
61
+ const pad = (size: number) => {
62
+ const r = size % 512;
63
+ if (r) write(new Uint8Array(512 - r));
64
+ };
65
+ try {
66
+ 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 : "";
69
+ let size = 0;
70
+ if (e.type === "file") size = lstatSync(e.source).size;
71
+ else if (e.type === "bytes") size = e.data.length;
72
+ 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));
75
+ if (pax.length) {
76
+ const body = enc.encode(pax.join(""));
77
+ write(header("././@PaxHeader", body.length, 0o644, "x"));
78
+ write(body);
79
+ pad(body.length);
80
+ }
81
+ 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");
90
+ try {
91
+ const buf = new Uint8Array(1 << 20);
92
+ let left = size;
93
+ while (left > 0) {
94
+ const n = readSync(src, buf, 0, Math.min(buf.length, left), null);
95
+ if (n <= 0) throw new Error(`${e.source} shrank while archiving`);
96
+ write(buf.subarray(0, n));
97
+ left -= n;
98
+ }
99
+ } finally {
100
+ closeSync(src);
101
+ }
102
+ pad(size);
103
+ }
104
+ }
105
+ write(new Uint8Array(1024));
106
+ } finally {
107
+ closeSync(fd);
108
+ }
109
+ renameSync(tmp, out);
110
+ }
111
+
112
+ /** Entries for the contents of `dir` (recursively), with paths prefixed by `prefix` ("" = none). */
113
+ export function dirEntries(dir: string, prefix = ""): TarEntry[] {
114
+ const out: TarEntry[] = [];
115
+ const walk = (abs: string, rel: string) => {
116
+ for (const name of readdirSync(abs).sort()) {
117
+ const a = join(abs, name);
118
+ const r = rel ? `${rel}/${name}` : name;
119
+ const st = lstatSync(a);
120
+ if (st.isSymbolicLink()) out.push({ path: r, type: "symlink", target: readlinkSync(a) });
121
+ else if (st.isDirectory()) {
122
+ out.push({ path: r, type: "dir", mode: 0o755 });
123
+ walk(a, r);
124
+ } else if (st.isFile()) out.push({ path: r, type: "file", mode: st.mode & 0o111 ? 0o755 : 0o644, source: a });
125
+ // sockets, fifos, devices are skipped
126
+ }
127
+ };
128
+ walk(dir, prefix);
129
+ return out;
130
+ }
131
+
132
+ /** gzip `src` into `out` (streaming; zlib writes mtime 0, so output is deterministic). */
133
+ export async function gzipFile(src: string, out: string, level = 6): Promise<void> {
134
+ const tmp = `${out}.partial`;
135
+ await pipeline(createReadStream(src), createGzip({ level }), createWriteStream(tmp));
136
+ renameSync(tmp, out);
137
+ }
138
+
139
+ /** Writes a deterministic `.tar.gz`. */
140
+ export async function writeTarGz(out: string, entries: readonly TarEntry[], level = 6): Promise<void> {
141
+ const tar = `${out}.tar-tmp`;
142
+ try {
143
+ writeTar(tar, entries);
144
+ await gzipFile(tar, out, level);
145
+ } finally {
146
+ rmSync(tar, { force: true });
147
+ }
148
+ }
149
+
150
+ /** Writes a deterministic zstd-compressed tar (level 3: fast to write, and unpacks far faster than gzip). */
151
+ export async function writeTarZst(out: string, entries: readonly TarEntry[], level = 3): Promise<void> {
152
+ const tar = `${out}.tar-tmp`;
153
+ const tmp = `${out}.partial`;
154
+ try {
155
+ writeTar(tar, entries);
156
+ await Bun.write(tmp, await Bun.zstdCompress(await Bun.file(tar).bytes(), { level }));
157
+ renameSync(tmp, out);
158
+ } finally {
159
+ rmSync(tar, { force: true });
160
+ }
161
+ }
162
+
163
+ /** Extracts a tar or tar.gz file into `dir`. */
164
+ export async function extractTar(archive: string, dir: string): Promise<void> {
165
+ const a = new Bun.Archive(await Bun.file(archive).bytes());
166
+ await a.extract(dir);
167
+ }