@xemahq/repo-build-tooling 0.26.0 → 0.26.1

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
@@ -39,9 +39,9 @@ pnpm exec xema-scrub-swagger-paths dist
39
39
 
40
40
  `xema-composed-image members --write|--check` accepts `--manifests <glob>` (repeatable, default `**/xema-biome.json`), `--canonical-dockerfile <path>` and `--cross-repo-manifest <path>`. `xema-composed-image dockerfile` requires `--copy`, `--cache-id` and `--build-source`; it pins the version of this tool itself, and there is no flag for it (`--tooling-version` is refused as an unknown option).
41
41
 
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 exactly the bytes 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.
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 (checksum-verified, 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` refuses the build unless the stage was made at `BUILD_SHA`, from the context's bytes, with this tool version, for the platform the build runs on, with the Dockerfile's filters, and every addon loads and the engine runs there. `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 `--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.0",
3
+ "version": "0.26.1",
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:eb28744794a3da50dbb000a38b05bdd4da61176ff6495a664e1d58d22d1979ad"
57
+ "sourceDigest": "sha256:22f615a6346ac6348378921933b2ebf602603c3d87fe6cd4350c8b238ddd6a1f"
58
58
  }
59
59
  }
@@ -15,7 +15,7 @@ import { assertPinsUnchanged } from './composed-image-in-image.mjs';
15
15
  export const injectFlags = () => ['--config.inject-workspace-packages=true', '--config.dedupe-injected-deps=false'];
16
16
 
17
17
  /** The `--lockfile-only` re-resolve for `filters` (`['--filter', '<pkg>...', ...]`) as `[file, ...args]`. */
18
- export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-only', '--prefer-offline', ...injectFlags(), ...filters];
18
+ export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-only', '--offline', ...injectFlags(), ...filters];
19
19
 
20
20
  /** The production install for `filters` on `linux/<arch>` as `[file, ...args]`. */
21
21
  export const productionInstallCommand = (filters, arch) => [
@@ -25,6 +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
29
  assert.ok(calls[0].line.includes('--lockfile-only') && !calls[0].line.includes('--frozen-lockfile'));
29
30
  });
30
31
 
@@ -83,9 +83,9 @@ export function renderPrebuiltDockerfile({ root = process.cwd(), imageDir = COMP
83
83
  #
84
84
  # Stages:
85
85
  # proof The staged tree and the repository's inputs meet. The root build inputs that decide the
86
- # staged bytes are COPIED into \`/${PROOF_DIRECTORY}/${CONTEXT_DIRECTORY}/\` and the \`--copy\` directories into
86
+ # staged tree are COPIED into \`/${PROOF_DIRECTORY}/${CONTEXT_DIRECTORY}/\` and the \`--copy\` directories into
87
87
  # \`/${COPIED_DIRECTORY}/\`, which puts them in the image's content tag. \`prove\` refuses the build unless the
88
- # stage was made at BUILD_SHA from exactly those bytes, with this tool, for this platform, and
88
+ # stage's manifest names BUILD_SHA and those bytes, this tool and this platform, and
89
89
  # the literal \`--filter\` words below are the ones it staged. Then the Prisma clients run and every
90
90
  # member loads ALONE as \`node\`, production, on the target arch.
91
91
  # (final) the same two COPYs of the staged tree, plus the proof's verdict, so the image cannot exist without it.
@@ -3,15 +3,16 @@
3
3
  // with the staged tree at <root>, the stage manifest in <proof dir>, the repository's root build inputs COPYed
4
4
  // into <proof dir>/context and its `--copy` directories into <proof dir>/copied.
5
5
  //
6
- // The image's content tag is computed from the build context, not from the stage, so the stage must be proved
7
- // to be the build context's. `prove` refuses the image unless: the stage was made at <commit> (the BUILD_SHA
8
- // the image is built for, so a stale stage cannot ship); the root build inputs and `--copy` directories in the
9
- // build context are exactly the ones the stage hashed (which also proves the Dockerfile COPYs every one of them,
10
- // so the content tag covers them); the `--filter` words the Dockerfile carries (the ones the content hash reads
6
+ // The image's content tag is computed from the build context, not from the stage, so the stage must be BOUND
7
+ // to the build context. `prove` binds it; it does NOT detect a stage altered on the runner after `stage-tree`
8
+ // wrote it (the manifest and the layers come from the same job). It refuses the image unless: the stage claims
9
+ // <commit> (the BUILD_SHA the image is built for, so a stale stage cannot ship); the root build inputs and
10
+ // `--copy` directories in the build context are the ones the stage recorded hashes of (so the Dockerfile COPYs
11
+ // every one of them and the content tag covers them); the `--filter` words the Dockerfile carries (the ones the content hash reads
11
12
  // to narrow the lockfile) are the members' filters the stage installed; this tool is the version the stage
12
13
  // recorded; this process runs on the manifest's platform; the Node ABI is the stager's when the tree carries
13
14
  // addons; every package's addons load here; Prisma computes, HERE, the binaryTarget whose schema engine was
14
- // staged, and that engine has the staged sha256 and reports the pinned version; every member's dist/main.js
15
+ // staged, and that engine has the sha256 the manifest records and reports the pinned version; every member's dist/main.js
15
16
  // exists. It then writes <proof dir>/xema-composed-verified.json, which the final stage ships, so the image
16
17
  // cannot be built without the proof.
17
18
  //
@@ -93,3 +93,16 @@ test('the Prisma schema engine is checked against the staged sha256 and version'
93
93
  const stage = proofStage({ prisma: { enginesVersion: 'v1', binaryTarget: 'debian-openssl-3.0.x', sha256: 'f'.repeat(64) } });
94
94
  await assert.rejects(prove(stage), /the stage staged a schema engine, but the app carries no prisma CLI/);
95
95
  });
96
+
97
+ test('hashDirectory covers symlinks by path and target', () => {
98
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xema-hash-'));
99
+ fs.writeFileSync(path.join(dir, 'a.txt'), 'a');
100
+ const bare = hashDirectory(dir);
101
+ fs.symlinkSync('a.txt', path.join(dir, 'link'));
102
+ const linked = hashDirectory(dir);
103
+ assert.notEqual(linked, bare);
104
+ fs.rmSync(path.join(dir, 'link'));
105
+ fs.symlinkSync('elsewhere', path.join(dir, 'link'));
106
+ assert.notEqual(hashDirectory(dir), linked);
107
+ fs.rmSync(dir, { recursive: true, force: true });
108
+ });
@@ -5,7 +5,7 @@
5
5
  // package.json + declared `files` (the built outputs among them), the injected workspace
6
6
  // copies, the member index and host link, the migrate script
7
7
  // <stage>/layers/store/monorepo/node_modules/.pnpm/ the registry packages (stable until the lockfile changes)
8
- // <stage>/proof/xema-composed-image.json the manifest the in-image `prove` checks
8
+ // <stage>/proof/xema-composed-image.json the manifest the in-image `prove` binds to BUILD_SHA and the context
9
9
  //
10
10
  // The install is the runner's OFFLINE production install of the members' closures from the pnpm store the job's own
11
11
  // `pnpm install` filled (`composed-image-install.mjs`), into this staging tree rather than into the job's node_modules,
@@ -91,27 +91,37 @@ async function get(url) {
91
91
  return Buffer.from(await response.arrayBuffer());
92
92
  }
93
93
 
94
- /** The sha256 Prisma publishes next to `url` (`<url>.sha256`: `<hex> <name>`). */
95
- async function publishedSha256(url) {
96
- const [hex] = (await get(`${url}.sha256`)).toString('utf8').trim().split(/\s+/);
97
- if (!/^[a-f0-9]{64}$/.test(hex)) throw new Error(`${url}.sha256 is not a sha256`);
98
- return hex;
99
- }
94
+ /**
95
+ * The sha256 of the schema engine, by engines commit (the lockfile's @prisma/engines-version) and
96
+ * binaryTarget. COMMITTED here, reviewed with the tool: the `.sha256` Prisma publishes next to the
97
+ * binary is served by the same host as the binary, so it is no check on it. An engines commit or
98
+ * binaryTarget not in this table is refused; add its row (computed from a binary you trust) when the
99
+ * lockfile moves to a new engines commit.
100
+ */
101
+ export const SCHEMA_ENGINE_SHA256 = {
102
+ '3c6e192761c0362d496ed980de936e2f3cebcd3a': {
103
+ 'debian-openssl-3.0.x': '94eeddaaa41a25561feeefa56c054c95bc59cf0b219a86189afccf400adf8a8e',
104
+ 'linux-arm64-openssl-3.0.x': '5161008220716d697e81f4b7132a0a4b4be6e5be5675a94e937c24b279e637f8',
105
+ },
106
+ };
100
107
 
101
108
  /**
102
109
  * The schema engine at `enginesVersion` (the engines commit the lockfile pins, through the
103
110
  * deploy's @prisma/engines-version) for `binaryTarget`, fetched from Prisma for every stage and
104
- * written to `enginesDir`. Both the download and the engine must match the checksums Prisma
105
- * publishes. There is no cache: Prisma's own downloader keeps one across runs in the home
106
- * directory, honours mirror variables and retries, so it is not used.
111
+ * written to `enginesDir`. The unzipped engine must have the sha256 `checksums` commits for
112
+ * (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
114
+ * variables and retries, so it is not used.
107
115
  */
108
- export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVersion }) {
116
+ export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVersion, checksums = SCHEMA_ENGINE_SHA256 }) {
109
117
  if (!/^[a-f0-9]{40}$/.test(enginesVersion)) throw new Error(`@prisma/engines-version ${enginesVersion} is not an engines commit`);
118
+ const expected = checksums[enginesVersion]?.[binaryTarget];
119
+ if (expected === undefined) {
120
+ 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
+ }
110
122
  const url = `${PRISMA_ENGINES_URL}/${enginesVersion}/${binaryTarget}/schema-engine`;
111
- const [zippedSha256, engineSha256, zipped] = await Promise.all([publishedSha256(`${url}.gz`), publishedSha256(url), get(`${url}.gz`)]);
112
- if (sha256(zipped) !== zippedSha256) throw new Error(`${url}.gz has sha256 ${sha256(zipped)}, Prisma publishes ${zippedSha256}`);
113
- const engine = gunzipSync(zipped);
114
- if (sha256(engine) !== engineSha256) throw new Error(`${url} has sha256 ${sha256(engine)}, Prisma publishes ${engineSha256}`);
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}`);
115
125
  const file = join(enginesDir, `schema-engine-${binaryTarget}`);
116
126
  writeFileSync(file, engine);
117
127
  chmodSync(file, 0o755);
@@ -13,7 +13,7 @@ import { copyTool } from './composed-image-stage.mjs';
13
13
  import { parseStageArgs } from './service-image.mjs';
14
14
  import { parseProveArgs, prove } from './service-image-in-image.mjs';
15
15
  import { IMAGE_PLATFORMS, currentPlatform, nativeFormat, platformArch, scanNatives } from './service-image-platform.mjs';
16
- import { PRISMA_ENGINES_URL, deployArgs, downloadSchemaEngine, pnpmEntryName, stageService } from './service-image-stage.mjs';
16
+ import { PRISMA_ENGINES_URL, SCHEMA_ENGINE_SHA256, deployArgs, downloadSchemaEngine, pnpmEntryName, stageService } from './service-image-stage.mjs';
17
17
 
18
18
  const temps = [];
19
19
  after(() => {
@@ -256,11 +256,12 @@ test('stage fails when the downloader produces no engine', async () => {
256
256
  );
257
257
  });
258
258
 
259
- test('downloadSchemaEngine fetches the pinned engine, checks both published checksums and caches nothing', async (t) => {
259
+ test('downloadSchemaEngine fetches the pinned engine, checks it against the COMMITTED sha256 and trusts no published checksum', async (t) => {
260
260
  const engine = elf(183);
261
- const zipped = gzipSync(engine);
262
261
  const version = 'b'.repeat(40);
263
- const url = `${PRISMA_ENGINES_URL}/${version}/linux-arm64-openssl-3.0.x/schema-engine`;
262
+ const target = 'linux-arm64-openssl-3.0.x';
263
+ const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine`;
264
+ const checksums = { [version]: { [target]: sha256(engine) } };
264
265
  const serve = (files) => {
265
266
  const requested = [];
266
267
  t.mock.method(globalThis, 'fetch', async (asked) => {
@@ -270,24 +271,33 @@ test('downloadSchemaEngine fetches the pinned engine, checks both published chec
270
271
  });
271
272
  return requested;
272
273
  };
273
- const published = { [`${url}.gz`]: zipped, [`${url}.gz.sha256`]: `${sha256(zipped)} ./x/schema-engine.gz\n`, [`${url}.sha256`]: `${sha256(engine)} ./x/schema-engine\n` };
274
274
  const enginesDir = temp();
275
- const requested = serve(published);
276
- await downloadSchemaEngine({ enginesDir, binaryTarget: 'linux-arm64-openssl-3.0.x', enginesVersion: version });
277
- const file = path.join(enginesDir, 'schema-engine-linux-arm64-openssl-3.0.x');
275
+ const requested = serve({ [`${url}.gz`]: gzipSync(engine) });
276
+ await downloadSchemaEngine({ enginesDir, binaryTarget: target, enginesVersion: version, checksums });
277
+ const file = path.join(enginesDir, `schema-engine-${target}`);
278
278
  assert.deepEqual(fs.readFileSync(file), engine);
279
279
  assert.equal(fs.statSync(file).mode & 0o777, 0o755);
280
- assert.deepEqual(requested.sort(), Object.keys(published).sort());
280
+ assert.deepEqual(requested, [`${url}.gz`]);
281
281
  t.mock.restoreAll();
282
282
 
283
- serve({ ...published, [`${url}.sha256`]: `${'0'.repeat(64)} x\n` });
284
- await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'linux-arm64-openssl-3.0.x', enginesVersion: version }), /has sha256 .*, Prisma publishes 0{64}/);
283
+ // A binary whose published .sha256 agrees with it is still refused when it is not the committed one.
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
287
  t.mock.restoreAll();
286
- serve({ ...published, [`${url}.gz.sha256`]: undefined });
287
- await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'linux-arm64-openssl-3.0.x', enginesVersion: version }), /404 Not Found/);
288
+ const unfetched = serve({});
289
+ await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: 'c'.repeat(40), checksums }), /no committed sha256 .* engines commit c{40} for linux-arm64/);
290
+ await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'debian-openssl-3.0.x', enginesVersion: version, checksums }), /no committed sha256/);
291
+ assert.deepEqual(unfetched, []);
288
292
  await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'x', enginesVersion: '1.2.3' }), /is not an engines commit/);
289
293
  });
290
294
 
295
+ test('the committed engine table has the amd64 and arm64 rows for the engines commit xema-base pins', () => {
296
+ const rows = SCHEMA_ENGINE_SHA256['3c6e192761c0362d496ed980de936e2f3cebcd3a'];
297
+ assert.deepEqual(Object.keys(rows).sort(), ['debian-openssl-3.0.x', 'linux-arm64-openssl-3.0.x']);
298
+ for (const hex of Object.values(rows)) assert.match(hex, /^[a-f0-9]{64}$/);
299
+ });
300
+
291
301
  // ── prove ─────────────────────────────────────────────────────
292
302
 
293
303
  async function staged({ prisma = false } = {}) {
@@ -1,12 +1,13 @@
1
1
  // The checks every runner-staged image shares (`xema-service-image`, `xema-composed-image`): the proof stage
2
2
  // runs them on the TARGET platform, `--network=none`, against the stage manifest the runner wrote. The image's
3
- // content tag is computed from the build context, never from the stage, so each check ties the staged bytes
4
- // back to what the context (and so the tag) covers.
3
+ // content tag is computed from the build context, never from the stage, so each check binds the stage's
4
+ // manifest to what the context (and so the tag) covers. They do not detect a stage altered on the runner: the
5
+ // manifest and the layers are written by the same job.
5
6
  //
6
7
  // Loads only `node:` builtins and relative modules that do too: the in-image proofs import it from the
7
8
  // bind-mounted tool directory (Prisma's own modules are resolved from the staged tree).
8
9
  import { createHash } from 'node:crypto';
9
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
10
+ import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
10
11
  import { createRequire } from 'node:module';
11
12
  import { dirname, join } from 'node:path';
12
13
  import { fileURLToPath } from 'node:url';
@@ -53,6 +54,7 @@ export function hashDirectory(dir) {
53
54
  const rel = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
54
55
  if (entry.isDirectory()) walk(join(current, entry.name), rel);
55
56
  else if (entry.isFile()) hash.update(`${rel}\0${sha256File(join(current, entry.name))}\n`);
57
+ else if (entry.isSymbolicLink()) hash.update(`${rel}\0->${readlinkSync(join(current, entry.name))}\n`);
56
58
  }
57
59
  };
58
60
  walk(dir, '');