@xemahq/repo-build-tooling 0.29.0 → 0.30.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.29.0",
3
+ "version": "0.30.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)",
@@ -46,12 +46,13 @@
46
46
  "./service-image": "./src/service-image.mjs"
47
47
  },
48
48
  "dependencies": {
49
+ "@xemahq/contracts": "^0.117.1",
49
50
  "@xemahq/release-policy": "^16.0.0"
50
51
  },
51
52
  "scripts": {
52
53
  "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/service-image.test.mjs && node --test src/image-cpu-arch.test.mjs && node --test src/staged-image-proof.test.mjs && node --test src/effective-input.test.mjs"
53
54
  },
54
55
  "xemaRelease": {
55
- "sourceDigest": "sha256:a5327bf366fb31aeef8adb584888e52a5ae4da7aa5602eb672eb2876ed912a8b"
56
+ "sourceDigest": "sha256:4e12229e82ad289a206a1869e7811343c54502c154d87fd652c435de2263d0d2"
56
57
  }
57
58
  }
@@ -22,9 +22,11 @@
22
22
  import { execFileSync } from 'node:child_process';
23
23
  import { createHash } from 'node:crypto';
24
24
  import { readFileSync, readlinkSync, writeFileSync } from 'node:fs';
25
- import { join, posix, resolve } from 'node:path';
25
+ import { join, posix, relative, resolve, sep } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
27
 
28
+ import { effectiveInputTag } from '@xemahq/contracts/distribution';
29
+
28
30
  import { invokedDirectly } from './invoked-directly.mjs';
29
31
  import { rootBuildInputNames } from './staged-image-proof.mjs';
30
32
 
@@ -32,7 +34,7 @@ export const KEY_SCHEMA = 1;
32
34
  /** Build args that are a root-relative path or the commit, never part of the key. */
33
35
  export const NON_CONTENT_BUILD_ARGS = Object.freeze(['REPO_DIR', 'SERVICE_PATH', 'BUILD_SHA']);
34
36
  const DIGEST_PINNED = /^[^\s@]+@sha256:[0-9a-f]{64}$/;
35
- const COMPILE_ROOT_FILE = /^(\.npmrc|tsconfig[^/]*\.json)$/;
37
+ const COMPILE_ROOT_FILE = /^(\.npmrc|turbo\.json|tsconfig[^/]*\.json)$/;
36
38
  const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies'];
37
39
  const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
38
40
 
@@ -278,20 +280,27 @@ export function trackedFiles(root, dir) {
278
280
  return files;
279
281
  }
280
282
 
283
+ /** `{ fileName: bytes }` of the root files that steer the compile and the build: `.npmrc`, `turbo.json`, `tsconfig*.json`. */
284
+ export function readRootConfig(root) {
285
+ return Object.fromEntries(rootBuildInputNames(root).filter((n) => COMPILE_ROOT_FILE.test(n)).map((n) => [n, readFileSync(join(root, n))]));
286
+ }
287
+
281
288
  /** The key and parts of each of `services`, from one package-graph query and one read of the lockfile and tooling. */
282
289
  export function effectiveInputKeys({ root, services, platform, baseImage, dockerfile, scripts, buildArgs = {} }) {
283
290
  if (!Array.isArray(services) || services.length === 0) throw new Error('at least one service is required');
284
- const names = scripts.map((x) => posix.basename(x));
291
+ // A script is named by its root-relative path, so two files called `action.yml` in different directories are distinct.
292
+ const scriptName = (x) => relative(root, resolve(root, x)).split(sep).join('/');
293
+ const names = scripts.map(scriptName);
285
294
  const duplicate = names.find((n, i) => names.indexOf(n) !== i);
286
- if (duplicate !== undefined) throw new Error(`two --script files are named '${duplicate}': the key names scripts by file name`);
295
+ if (duplicate !== undefined) throw new Error(`--script '${duplicate}' is passed twice`);
287
296
  const graph = packageGraph(root);
288
297
  const toolManifest = JSON.parse(readFileSync(join(HERE, '..', 'package.json'), 'utf8'));
289
- const rootConfig = Object.fromEntries(rootBuildInputNames(root).filter((n) => COMPILE_ROOT_FILE.test(n)).map((n) => [n, readFileSync(join(root, n))]));
298
+ const rootConfig = readRootConfig(root);
290
299
  const lockText = readFileSync(join(root, 'pnpm-lock.yaml'), 'utf8');
291
300
  const install = installInputs({ lockText, packageJson: readFileSync(join(root, 'package.json'), 'utf8'), workspaceYaml: readFileSync(join(root, 'pnpm-workspace.yaml'), 'utf8') });
292
301
  const tooling = {
293
302
  dockerfile: readFileSync(dockerfile),
294
- scripts: Object.fromEntries(scripts.map((s) => [posix.basename(s), readFileSync(s)])),
303
+ scripts: Object.fromEntries(scripts.map((s) => [scriptName(s), readFileSync(resolve(root, s))])),
295
304
  buildArgs,
296
305
  stageTool: { name: toolManifest.name, version: toolManifest.version },
297
306
  };
@@ -320,15 +329,6 @@ export function effectiveInputKey({ service, ...options }) {
320
329
  return effectiveInputKeys({ ...options, services: [service] })[service];
321
330
  }
322
331
 
323
- /**
324
- * The one place a key becomes its registry tag: `ei-<key>`. INTERIM: this is to move to `@xemahq/contracts/distribution`
325
- * (edition's ruling), the single home of the prefix; until it is published this is the one copy.
326
- */
327
- export function effectiveInputTag(key) {
328
- if (!/^[0-9a-f]{64}$/.test(key)) throw new Error(`'${key}' is not an effective-input key (64 lowercase hex)`);
329
- return `ei-${key}`;
330
- }
331
-
332
332
  /**
333
333
  * The `--out` map: per service `{ schema, platform, key, parts, tag }`. `tag` is built only by `effectiveInputTag`, so a consumer
334
334
  * (release-policy's `prepare --effective-inputs`) reads it here and declares no prefix.
@@ -6,7 +6,9 @@ import { dirname, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { test } from 'node:test';
8
8
 
9
- import { canonicalJson, closureIn, effectiveInput, effectiveInputKeys, effectiveInputRecord, effectiveInputTag, installInputs, lockfileSlice, parseArgs, trackedFiles } from './effective-input.mjs';
9
+ import { effectiveInputTag } from '@xemahq/contracts/distribution';
10
+
11
+ import { canonicalJson, closureIn, effectiveInput, effectiveInputKeys, effectiveInputRecord, installInputs, lockfileSlice, parseArgs, readRootConfig, trackedFiles } from './effective-input.mjs';
10
12
 
11
13
  const DIGEST = `ghcr.io/x/node-runtime@sha256:${'a'.repeat(64)}`;
12
14
  const lock = (prefix, settings = 'autoInstallPeers: true') => `lockfileVersion: '9.0'
@@ -146,6 +148,7 @@ test('each input class changes the key', () => {
146
148
  'base image digest': keyOf(ws, { baseImage: `ghcr.io/x/node-runtime@sha256:${'b'.repeat(64)}` }),
147
149
  arch: keyOf(ws, { platform: 'linux/amd64' }),
148
150
  'root compile config': keyOf(ws, { rootConfig: { ...base.rootConfig, '.npmrc': 'a=c' } }),
151
+ 'root turbo config': keyOf(ws, { rootConfig: { ...base.rootConfig, 'turbo.json': '{"tasks":{}}' } }),
149
152
  };
150
153
  for (const [name, key] of Object.entries(variants)) assert.notEqual(key, original, `${name} must change the key`);
151
154
  assert.equal(new Set(Object.values(variants)).size, Object.keys(variants).length, 'the classes are distinct');
@@ -204,13 +207,6 @@ test('the command line takes repeated build args and refuses a duplicate', () =>
204
207
  assert.throws(() => parseArgs([...argv, '--build-arg', 'A=2']), /given twice/);
205
208
  });
206
209
 
207
- test('the tag is ei-<key>, and a bad key is refused', () => {
208
- const key = 'a'.repeat(64);
209
- assert.equal(effectiveInputTag(key), `ei-${key}`);
210
- assert.throws(() => effectiveInputTag('abc'), /not an effective-input key/);
211
- assert.throws(() => effectiveInputTag('ei-' + key), /not an effective-input key/);
212
- });
213
-
214
210
  test('the closure is walked transitively from the package graph, including a package with no build task, and refuses an unknown service', () => {
215
211
  const graph = new Map([
216
212
  ['svc', { name: 'svc', dir: 'apps/svc', dependencies: ['mid', 'external'] }],
@@ -244,8 +240,26 @@ test('a lockfileVersion other than 9.0 is refused by name', () => {
244
240
  }
245
241
  });
246
242
 
247
- test('two scripts with the same file name are refused', () => {
248
- assert.throws(() => effectiveInputKeys({ root: '.', services: ['s'], platform: 'linux/arm64', baseImage: DIGEST, dockerfile: 'D', scripts: ['a/x.mjs', 'b/x.mjs'] }), /two --script files are named 'x.mjs'/);
243
+ test('the same script passed twice is refused', () => {
244
+ assert.throws(() => effectiveInputKeys({ root: '.', services: ['s'], platform: 'linux/arm64', baseImage: DIGEST, dockerfile: 'D', scripts: ['a/x.mjs', './a/x.mjs'] }), /--script 'a\/x.mjs' is passed twice/);
245
+ });
246
+
247
+ test('editing the root turbo.json changes the key, through the real root-file read', () => {
248
+ const ws = workspace('');
249
+ try {
250
+ execFileSync('git', ['init', '-q'], { cwd: ws.root });
251
+ writeFileSync(join(ws.root, '.npmrc'), 'a=b');
252
+ writeFileSync(join(ws.root, 'turbo.json'), '{"tasks":{"build":{}}}');
253
+ for (const f of ['.npmrc', 'turbo.json']) execFileSync('git', ['add', f], { cwd: ws.root });
254
+ const keyNow = () => keyOf(ws, { rootConfig: readRootConfig(ws.root) });
255
+ assert.deepEqual(Object.keys(readRootConfig(ws.root)).sort(), ['.npmrc', 'turbo.json']);
256
+ const before = keyNow();
257
+ assert.equal(keyNow(), before);
258
+ writeFileSync(join(ws.root, 'turbo.json'), '{"tasks":{"build":{"outputs":["dist/**"]}}}');
259
+ assert.notEqual(keyNow(), before);
260
+ } finally {
261
+ rmSync(ws.root, { recursive: true });
262
+ }
249
263
  });
250
264
 
251
265
  test('the --out map carries the tag per service, from effectiveInputTag', () => {