@xemahq/repo-build-tooling 0.26.1 → 0.26.3

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 CHANGED
@@ -41,7 +41,7 @@ pnpm exec xema-scrub-swagger-paths dist
41
41
 
42
42
  `dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image that only COPIES a tree the CI runner staged: no `RUN pnpm install`, no corepack, no secret, no network (every RUN is `--network=none`). It requires `--build-source`, `--base <ref@sha256:…>` (the digest-pinned image the stages start FROM; a reference without a digest is refused) and `--node-major <n>` (that base's Node major, rendered as the `dev.xema.composed-image.node-major` label, which the stage compares with the runner's Node); `--copy` and `--stage-dir` are optional and `--cache-id` is refused (there is no cache). The proof stage COPIES the root build inputs (`pnpm-lock.yaml`, root `package.json`, `pnpm-workspace.yaml`, `.npmrc`, `.pnpmfile.cjs`, `turbo.json`, `tsconfig*.json`, whichever exist) into `/proof/context/` and the `--copy` directories into `/proof/copied/`, which puts the inputs that decided the staged tree in the image's content tag, and runs `prove`, `prisma-check` and `load --each`; the final stage COPIES the two staged layers and the proof's verdict. The `--filter <package>...` words in the `prove` line are literal: the content hash reads them to narrow the lockfile.
43
43
 
44
- `stage-tree <dir> --platform linux/<amd64|arm64> [--output <rel>]…` is the runner half, run after the build and the job's own `pnpm install --frozen-lockfile`. It checks the members' build outputs (as `stage` does), then runs an OFFLINE, frozen, `--ignore-scripts` production install of the members' closures into a staging tree from the job's pnpm store, selected for the target platform (`--os=linux --cpu=<arch> --libc=glibc`, workspace packages injected, the re-resolved lockfile checked to pin nothing new); copies in only each package's `package.json` and declared `files` (plus the built outputs among them), the `--copy` directories, the biome manifests and `image-build-inputs.json`; writes the member index and host link; fetches the platform's Prisma schema engine (checked against a sha256 COMMITTED in this tool per engines commit and binaryTarget, never against the checksum Prisma publishes beside it; an engines commit or target not in the table is refused; never cached) when the closure carries the prisma CLI; and refuses a dependency that needs its build script on the target, a package whose native code holds no ELF for the platform, an uncommitted change to a tracked file and an untracked root build input. Output: `<dir>/layers/store` (the registry packages, stable until the lockfile changes), `<dir>/layers/app` (everything else), `<dir>/proof/xema-composed-image.json` (commit, sha256 of each root build input and `--copy` directory, the literal filters, members, natives, engine, tool version, platform) and `<dir>/tool`. `--also` is not accepted. A refused stage leaves nothing behind. The in-image `prove` binds the stage to the build: it refuses unless the manifest names `BUILD_SHA`, the context's root build inputs and `--copy` directories, this tool version, the platform the build runs on and the Dockerfile's filters, and every addon loads and the engine runs there. It does not detect a stage altered on the runner after `stage-tree` wrote it (manifest and layers come from the same job); the image digest and provenance cover what ships. The lockfile re-resolve runs `--offline`. `stage <dir> [--output <rel>]… [--also <pkg>]…` remains as the outputs-only stage (`<dir>/out`, `<dir>/tool`) for per-service prebuilt images that still read it. `pnpm-context <dir> --subject <svc>…` builds the offline `xema-pnpm` context for those images (`staged.json`, `corepack/pnpm.tgz`); it prints `XEMA_PNPM_CONTEXT=<dir>` and `XEMA_PNPM_STORE=<store root>` and refuses a non-empty `<dir>`. `load --each` loads every member in its own process. Run `xema-composed-image` with no arguments for the full usage.
44
+ `stage-tree <dir> --platform linux/<amd64|arm64> [--output <rel>]…` is the runner half, run after the build and the job's own `pnpm install --frozen-lockfile`. It checks the members' build outputs (as `stage` does), then runs an OFFLINE, frozen, `--ignore-scripts` production install of the members' closures into a staging tree from the job's pnpm store, selected for the target platform (`--os=linux --cpu=<arch> --libc=glibc`, workspace packages injected, the re-resolved lockfile checked to pin nothing new); copies in only each package's `package.json` and declared `files` (plus the built outputs among them), the `--copy` directories, the biome manifests and `image-build-inputs.json`; writes the member index and host link; fetches the platform's Prisma schema engine (checked against a sha256 COMMITTED in this tool per engines commit and binaryTarget, never against the checksum Prisma publishes beside it; an engines commit or target not in the table is refused; never cached) when the closure carries the prisma CLI; and refuses a dependency that needs its build script on the target, a package whose native code holds no ELF for the platform, an uncommitted change to a tracked file and an untracked root build input. Output: `<dir>/layers/store` (the registry packages, stable until the lockfile changes), `<dir>/layers/app` (everything else), `<dir>/proof/xema-composed-image.json` (commit, sha256 of each root build input and `--copy` directory, the literal filters, members, natives, engine, tool version, platform) and `<dir>/tool`. `--also` is not accepted. A refused stage leaves nothing behind. The in-image `prove` binds the stage to the build: it refuses unless the manifest names `BUILD_SHA`, the context's root build inputs and `--copy` directories, this tool version, the platform the build runs on and the Dockerfile's filters, and every addon loads and the engine runs there. It does not detect a stage altered on the runner after `stage-tree` wrote it (manifest and layers come from the same job); the image digest and provenance cover what ships. The lockfile re-resolve runs `--prefer-offline` (its pins are checked unchanged; the production install is `--offline`). `stage <dir> [--output <rel>]… [--also <pkg>]…` remains as the outputs-only stage (`<dir>/out`, `<dir>/tool`) for per-service prebuilt images that still read it. `pnpm-context <dir> --subject <svc>…` builds the offline `xema-pnpm` context for those images (`staged.json`, `corepack/pnpm.tgz`); it prints `XEMA_PNPM_CONTEXT=<dir>` and `XEMA_PNPM_STORE=<store root>` and refuses a non-empty `<dir>`. `load --each` loads every member in its own process. Run `xema-composed-image` with no arguments for the full usage.
45
45
 
46
46
  Every step refuses rather than skips. The package has no dependencies.
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM, depending only on @xemahq/release-policy for registry access, so the published artifact is the reviewed source.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -54,6 +54,6 @@
54
54
  "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs && node --test src/composed-image.test.mjs && node --test src/composed-image-coordinate-cli.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/load-image.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs && node --test src/service-bootstrap.test.mjs && node --test src/service-bootstrap-protocols.test.mjs && node --test src/composed-image-prebuilt-dockerfile.test.mjs && node --test src/composed-image-in-image-closure.test.mjs && node --test src/composed-image-stage.test.mjs && node --test src/composed-image-stage-tree.test.mjs && node --test src/composed-image-install.test.mjs && node --test src/composed-image-proof.test.mjs && node --test src/composed-image-pnpm-context.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-image-prisma-engine.test.mjs && node --test src/composed-migrate.test.mjs && node --test src/promote-image.test.mjs && node --test src/arm64-supplement-needed.test.mjs && node --test src/prepush-sanity.test.mjs && node --test src/service-image.test.mjs && node --test src/install-git-hooks.test.mjs"
55
55
  },
56
56
  "xemaRelease": {
57
- "sourceDigest": "sha256:22f615a6346ac6348378921933b2ebf602603c3d87fe6cd4350c8b238ddd6a1f"
57
+ "sourceDigest": "sha256:e36e1ebf9b6c0b4f12640d1f24212c43bb61d9a5a532279fb4f12a20dde70519"
58
58
  }
59
59
  }
@@ -14,8 +14,14 @@ import { assertPinsUnchanged } from './composed-image-in-image.mjs';
14
14
  /** The install-flag words that make pnpm copy workspace packages into the store layout. */
15
15
  export const injectFlags = () => ['--config.inject-workspace-packages=true', '--config.dedupe-injected-deps=false'];
16
16
 
17
- /** The `--lockfile-only` re-resolve for `filters` (`['--filter', '<pkg>...', ...]`) as `[file, ...args]`. */
18
- export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-only', '--offline', ...injectFlags(), ...filters];
17
+ /**
18
+ * The `--lockfile-only` re-resolve for `filters` (`['--filter', '<pkg>...', ...]`) as `[file, ...args]`.
19
+ * `--prefer-offline`, not `--offline`: the re-resolve needs registry metadata for ranges the committed lockfile
20
+ * already pins, and a runner's metadata cache is cold or stale (a frozen install does not fill it), so `--offline`
21
+ * fails with ERR_PNPM_NO_OFFLINE_META. Registry access here cannot change what ships: `assertPinsUnchanged` fails
22
+ * the stage if the re-resolve moves any pin, and the production install after it is `--offline --frozen-lockfile`.
23
+ */
24
+ export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-only', '--prefer-offline', ...injectFlags(), ...filters];
19
25
 
20
26
  /** The production install for `filters` on `linux/<arch>` as `[file, ...args]`. */
21
27
  export const productionInstallCommand = (filters, arch) => [
@@ -25,7 +25,7 @@ test('the re-resolve then the production install run in the tree, offline and fr
25
25
  const install = calls[1].line.join(' ');
26
26
  for (const word of ['install --prod --offline --ignore-scripts', '--frozen-lockfile', '--os=linux --cpu=arm64 --libc=glibc', '--config.inject-workspace-packages=true', '--filter @t/a... --filter @t/b...']) assert.ok(install.includes(word), word);
27
27
  assert.doesNotMatch(install, /--prefer-offline/);
28
- assert.ok(calls[0].line.includes('--offline') && !calls[0].line.includes('--prefer-offline'));
28
+ assert.ok(calls[0].line.includes('--prefer-offline') && !calls[0].line.includes('--offline'));
29
29
  assert.ok(calls[0].line.includes('--lockfile-only') && !calls[0].line.includes('--frozen-lockfile'));
30
30
  });
31
31
 
@@ -85,10 +85,31 @@ export function deployedPackages(appDir) {
85
85
 
86
86
  const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
87
87
 
88
- async function get(url) {
89
- const response = await fetch(url);
90
- if (!response.ok) throw new Error(`GET ${url}: ${response.status} ${response.statusText}`);
91
- return Buffer.from(await response.arrayBuffer());
88
+ /** What names a failed engine download: the engine, the URL, the engines commit and the target. */
89
+ const engineFailure = ({ url, enginesVersion, binaryTarget }, detail) =>
90
+ `Prisma schema-engine download failed: ${detail} (url ${url}, engines commit ${enginesVersion}, target ${binaryTarget})`;
91
+
92
+ /** GET `url` as bytes. A transport failure or a non-2xx status names the engine, URL, commit, target and bytes received. */
93
+ async function getEngine(url, identity) {
94
+ const named = { url, ...identity };
95
+ let response;
96
+ try {
97
+ response = await fetch(url);
98
+ } catch (cause) {
99
+ throw new Error(engineFailure(named, `request failed after 0 bytes: ${cause?.message ?? cause}`), { cause });
100
+ }
101
+ if (!response.ok) throw new Error(engineFailure(named, `HTTP ${response.status} ${response.statusText}`));
102
+ const chunks = [];
103
+ let received = 0;
104
+ try {
105
+ for await (const chunk of response.body) {
106
+ chunks.push(chunk);
107
+ received += chunk.length;
108
+ }
109
+ } catch (cause) {
110
+ throw new Error(engineFailure(named, `body read failed after ${received} bytes: ${cause?.message ?? cause}`), { cause });
111
+ }
112
+ return Buffer.concat(chunks);
92
113
  }
93
114
 
94
115
  /**
@@ -105,12 +126,32 @@ export const SCHEMA_ENGINE_SHA256 = {
105
126
  },
106
127
  };
107
128
 
129
+ /** In-process memo of the verified engine bytes, by `${enginesVersion}:${binaryTarget}`; holds the promise so concurrent stagers share one download. */
130
+ const verifiedEngines = new Map();
131
+
132
+ async function fetchVerifiedEngine({ binaryTarget, enginesVersion, expected }) {
133
+ const url = `${PRISMA_ENGINES_URL}/${enginesVersion}/${binaryTarget}/schema-engine`;
134
+ const identity = { enginesVersion, binaryTarget };
135
+ const compressed = await getEngine(`${url}.gz`, identity);
136
+ let engine;
137
+ try {
138
+ engine = gunzipSync(compressed);
139
+ } catch (cause) {
140
+ throw new Error(engineFailure({ url: `${url}.gz`, ...identity }, `gunzip failed on ${compressed.length} bytes: ${cause?.message ?? cause}`), { cause });
141
+ }
142
+ const actual = sha256(engine);
143
+ if (actual !== expected) {
144
+ throw new Error(`Prisma schema-engine ${url}.gz (engines commit ${enginesVersion}, target ${binaryTarget}) unzips to sha256 ${actual}, the committed sha256 is ${expected}`);
145
+ }
146
+ return engine;
147
+ }
148
+
108
149
  /**
109
150
  * The schema engine at `enginesVersion` (the engines commit the lockfile pins, through the
110
- * deploy's @prisma/engines-version) for `binaryTarget`, fetched from Prisma for every stage and
151
+ * deploy's @prisma/engines-version) for `binaryTarget`, fetched from Prisma once per process (the verified bytes are memoised by commit and target) and
111
152
  * written to `enginesDir`. The unzipped engine must have the sha256 `checksums` commits for
112
153
  * (enginesVersion, binaryTarget); a pair it does not list is refused before any download. There is
113
- * no cache: Prisma's own downloader keeps one across runs in the home directory, honours mirror
154
+ * no cross-run cache: Prisma's own downloader keeps one across runs in the home directory, honours mirror
114
155
  * variables and retries, so it is not used.
115
156
  */
116
157
  export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVersion, checksums = SCHEMA_ENGINE_SHA256 }) {
@@ -119,9 +160,17 @@ export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVe
119
160
  if (expected === undefined) {
120
161
  throw new Error(`no committed sha256 for the Prisma schema engine at engines commit ${enginesVersion} for ${binaryTarget}: add it to SCHEMA_ENGINE_SHA256 in service-image-stage.mjs`);
121
162
  }
122
- const url = `${PRISMA_ENGINES_URL}/${enginesVersion}/${binaryTarget}/schema-engine`;
123
- const engine = gunzipSync(await get(`${url}.gz`));
124
- if (sha256(engine) !== expected) throw new Error(`${url}.gz unzips to sha256 ${sha256(engine)}, the committed sha256 is ${expected}`);
163
+ const key = `${enginesVersion}:${binaryTarget}`;
164
+ let verified = verifiedEngines.get(key);
165
+ if (verified === undefined) {
166
+ verified = fetchVerifiedEngine({ binaryTarget, enginesVersion, expected });
167
+ verifiedEngines.set(key, verified);
168
+ // A failed download is not remembered: the next call fetches again.
169
+ verified.catch(() => {
170
+ if (verifiedEngines.get(key) === verified) verifiedEngines.delete(key);
171
+ });
172
+ }
173
+ const engine = await verified;
125
174
  const file = join(enginesDir, `schema-engine-${binaryTarget}`);
126
175
  writeFileSync(file, engine);
127
176
  chmodSync(file, 0o755);
@@ -282,8 +282,10 @@ test('downloadSchemaEngine fetches the pinned engine, checks it against the COMM
282
282
 
283
283
  // A binary whose published .sha256 agrees with it is still refused when it is not the committed one.
284
284
  const other = elf(184);
285
- serve({ [`${url}.gz`]: gzipSync(other), [`${url}.sha256`]: `${sha256(other)} x\n` });
286
- await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }), /unzips to sha256 .*, the committed sha256 is /);
285
+ const mismatchChecksums = { ['d'.repeat(40)]: { [target]: sha256(engine) } };
286
+ const mismatchUrl = `${PRISMA_ENGINES_URL}/${'d'.repeat(40)}/${target}/schema-engine`;
287
+ serve({ [`${mismatchUrl}.gz`]: gzipSync(other), [`${mismatchUrl}.sha256`]: `${sha256(other)} x\n` });
288
+ await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: 'd'.repeat(40), checksums: mismatchChecksums }), new RegExp(`unzips to sha256 ${sha256(other)}, the committed sha256 is ${sha256(engine)}`));
287
289
  t.mock.restoreAll();
288
290
  const unfetched = serve({});
289
291
  await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: 'c'.repeat(40), checksums }), /no committed sha256 .* engines commit c{40} for linux-arm64/);
@@ -292,6 +294,94 @@ test('downloadSchemaEngine fetches the pinned engine, checks it against the COMM
292
294
  await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'x', enginesVersion: '1.2.3' }), /is not an engines commit/);
293
295
  });
294
296
 
297
+ test('downloadSchemaEngine downloads once per engines commit and target, however many services stage', async (t) => {
298
+ const engine = elf(185);
299
+ const version = 'e'.repeat(40);
300
+ const target = 'linux-arm64-openssl-3.0.x';
301
+ const checksums = { [version]: { [target]: sha256(engine) } };
302
+ const fetches = [];
303
+ t.mock.method(globalThis, 'fetch', async (asked) => {
304
+ fetches.push(asked);
305
+ return new Response(gzipSync(engine));
306
+ });
307
+ const dirs = [temp(), temp(), temp()];
308
+ await Promise.all(dirs.slice(0, 2).map((enginesDir) => downloadSchemaEngine({ enginesDir, binaryTarget: target, enginesVersion: version, checksums })));
309
+ await downloadSchemaEngine({ enginesDir: dirs[2], binaryTarget: target, enginesVersion: version, checksums });
310
+ assert.equal(fetches.length, 1);
311
+ for (const dir of dirs) assert.deepEqual(fs.readFileSync(path.join(dir, `schema-engine-${target}`)), engine);
312
+ });
313
+
314
+ test('downloadSchemaEngine names the engine, URL, commit, target, bytes and cause when the body read fails', async (t) => {
315
+ const engine = elf(186);
316
+ const version = 'f'.repeat(40);
317
+ const target = 'linux-arm64-openssl-3.0.x';
318
+ const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine.gz`;
319
+ const checksums = { [version]: { [target]: sha256(engine) } };
320
+ t.mock.method(globalThis, 'fetch', async () => {
321
+ let sent = false;
322
+ const body = new ReadableStream({
323
+ pull(controller) {
324
+ if (sent) return controller.error(new TypeError('terminated'));
325
+ sent = true;
326
+ controller.enqueue(new Uint8Array(7));
327
+ },
328
+ });
329
+ return new Response(body);
330
+ });
331
+ const error = await downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }).then(
332
+ () => assert.fail('expected a rejection'),
333
+ (e) => e,
334
+ );
335
+ for (const part of ['schema-engine', url, version, target, '7 bytes', 'terminated']) assert.ok(error.message.includes(part), `${part} in: ${error.message}`);
336
+ assert.equal(error.cause.message, 'terminated');
337
+ });
338
+
339
+ test('downloadSchemaEngine names the engine, URL, commit and target on a non-2xx status', async (t) => {
340
+ const engine = elf(187);
341
+ const version = '1'.repeat(40);
342
+ const target = 'linux-arm64-openssl-3.0.x';
343
+ const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine.gz`;
344
+ const checksums = { [version]: { [target]: sha256(engine) } };
345
+ t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 503, statusText: 'Service Unavailable' }));
346
+ await assert.rejects(
347
+ downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }),
348
+ (error) => [`schema-engine`, url, version, target, 'HTTP 503 Service Unavailable'].every((part) => error.message.includes(part)),
349
+ );
350
+ });
351
+
352
+ test('downloadSchemaEngine does not remember a failed download', async (t) => {
353
+ const engine = elf(188);
354
+ const version = '2'.repeat(40);
355
+ const target = 'linux-arm64-openssl-3.0.x';
356
+ const checksums = { [version]: { [target]: sha256(engine) } };
357
+ let calls = 0;
358
+ t.mock.method(globalThis, 'fetch', async () => {
359
+ calls += 1;
360
+ return calls === 1 ? new Response('', { status: 502, statusText: 'Bad Gateway' }) : new Response(gzipSync(engine));
361
+ });
362
+ await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }), /HTTP 502/);
363
+ const enginesDir = temp();
364
+ await downloadSchemaEngine({ enginesDir, binaryTarget: target, enginesVersion: version, checksums });
365
+ assert.equal(calls, 2);
366
+ assert.deepEqual(fs.readFileSync(path.join(enginesDir, `schema-engine-${target}`)), engine);
367
+ });
368
+
369
+ test('downloadSchemaEngine names the engine, URL, commit, target and cause when the gzip is corrupt', async (t) => {
370
+ const engine = elf(189);
371
+ const version = '3'.repeat(40);
372
+ const target = 'linux-arm64-openssl-3.0.x';
373
+ const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine.gz`;
374
+ const checksums = { [version]: { [target]: sha256(engine) } };
375
+ t.mock.method(globalThis, 'fetch', async () => new Response(Buffer.from('not gzip at all')));
376
+ const error = await downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }).then(
377
+ () => assert.fail('expected a rejection'),
378
+ (e) => e,
379
+ );
380
+ for (const part of ['schema-engine', url, version, target, 'gunzip failed']) assert.ok(error.message.includes(part), `${part} in: ${error.message}`);
381
+ assert.ok(error.cause instanceof Error);
382
+ assert.ok(error.message.includes(error.cause.message));
383
+ });
384
+
295
385
  test('the committed engine table has the amd64 and arm64 rows for the engines commit xema-base pins', () => {
296
386
  const rows = SCHEMA_ENGINE_SHA256['3c6e192761c0362d496ed980de936e2f3cebcd3a'];
297
387
  assert.deepEqual(Object.keys(rows).sort(), ['debian-openssl-3.0.x', 'linux-arm64-openssl-3.0.x']);