@xemahq/repo-build-tooling 0.9.0 → 0.9.2

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
@@ -95,6 +95,7 @@ build carries every composable platform service of the repository at one commit.
95
95
  `REGISTRY_TOKEN`, optional `TAG_ARGS_FILE`, `GITHUB_OUTPUT`. Outputs `image`, `content_tag`,
96
96
  `exists`, `entry`. The registry check is also exported as `./ghcr-manifest`
97
97
  (`ghcrManifestExists`, `acquireGhcrPullToken`).
98
+ - `load-image <image>` runs the member-load check inside the shipped image (`docker run`), as its own user and `NODE_ENV`; `load` runs in the builder and cannot prove that.
98
99
  - `smoke-refusal <image> [--member <biomeId>/<componentKey>]x2` boots the pushed image
99
100
  with two members on one port and fails unless the host refuses at the declaration, names
100
101
  the collision, and constructs no member. Needs only `docker`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM with zero dependencies 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)",
@@ -37,9 +37,9 @@
37
37
  "./scrub-swagger-plugin-paths": "./src/scrub-swagger-plugin-paths.mjs"
38
38
  },
39
39
  "scripts": {
40
- "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/ghcr-manifest.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs"
40
+ "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/ghcr-manifest.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"
41
41
  },
42
42
  "xemaRelease": {
43
- "sourceDigest": "sha256:89bea8bf6a2415cef4ea2dd5f199135d3ee71cba50d1c9eb93cc92f1b9eb1984"
43
+ "sourceDigest": "sha256:a1775245f7e0b751aeab7d53393103ddc32079c97043755a22b9fa261fa05134"
44
44
  }
45
45
  }
@@ -32,19 +32,33 @@ function required(env, name) {
32
32
  return value;
33
33
  }
34
34
 
35
+ /**
36
+ * The consumer's own `package.json` of the hash package, found through Node's own
37
+ * `node_modules` lookup paths for the consumer, NOT through `require.resolve`: the
38
+ * package's `exports` map need not expose `./package.json`. Only an absent file
39
+ * means "not installed"; any other read error is rethrown as it is.
40
+ */
41
+ function findHashManifest(root) {
42
+ for (const modulesDir of createRequire(join(root, 'package.json')).resolve.paths(HASH_PACKAGE) ?? []) {
43
+ const candidate = join(modulesDir, HASH_PACKAGE, 'package.json');
44
+ try {
45
+ readFileSync(candidate, 'utf8');
46
+ return candidate;
47
+ } catch (error) {
48
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+
35
54
  /** The consumer's own `xema-compute-image-tag` bin path; refuses when it is not installed there. */
36
55
  export function resolveHashBin(root) {
37
- let manifestPath;
38
- try {
39
- manifestPath = createRequire(join(root, 'package.json')).resolve(`${HASH_PACKAGE}/package.json`);
40
- } catch (error) {
41
- if (error?.code === 'MODULE_NOT_FOUND' || error?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
42
- throw new Error(
43
- `${HASH_PACKAGE} is not installed at ${root} — the content tag is computed only by its ${HASH_BIN} bin; ` +
44
- 'add it as a devDependency of this repository and install',
45
- );
46
- }
47
- throw error;
56
+ const manifestPath = findHashManifest(root);
57
+ if (manifestPath === undefined) {
58
+ throw new Error(
59
+ `${HASH_PACKAGE} is not installed at ${root} — the content tag is computed only by its ${HASH_BIN} bin; ` +
60
+ 'add it as a devDependency of this repository and install',
61
+ );
48
62
  }
49
63
  const bin = JSON.parse(readFileSync(manifestPath, 'utf8')).bin?.[HASH_BIN];
50
64
  if (typeof bin !== 'string') throw new Error(`${HASH_PACKAGE} at ${manifestPath} ships no "${HASH_BIN}" bin`);
@@ -26,7 +26,7 @@ function registry(manifestStatus) {
26
26
  }
27
27
 
28
28
  /** A consumer root whose own install carries a stand-in `xema-compute-image-tag` that echoes its arguments into a file. */
29
- function consumer({ withHash = true } = {}) {
29
+ function consumer({ withHash = true, exportsMap } = {}) {
30
30
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'xema-coordinate-'));
31
31
  fs.writeFileSync(path.join(root, 'package.json'), '{"name":"consumer"}');
32
32
  if (withHash) {
@@ -34,7 +34,7 @@ function consumer({ withHash = true } = {}) {
34
34
  fs.mkdirSync(path.join(pkg, 'dist/bin'), { recursive: true });
35
35
  fs.writeFileSync(
36
36
  path.join(pkg, 'package.json'),
37
- JSON.stringify({ name: '@xemahq/distribution-source-hash', version: '1.0.0', bin: { 'xema-compute-image-tag': 'dist/bin/compute-image-tag.js' } }),
37
+ JSON.stringify({ name: '@xemahq/distribution-source-hash', version: '1.0.0', ...(exportsMap ? { exports: exportsMap } : {}), bin: { 'xema-compute-image-tag': 'dist/bin/compute-image-tag.js' } }),
38
38
  );
39
39
  fs.writeFileSync(
40
40
  path.join(pkg, 'dist/bin/compute-image-tag.js'),
@@ -106,3 +106,18 @@ test('emitCoordinate prints every output and appends them to GITHUB_OUTPUT', ()
106
106
  assert.equal(fs.readFileSync(file, 'utf8'), `image=i\ncontent_tag=${HASH}\nexists=false\nentry={}\n`);
107
107
  assert.match(lines.at(-1), /is missing — build/);
108
108
  });
109
+
110
+ test('a package whose exports map lacks ./package.json still resolves, as published 0.2.2 does', () => {
111
+ const root = consumer({ exportsMap: { '.': './dist/index.js' } });
112
+ assert.equal(
113
+ resolveHashBin(root),
114
+ path.join(root, 'node_modules/@xemahq/distribution-source-hash', 'dist/bin/compute-image-tag.js'),
115
+ );
116
+ });
117
+
118
+ test('a read error other than absence is rethrown with its real message, not mapped to not installed', () => {
119
+ const root = consumer({ withHash: false });
120
+ // package.json is a directory: EISDIR, which is not "absent"
121
+ fs.mkdirSync(path.join(root, 'node_modules/@xemahq/distribution-source-hash/package.json'), { recursive: true });
122
+ assert.throws(() => resolveHashBin(root), (error) => error.code === 'EISDIR' && !/not installed/.test(error.message));
123
+ });
@@ -13,11 +13,12 @@
13
13
  // builder as the `node` user with NODE_ENV=production, after the production
14
14
  // install, which is the user and environment the image runs with.
15
15
 
16
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
17
- import { join, posix } from 'node:path';
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { dirname, join, posix } from 'node:path';
18
18
 
19
19
  import { COMPOSED_IMAGE_DIR } from './composed-image-paths.mjs';
20
20
 
21
+ const TOKEN_REFERENCE = /_authToken\b|\$\{(?:NPM_TOKEN|GH_PACKAGES_TOKEN)\}/;
21
22
  const VERSION = /^\d+\.\d+\.\d+$/;
22
23
  const DIRECTORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*\/?$/;
23
24
  const CACHE_ID = /^[a-z0-9][a-z0-9-]*$/;
@@ -39,8 +40,14 @@ function validate({ toolingVersion, copy, cacheId, buildSource }) {
39
40
  if (!REPOSITORY.test(buildSource ?? '')) throw new Error(`--build-source must name a repository, got "${buildSource}"`);
40
41
  }
41
42
 
42
- /** The Dockerfile text. `imageDir` is where the repository keeps the image (and so where this is written). */
43
- export function renderComposedDockerfile({ toolingVersion, copy, cacheId, buildSource, imageDir = COMPOSED_IMAGE_DIR }) {
43
+ /** Whether the repository's `.npmrc` authenticates with a token (`_authToken`, `${NPM_TOKEN}`); a public-registry `.npmrc` needs none. */
44
+ export function npmrcUsesToken(npmrcText) {
45
+ return TOKEN_REFERENCE.test(npmrcText);
46
+ }
47
+
48
+ /** The Dockerfile text. `usesToken` mounts the `npm_token` build secret; derive it with `npmrcUsesToken`. `imageDir` is where the repository keeps the image (and so where this is written). */
49
+ export function renderComposedDockerfile({ toolingVersion, copy, cacheId, buildSource, usesToken, imageDir = COMPOSED_IMAGE_DIR }) {
50
+ if (typeof usesToken !== 'boolean') throw new Error('usesToken must be a boolean derived from .npmrc');
44
51
  validate({ toolingVersion, copy, cacheId, buildSource });
45
52
  const dirFlag = imageDir === COMPOSED_IMAGE_DIR ? '' : ` --image-dir ${imageDir}`;
46
53
  const tool = (command) => `xema-composed-image ${command}${dirFlag}`;
@@ -48,6 +55,11 @@ export function renderComposedDockerfile({ toolingVersion, copy, cacheId, buildS
48
55
  .map((d) => `COPY ${d} ./${d}`)
49
56
  .join('\n');
50
57
  const store = `--mount=type=cache,id=pnpm-store-${cacheId},target=/root/.pnpm-store`;
58
+ const mount = usesToken ? ' --mount=type=secret,id=npm_token,required=true \\\\\n' : '';
59
+ const tokenEnv = usesToken ? 'NPM_TOKEN="$(cat /run/secrets/npm_token)" GH_PACKAGES_TOKEN="$NPM_TOKEN" \\\\\n ' : '';
60
+ const tokenExport = usesToken
61
+ ? 'export NPM_TOKEN="$(cat /run/secrets/npm_token)" && export GH_PACKAGES_TOKEN="$NPM_TOKEN" && \\\\\n '
62
+ : '';
51
63
  const metadata = `--mount=type=cache,id=pnpm-metadata-${cacheId},target=/root/.cache/pnpm`;
52
64
  return `# @generated by \`xema-composed-image dockerfile --write\` (@xemahq/repo-build-tooling@${toolingVersion}) — DO NOT EDIT.
53
65
  # Re-render with the same options; \`--check\` refuses a stale file.
@@ -90,13 +102,9 @@ RUN ${tool('filters')} > /tmp/member-filters && \\
90
102
  test -s /tmp/member-filters
91
103
 
92
104
  RUN ${store} \\
93
- --mount=type=secret,id=npm_token,required=true \\
94
- NPM_TOKEN="$(cat /run/secrets/npm_token)" GH_PACKAGES_TOKEN="$NPM_TOKEN" \\
95
- pnpm fetch $(cat /tmp/member-filters) --frozen-lockfile
105
+ ${mount} ${tokenEnv}pnpm fetch $(cat /tmp/member-filters) --frozen-lockfile
96
106
  RUN ${store} \\
97
- --mount=type=secret,id=npm_token,required=true \\
98
- NPM_TOKEN="$(cat /run/secrets/npm_token)" GH_PACKAGES_TOKEN="$NPM_TOKEN" \\
99
- pnpm install --prefer-offline $(cat /tmp/member-filters) --frozen-lockfile
107
+ ${mount} ${tokenEnv}pnpm install --prefer-offline $(cat /tmp/member-filters) --frozen-lockfile
100
108
 
101
109
  # Every member that owns a Prisma schema generates its OWN client into its own
102
110
  # \`node_modules/.prisma/<service>\` (the schema's \`output\`), before it builds.
@@ -106,9 +114,7 @@ RUN for schema in $(${tool('dirs')}); do \\
106
114
  fi; \\
107
115
  done
108
116
 
109
- RUN --mount=type=secret,id=npm_token,required=true \\
110
- NPM_TOKEN="$(cat /run/secrets/npm_token)" GH_PACKAGES_TOKEN="$NPM_TOKEN" \\
111
- pnpm $(cat /tmp/member-filters) run build
117
+ RUN ${mount ? `${mount.trimStart()} ` : ''}${tokenEnv}pnpm $(cat /tmp/member-filters) run build
112
118
 
113
119
  # Production install, with the members' WORKSPACE packages INJECTED — the same
114
120
  # resolution the per-service image gets from \`pnpm deploy\` (legacy deploy
@@ -134,9 +140,7 @@ RUN --mount=type=secret,id=npm_token,required=true \\
134
140
  # install built, and the members' \`postinstall\` is the Prisma loop below.
135
141
  RUN ${store} \\
136
142
  ${metadata} \\
137
- --mount=type=secret,id=npm_token,required=true \\
138
- export NPM_TOKEN="$(cat /run/secrets/npm_token)" && export GH_PACKAGES_TOKEN="$NPM_TOKEN" && \\
139
- INJECT="--config.inject-workspace-packages=true --config.dedupe-injected-deps=false" && \\
143
+ ${mount} ${tokenExport}INJECT="--config.inject-workspace-packages=true --config.dedupe-injected-deps=false" && \\
140
144
  cp pnpm-lock.yaml /tmp/pnpm-lock.committed.yaml && \\
141
145
  pnpm install --lockfile-only --prefer-offline \${INJECT} $(cat /tmp/member-filters) && \\
142
146
  ${tool('pins')} /tmp/pnpm-lock.committed.yaml && \\
@@ -170,10 +174,16 @@ CMD ["node", "/monorepo/xema-composed-process"]
170
174
  /** `--write` writes the rendered Dockerfile; `--check` throws when the committed one differs. */
171
175
  export function syncComposedDockerfile({ mode, root = process.cwd(), imageDir = COMPOSED_IMAGE_DIR, ...options }) {
172
176
  if (mode !== 'write' && mode !== 'check') throw new Error(`mode must be write or check, got "${mode}"`);
173
- const rendered = renderComposedDockerfile({ ...options, imageDir });
177
+ validate(options);
178
+ const npmrc = join(root, '.npmrc');
179
+ if (!existsSync(npmrc)) throw new Error(`${npmrc} is absent — the Dockerfile copies it and its token use decides the build secret`);
180
+ const rendered = renderComposedDockerfile({ ...options, usesToken: npmrcUsesToken(readFileSync(npmrc, 'utf8')), imageDir });
174
181
  const relativeTarget = posix.join(imageDir, 'Dockerfile');
175
182
  const target = join(root, relativeTarget);
176
- if (mode === 'write') writeFileSync(target, rendered);
183
+ if (mode === 'write') {
184
+ mkdirSync(dirname(target), { recursive: true });
185
+ writeFileSync(target, rendered);
186
+ }
177
187
  else if ((existsSync(target) ? readFileSync(target, 'utf8') : '') !== rendered) {
178
188
  throw new Error(`${relativeTarget} is stale. Run: xema-composed-image dockerfile --write (with the same options)`);
179
189
  }
@@ -4,13 +4,14 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
- import { renderComposedDockerfile, syncComposedDockerfile } from './composed-image-dockerfile.mjs';
7
+ import { npmrcUsesToken, renderComposedDockerfile, syncComposedDockerfile } from './composed-image-dockerfile.mjs';
8
8
 
9
9
  const OPTIONS = {
10
10
  toolingVersion: '0.9.0',
11
11
  copy: ['packages', 'tooling/dev/', 'biomes'],
12
12
  cacheId: 'alpha-composed-process',
13
13
  buildSource: 'alpha',
14
+ usesToken: true,
14
15
  };
15
16
 
16
17
  test('the template carries every parameter and no per-repository literal', () => {
@@ -51,9 +52,35 @@ test('the tool version, the directories and the ids are validated', () => {
51
52
  test('--write renders and --check refuses a stale or missing file', () => {
52
53
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'xema-dockerfile-'));
53
54
  fs.mkdirSync(path.join(root, 'infra/composed-process'), { recursive: true });
55
+ fs.writeFileSync(path.join(root, '.npmrc'), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n');
54
56
  assert.throws(() => syncComposedDockerfile({ mode: 'check', root, ...OPTIONS }), /is stale/);
55
57
  assert.equal(syncComposedDockerfile({ mode: 'write', root, ...OPTIONS }), 'infra/composed-process/Dockerfile');
56
58
  syncComposedDockerfile({ mode: 'check', root, ...OPTIONS });
57
59
  assert.throws(() => syncComposedDockerfile({ mode: 'check', root, ...OPTIONS, toolingVersion: '0.9.1' }), /is stale/);
58
60
  assert.throws(() => syncComposedDockerfile({ mode: 'sideways', root, ...OPTIONS }), /write or check/);
59
61
  });
62
+
63
+ test('the npm_token secret is mounted only when .npmrc references a token', () => {
64
+ const withToken = renderComposedDockerfile(OPTIONS);
65
+ assert.equal(withToken.match(/type=secret,id=npm_token,required=true/g).length, 4);
66
+ assert.equal(withToken.match(/NPM_TOKEN="\$\(cat \/run\/secrets\/npm_token\)"/g).length, 4);
67
+ const without = renderComposedDockerfile({ ...OPTIONS, usesToken: false });
68
+ assert.doesNotMatch(without, /npm_token|NPM_TOKEN|GH_PACKAGES_TOKEN|type=secret/);
69
+ assert.match(without, /RUN --mount=type=cache,id=pnpm-store-alpha-composed-process,target=\/root\/.pnpm-store \\\n {2}pnpm fetch/);
70
+ assert.match(without, /RUN pnpm \$\(cat \/tmp\/member-filters\) run build/);
71
+ assert.throws(() => renderComposedDockerfile({ ...OPTIONS, usesToken: undefined }), /usesToken/);
72
+ });
73
+
74
+ test('the token use is derived from .npmrc, deterministically', () => {
75
+ assert.equal(npmrcUsesToken('//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n'), true);
76
+ assert.equal(npmrcUsesToken('@x:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=abc'), true);
77
+ assert.equal(npmrcUsesToken('registry=https://registry.npmjs.org/\nauto-install-peers=true\n'), false);
78
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'xema-dockerfile-npmrc-'));
79
+ assert.throws(() => syncComposedDockerfile({ mode: 'write', root, ...OPTIONS }), /\.npmrc is absent/);
80
+ fs.writeFileSync(path.join(root, '.npmrc'), 'registry=https://registry.npmjs.org/\n');
81
+ syncComposedDockerfile({ mode: 'write', root, ...OPTIONS });
82
+ const text = fs.readFileSync(path.join(root, 'infra/composed-process/Dockerfile'), 'utf8');
83
+ assert.doesNotMatch(text, /npm_token/);
84
+ fs.writeFileSync(path.join(root, '.npmrc'), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n');
85
+ assert.throws(() => syncComposedDockerfile({ mode: 'check', root, ...OPTIONS }), /is stale/);
86
+ });
@@ -35,6 +35,9 @@
35
35
  // and the `xema-composed-process` link the image runs.
36
36
  // load evaluate every member's entry exactly as the host does
37
37
  // (`readServiceEntry`) and refuse the image if any graph does not load.
38
+ // load-image <image>
39
+ // the same check against the SHIPPED image (`docker run`), as its own
40
+ // user and NODE_ENV: `load` runs in the builder, which is not what ships.
38
41
  // coordinate --image-name <name>
39
42
  // the image's content tag (computed ONLY by the repository's own
40
43
  // `xema-compute-image-tag`) and whether GHCR already serves it
@@ -67,7 +70,7 @@
67
70
  // ═══════════════════════════════════════════════════════════════════════════
68
71
 
69
72
  import { execFileSync } from 'node:child_process';
70
- import { existsSync, readFileSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs';
73
+ import { existsSync, mkdirSync, readFileSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs';
71
74
  import { createRequire } from 'node:module';
72
75
  import { dirname, join, posix, relative, resolve } from 'node:path';
73
76
  import { pathToFileURL } from 'node:url';
@@ -75,6 +78,7 @@ import { pathToFileURL } from 'node:url';
75
78
  import { COMPOSED_IMAGE_DIR } from './composed-image-paths.mjs';
76
79
  import { composedCoordinate, emitCoordinate } from './composed-image-coordinate.mjs';
77
80
  import { syncComposedDockerfile } from './composed-image-dockerfile.mjs';
81
+ import { runImageLoad } from './load-image.mjs';
78
82
  import { runRefusalSmoke } from './smoke-refusal.mjs';
79
83
 
80
84
  export { COMPOSED_IMAGE_DIR };
@@ -259,8 +263,10 @@ export function syncImageBuildInputs({ mode, root = process.cwd(), imageDir = CO
259
263
  const rendered = renderImageBuildInputs(derived);
260
264
  const relativeTarget = posix.join(imageDir, IMAGE_BUILD_INPUTS_FILE);
261
265
  const target = join(root, relativeTarget);
262
- if (mode === 'write') writeFileSync(target, rendered);
263
- else if ((existsSync(target) ? readFileSync(target, 'utf8') : '') !== rendered) {
266
+ if (mode === 'write') {
267
+ mkdirSync(dirname(target), { recursive: true });
268
+ writeFileSync(target, rendered);
269
+ } else if ((existsSync(target) ? readFileSync(target, 'utf8') : '') !== rendered) {
264
270
  throw new Error(
265
271
  `${relativeTarget} is stale — the composed image would not carry exactly the composable services. ` +
266
272
  'Run: xema-composed-image members --write (with the same options)',
@@ -438,6 +444,7 @@ const USAGE =
438
444
  ' members --write|--check [--manifests <glob>]… [--canonical-dockerfile <path>] [--cross-repo-manifest <path>]\n' +
439
445
  ' filters | dirs | pins <committed-lockfile> | index <commit> --build-source <repo> | load\n' +
440
446
  ' coordinate --image-name <name> (env: SHA REGISTRY REGISTRY_ORG REGISTRY_USER REGISTRY_TOKEN [TAG_ARGS_FILE])\n' +
447
+ ' load-image <image>\n' +
441
448
  ' smoke-refusal <image> [--member <biomeId>/<componentKey>]x2\n' +
442
449
  ' dockerfile --write|--check --tooling-version <x.y.z> --copy <dir>… --cache-id <id> --build-source <repo>';
443
450
 
@@ -514,6 +521,9 @@ export function main(argv, { log = console.log, write = (text) => process.stdout
514
521
  return composedCoordinate({ root, imageDir, imageName: options['image-name'] }).then((outputs) =>
515
522
  emitCoordinate(outputs, { log }),
516
523
  );
524
+ case 'load-image':
525
+ runImageLoad({ image: argument, log });
526
+ return;
517
527
  case 'smoke-refusal':
518
528
  runRefusalSmoke({ image: argument, members: options.member, log, write });
519
529
  return;
@@ -247,3 +247,12 @@ test('the new commands parse their options; coordinate is asynchronous and refus
247
247
  assert.throws(() => main(['dockerfile', '--write'], { log() {} }), /exact x\.y\.z/);
248
248
  assert.throws(() => main(['smoke-refusal'], { log() {} }), /usage/);
249
249
  });
250
+
251
+ test('members --write on a first run creates the missing image directory', () => {
252
+ const root = fixture();
253
+ assert.equal(fs.existsSync(path.join(root, 'infra/composed-process')), false);
254
+ main(['members', '--root', root, '--manifests', 'products/**/xema-biome.json', '--write'], { log() {} });
255
+ assert.deepEqual(JSON.parse(fs.readFileSync(path.join(root, 'infra/composed-process/image-build-inputs.json'), 'utf8')), {
256
+ workspacePackages: ['products/pai/core/pai-core/api/pai-core-api'],
257
+ });
258
+ });
@@ -0,0 +1,27 @@
1
+ // `xema-composed-image load-image <image>`: proves every composed member module
2
+ // loads in the SHIPPED image, not in the builder stage that produced it. It pipes
3
+ // `load-members-in-image.mjs` into the image's own node, so the check sees the
4
+ // final stage's filesystem, `node_modules`, prune, user and NODE_ENV. Needs only
5
+ // `docker` and the image reference: no git history, no install.
6
+
7
+ import { spawnSync } from 'node:child_process';
8
+ import { readFileSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const LOAD_SCRIPT = fileURLToPath(new URL('./load-members-in-image.mjs', import.meta.url));
12
+
13
+ /** Returns the number of member module graphs that loaded; throws naming each that did not. */
14
+ export function runImageLoad({ image, spawn = spawnSync, log = console.log, script = () => readFileSync(LOAD_SCRIPT, 'utf8') }) {
15
+ if (!image) throw new Error('usage: xema-composed-image load-image <image>');
16
+ const result = spawn('docker', ['run', '--rm', '-i', '--entrypoint', 'node', image, '--input-type=module', '-'], {
17
+ encoding: 'utf8',
18
+ maxBuffer: 64 * 1024 * 1024,
19
+ input: script(),
20
+ });
21
+ if (result.error) throw result.error;
22
+ if (result.status !== 0) throw new Error(`the shipped image ${image} does not load:\n${result.stderr ?? ''}`);
23
+ const count = Number.parseInt(String(result.stdout).trim(), 10);
24
+ if (!Number.isInteger(count) || count < 1) throw new Error(`the image load check reported no members: ${result.stdout}`);
25
+ log(`composed image ${image}: all ${count} member module graph(s) load in the shipped image`);
26
+ return count;
27
+ }
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { runImageLoad } from './load-image.mjs';
5
+
6
+ test('the check runs inside the image through its own node and passes on a clean load', () => {
7
+ const calls = [];
8
+ const lines = [];
9
+ const spawn = (command, args, options) => {
10
+ calls.push({ command, args, options });
11
+ return { status: 0, stdout: '3\n', stderr: '' };
12
+ };
13
+ assert.equal(runImageLoad({ image: 'img@sha256:x', spawn, log: (l) => lines.push(l), script: () => 'SCRIPT' }), 3);
14
+ assert.equal(calls[0].command, 'docker');
15
+ assert.deepEqual(calls[0].args, ['run', '--rm', '-i', '--entrypoint', 'node', 'img@sha256:x', '--input-type=module', '-']);
16
+ assert.equal(calls[0].options.input, 'SCRIPT');
17
+ assert.match(lines[0], /all 3 member module graph\(s\) load in the shipped image/);
18
+ });
19
+
20
+ test('a member that does not load fails the check with the image named', () => {
21
+ const spawn = () => ({ status: 1, stdout: '', stderr: '1 of 3 member(s) do not load:\n - a/b: boom\n' });
22
+ assert.throws(() => runImageLoad({ image: 'img', spawn, script: () => '' }), /shipped image img does not load:[\s\S]*a\/b: boom/);
23
+ });
24
+
25
+ test('an image that reports no members is refused', () => {
26
+ const spawn = () => ({ status: 0, stdout: '', stderr: '' });
27
+ assert.throws(() => runImageLoad({ image: 'img', spawn, script: () => '' }), /reported no members/);
28
+ });
29
+
30
+ test('an image reference is required', () => {
31
+ assert.throws(() => runImageLoad({ spawn: () => ({}) }), /usage/);
32
+ });
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ // INVOKER: load-image.mjs, INSIDE the composed-process image (piped to the image's
3
+ // own node on stdin). Self-contained on purpose: it resolves nothing from the
4
+ // repository's checkout, only from the image's own filesystem, as the image's own
5
+ // user and NODE_ENV.
6
+ //
7
+ // Evaluates every member's entry exactly as the host does (`readServiceEntry`),
8
+ // from the member's own directory, and exits non-zero naming each that does not load.
9
+ //
10
+ // Usage: docker run -i --entrypoint node <image> --input-type=module - < load-members-in-image.mjs
11
+ import { readFileSync } from 'node:fs';
12
+ import { createRequire } from 'node:module';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ const indexPath = process.env.XEMA_COMPOSED_MEMBER_INDEX;
16
+ if (!indexPath) throw new Error('XEMA_COMPOSED_MEMBER_INDEX is not set — not a composed-process image');
17
+ const root = dirname(indexPath);
18
+ const members = Object.entries(JSON.parse(readFileSync(indexPath, 'utf8')).members);
19
+ if (members.length === 0) throw new Error('the image member index carries no members');
20
+
21
+ const problems = [];
22
+ for (const [key, { entry }] of members) {
23
+ try {
24
+ const memberDirectory = dirname(dirname(join(root, entry)));
25
+ createRequire(join(memberDirectory, 'package.json'))('@xemahq/xema-service-nest').readServiceEntry(join(root, entry), {});
26
+ } catch (error) {
27
+ problems.push(`${key}: ${error?.message ?? String(error)}`);
28
+ }
29
+ }
30
+ if (problems.length > 0) {
31
+ process.stderr.write(`${problems.length} of ${members.length} member(s) do not load:\n${problems.map((p) => ` - ${p}`).join('\n')}\n`);
32
+ process.exit(1);
33
+ }
34
+ process.stdout.write(`${members.length}\n`);