@xemahq/repo-build-tooling 0.13.0 → 0.14.0

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
@@ -33,7 +33,7 @@ pnpm exec xema-scrub-swagger-paths dist
33
33
  | `xema-check-no-vendored-deps` | Fails on a `workspace:` range for a package the repository does not own |
34
34
  | `xema-check-workspace-ranges` | Fails on a `workspace:` range the local package does not satisfy |
35
35
 
36
- `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 `--tooling-version`, `--copy`, `--cache-id` and `--build-source`. `dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image assembled from `stage <dir> [--output <rel>]… [--also <pkg>]…` output (requires `--tooling-version`, `--cache-id`, `--build-source`; `--copy` and `--stage-dir` optional); `load --each` loads every member in its own process. `stage` and `filters` accept `--also <workspace package name>` (repeatable): the package's closure is built and staged (built outputs only, no Prisma checks) alongside the members; an unknown name is refused. Run `xema-composed-image` with no arguments for the full usage.
36
+ `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). `dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image assembled from `stage <dir> [--output <rel>]… [--also <pkg>]…` output (requires `--cache-id`, `--build-source`; `--copy` and `--stage-dir` optional); `load --each` loads every member in its own process. `stage` and `filters` accept `--also <workspace package name>` (repeatable): the package's closure is built and staged (built outputs only, no Prisma checks) alongside the members; an unknown name is refused. Run `xema-composed-image` with no arguments for the full usage.
37
37
 
38
38
  Every step refuses rather than skips. The package has no dependencies.
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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)",
@@ -44,6 +44,6 @@
44
44
  "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-stage.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-migrate.test.mjs"
45
45
  },
46
46
  "xemaRelease": {
47
- "sourceDigest": "sha256:a3994566dfeac8ede246a07315647fa7f10828b510cf5835a0640a8a97650f5c"
47
+ "sourceDigest": "sha256:003d770229d6ab378573068cfaf5b02e89b6bbd632dcfce3ccb17125d94322e1"
48
48
  }
49
49
  }
@@ -5,7 +5,7 @@
5
5
  // build context carries (`--copy`, repeatable), its pnpm cache id (`--cache-id`),
6
6
  // the repository its member subjects name in an edition's image lock
7
7
  // (`--build-source`) and the EXACT version of this tool the builder installs
8
- // (`--tooling-version`). The tool is pinned exactly so the image directory, which
8
+ // (its own package version, never a flag). The tool is pinned exactly so the image directory, which
9
9
  // the content tag hashes, changes whenever the tool does.
10
10
  //
11
11
  // NOTHING OF THE TOOL SHIPS: it is installed globally in the builder stage only,
@@ -17,19 +17,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
17
  import { dirname, join, posix } from 'node:path';
18
18
 
19
19
  import { COMPOSED_IMAGE_DIR } from './composed-image-paths.mjs';
20
+ import { toolVersion } from './composed-image-stage.mjs';
20
21
 
21
22
  const TOKEN_REFERENCE = /_authToken\b|\$\{(?:NPM_TOKEN|GH_PACKAGES_TOKEN)\}/;
22
- const VERSION = /^\d+\.\d+\.\d+$/;
23
23
  const DIRECTORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*\/?$/;
24
24
  const CACHE_ID = /^[a-z0-9][a-z0-9-]*$/;
25
25
  const REPOSITORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
26
26
  export const NODE_BASE =
27
27
  'mirror.gcr.io/library/node:22-bookworm-slim@sha256:d649c27dae7ba0137b3cef5dd75baa422c08dc3d9e3fc0c23dfb172dc3cc6436';
28
28
 
29
- function validate({ toolingVersion, copy, cacheId, buildSource }) {
30
- if (!VERSION.test(toolingVersion ?? '')) {
31
- throw new Error(`--tooling-version must be an exact x.y.z version, got "${toolingVersion}"`);
32
- }
29
+ function validate({ copy, cacheId, buildSource }) {
33
30
  if (!Array.isArray(copy) || copy.length === 0) throw new Error('--copy needs at least one directory');
34
31
  for (const directory of copy) {
35
32
  if (!DIRECTORY.test(directory) || directory.startsWith('/') || directory.split('/').includes('..')) {
@@ -46,9 +43,10 @@ export function npmrcUsesToken(npmrcText) {
46
43
  }
47
44
 
48
45
  /** 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 }) {
46
+ export function renderComposedDockerfile({ copy, cacheId, buildSource, usesToken, imageDir = COMPOSED_IMAGE_DIR }) {
50
47
  if (typeof usesToken !== 'boolean') throw new Error('usesToken must be a boolean derived from .npmrc');
51
- validate({ toolingVersion, copy, cacheId, buildSource });
48
+ validate({ copy, cacheId, buildSource });
49
+ const toolingVersion = toolVersion();
52
50
  const dirFlag = imageDir === COMPOSED_IMAGE_DIR ? '' : ` --image-dir ${imageDir}`;
53
51
  const tool = (command) => `xema-composed-image ${command}${dirFlag}`;
54
52
  const copies = [...new Set([...copy.map((d) => posix.normalize(`${d}/`)), posix.normalize(`${imageDir}/`)])]
@@ -4,10 +4,11 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
+ import { toolVersion } from './composed-image-stage.mjs';
8
+
7
9
  import { npmrcUsesToken, renderComposedDockerfile, syncComposedDockerfile } from './composed-image-dockerfile.mjs';
8
10
 
9
11
  const OPTIONS = {
10
- toolingVersion: '0.9.0',
11
12
  copy: ['packages', 'tooling/dev/', 'biomes'],
12
13
  cacheId: 'alpha-composed-process',
13
14
  buildSource: 'alpha',
@@ -16,7 +17,7 @@ const OPTIONS = {
16
17
 
17
18
  test('the template carries every parameter and no per-repository literal', () => {
18
19
  const text = renderComposedDockerfile(OPTIONS);
19
- assert.match(text, /npm install --global --ignore-scripts @xemahq\/repo-build-tooling@0\.9\.0\n/);
20
+ assert.ok(text.includes(`npm install --global --ignore-scripts @xemahq/repo-build-tooling@${toolVersion()}\n`));
20
21
  assert.match(text, /^COPY packages\/ \.\/packages\/\nCOPY tooling\/dev\/ \.\/tooling\/dev\/\nCOPY biomes\/ \.\/biomes\/\nCOPY infra\/composed-process\/ \.\/infra\/composed-process\/$/m);
21
22
  assert.match(text, /id=pnpm-store-alpha-composed-process,/);
22
23
  assert.match(text, /id=pnpm-metadata-alpha-composed-process,/);
@@ -39,9 +40,7 @@ test('nothing of the tool reaches the final stage; load runs in the builder as n
39
40
  assert.match(final, /CMD \["node", "\/monorepo\/xema-composed-process"\]/);
40
41
  });
41
42
 
42
- test('the tool version, the directories and the ids are validated', () => {
43
- assert.throws(() => renderComposedDockerfile({ ...OPTIONS, toolingVersion: '^0.9.0' }), /exact x\.y\.z/);
44
- assert.throws(() => renderComposedDockerfile({ ...OPTIONS, toolingVersion: undefined }), /exact x\.y\.z/);
43
+ test('the directories and the ids are validated', () => {
45
44
  assert.throws(() => renderComposedDockerfile({ ...OPTIONS, copy: [] }), /at least one/);
46
45
  for (const bad of ['../x', '/abs', 'a b', 'a;b']) assert.throws(() => renderComposedDockerfile({ ...OPTIONS, copy: [bad] }), /repository-relative/);
47
46
  assert.throws(() => renderComposedDockerfile({ ...OPTIONS, cacheId: 'Bad_Id' }), /--cache-id/);
@@ -56,7 +55,7 @@ test('--write renders and --check refuses a stale or missing file', () => {
56
55
  assert.throws(() => syncComposedDockerfile({ mode: 'check', root, ...OPTIONS }), /is stale/);
57
56
  assert.equal(syncComposedDockerfile({ mode: 'write', root, ...OPTIONS }), 'infra/composed-process/Dockerfile');
58
57
  syncComposedDockerfile({ mode: 'check', root, ...OPTIONS });
59
- assert.throws(() => syncComposedDockerfile({ mode: 'check', root, ...OPTIONS, toolingVersion: '0.9.1' }), /is stale/);
58
+ assert.throws(() => syncComposedDockerfile({ mode: 'check', root, ...OPTIONS, cacheId: 'beta-composed-process' }), /is stale/);
60
59
  assert.throws(() => syncComposedDockerfile({ mode: 'sideways', root, ...OPTIONS }), /write or check/);
61
60
  });
62
61
 
@@ -7,17 +7,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { dirname, join, posix } from 'node:path';
8
8
 
9
9
  import { COMPOSED_IMAGE_DIR } from './composed-image-paths.mjs';
10
+ import { toolVersion } from './composed-image-stage.mjs';
10
11
  import { NODE_BASE, npmrcUsesToken } from './composed-image-dockerfile.mjs';
11
12
  import { memberClosure, memberFilters, memberManifests } from './composed-image-members.mjs';
12
13
 
13
14
  export const DEFAULT_STAGE_DIR = '.xema-stage';
14
- const VERSION = /^\d+\.\d+\.\d+$/;
15
15
  const DIRECTORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*\/?$/;
16
16
  const CACHE_ID = /^[a-z0-9][a-z0-9-]*$/;
17
17
  const REPOSITORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
18
18
 
19
- function validate({ toolingVersion, copy, cacheId, buildSource, stageDir }) {
20
- if (!VERSION.test(toolingVersion ?? '')) throw new Error(`--tooling-version must be an exact x.y.z version, got "${toolingVersion}"`);
19
+ function validate({ copy, cacheId, buildSource, stageDir }) {
21
20
  for (const directory of [...(copy ?? []), stageDir]) {
22
21
  if (!DIRECTORY.test(directory) || directory.startsWith('/') || directory.split('/').includes('..')) {
23
22
  throw new Error(`"${directory}" is not a repository-relative directory`);
@@ -69,7 +68,6 @@ const parts = (sources) => sources.map((source) => ` ${source} \\`).join('\n'
69
68
  export function renderPrebuiltDockerfile({
70
69
  root = process.cwd(),
71
70
  imageDir = COMPOSED_IMAGE_DIR,
72
- toolingVersion,
73
71
  copy = [],
74
72
  cacheId,
75
73
  buildSource,
@@ -78,7 +76,8 @@ export function renderPrebuiltDockerfile({
78
76
  exec,
79
77
  }) {
80
78
  if (typeof usesToken !== 'boolean') throw new Error('usesToken must be a boolean derived from .npmrc');
81
- validate({ toolingVersion, copy, cacheId, buildSource, stageDir });
79
+ validate({ copy, cacheId, buildSource, stageDir });
80
+ const toolingVersion = toolVersion();
82
81
  const workspaceFile = join(root, 'pnpm-workspace.yaml');
83
82
  const built = existsSync(workspaceFile) ? onlyBuiltDependencies(readFileSync(workspaceFile, 'utf8')) : [];
84
83
  if (built.length === 0) {
@@ -8,7 +8,7 @@ import { parseArguments } from './composed-image.mjs';
8
8
  import { CLOSURE, fakePnpm, workspace } from './composed-image-fixture.test.mjs';
9
9
 
10
10
  function options(root, extra = {}) {
11
- return { root, toolingVersion: '0.11.0', copy: ['tooling/dev'], cacheId: 'alpha-composed-process', buildSource: 'alpha', usesToken: true, exec: fakePnpm(root), ...extra };
11
+ return { root, copy: ['tooling/dev'], cacheId: 'alpha-composed-process', buildSource: 'alpha', usesToken: true, exec: fakePnpm(root), ...extra };
12
12
  }
13
13
 
14
14
  test('the closure comes from pnpm ls with the members filters, and the filters are literal per member', () => {
@@ -61,10 +61,10 @@
61
61
  // smoke-refusal <image> [--member <biomeId>/<componentKey>]x2
62
62
  // boot the pushed image with a declaration the host must refuse
63
63
  // (`smoke-refusal.mjs`).
64
- // dockerfile --write|--check --tooling-version <x.y.z> --copy <dir>… --cache-id <id> --build-source <repo>
64
+ // dockerfile --write|--check --copy <dir>… --cache-id <id> --build-source <repo>
65
65
  // render `<image-dir>/Dockerfile` from the package's template
66
66
  // (`composed-image-dockerfile.mjs`).
67
- // dockerfile --prebuilt --write|--check --tooling-version <x.y.z> --cache-id <id> --build-source <repo> [--copy <dir>]… [--stage-dir <dir>]
67
+ // dockerfile --prebuilt --write|--check --cache-id <id> --build-source <repo> [--copy <dir>]… [--stage-dir <dir>]
68
68
  // render the Dockerfile that ASSEMBLES the image from `stage` output, and its
69
69
  // `Dockerfile.dockerignore` (`composed-image-prebuilt-dockerfile.mjs`).
70
70
  //
@@ -462,13 +462,13 @@ const USAGE =
462
462
  ' coordinate --image-name <name> (env: SHA REGISTRY REGISTRY_ORG [TAG_ARGS_FILE])\n' +
463
463
  ' load-image <image>\n' +
464
464
  ' smoke-refusal <image> [--member <biomeId>/<componentKey>]x2\n' +
465
- ' dockerfile --write|--check --tooling-version <x.y.z> --copy <dir>… --cache-id <id> --build-source <repo>\n' +
466
- ' dockerfile --prebuilt --write|--check --tooling-version <x.y.z> --cache-id <id> --build-source <repo> [--copy <dir>]… [--stage-dir <dir>]';
465
+ ' dockerfile --write|--check --copy <dir>… --cache-id <id> --build-source <repo>\n' +
466
+ ' dockerfile --prebuilt --write|--check --cache-id <id> --build-source <repo> [--copy <dir>]… [--stage-dir <dir>]';
467
467
 
468
468
  const REPEATABLE = new Set(['manifests', 'copy', 'member', 'output', 'also']);
469
469
  const BOOLEAN = new Set(['prebuilt', 'each']);
470
470
  const VALUED = new Set(['root', 'image-dir', 'manifests', 'canonical-dockerfile', 'cross-repo-manifest', 'build-source',
471
- 'image-name', 'tooling-version', 'cache-id', 'copy', 'member', 'stage-dir', 'output', 'also',
471
+ 'image-name', 'cache-id', 'copy', 'member', 'stage-dir', 'output', 'also',
472
472
  ]);
473
473
 
474
474
  export function parseArguments(argv) {
@@ -573,7 +573,6 @@ export function main(argv, { log = console.log, write = (text) => process.stdout
573
573
  mode: options.mode,
574
574
  root,
575
575
  imageDir,
576
- toolingVersion: options['tooling-version'],
577
576
  copy: options.copy,
578
577
  cacheId: options['cache-id'],
579
578
  buildSource: options['build-source'],
@@ -586,7 +585,6 @@ export function main(argv, { log = console.log, write = (text) => process.stdout
586
585
  mode: options.mode,
587
586
  root,
588
587
  imageDir,
589
- toolingVersion: options['tooling-version'],
590
588
  copy: options.copy,
591
589
  cacheId: options['cache-id'],
592
590
  buildSource: options['build-source'],
@@ -239,12 +239,13 @@ test('the bin runs through a symlink, as npm installs it, and exits non-zero on
239
239
  });
240
240
 
241
241
  test('the new commands parse their options; coordinate refuses without an image name', async () => {
242
- const parsed = parseArguments(['dockerfile', '--write', '--copy', 'a', '--copy', 'b', '--cache-id', 'c', '--tooling-version', '1.2.3']);
242
+ const parsed = parseArguments(['dockerfile', '--write', '--copy', 'a', '--copy', 'b', '--cache-id', 'c']);
243
243
  assert.deepEqual(parsed.options.copy, ['a', 'b']);
244
244
  assert.equal(parsed.options['cache-id'], 'c');
245
245
  assert.deepEqual(parseArguments(['smoke-refusal', 'img', '--member', 'x/y', '--member', 'x/z']).options.member, ['x/y', 'x/z']);
246
246
  assert.throws(() => main(['coordinate'], { log() {} }), /--image-name/);
247
- assert.throws(() => main(['dockerfile', '--write'], { log() {} }), /exact x\.y\.z/);
247
+ assert.throws(() => main(['dockerfile', '--write'], { log() {} }), /at least one/);
248
+ assert.throws(() => parseArguments(['dockerfile', '--write', '--tooling-version', '1.2.3']), /unknown option --tooling-version/);
248
249
  assert.throws(() => main(['smoke-refusal'], { log() {} }), /usage/);
249
250
  });
250
251