@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 +3 -2
- package/src/effective-input.mjs +15 -15
- package/src/effective-input.test.mjs +24 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/repo-build-tooling",
|
|
3
|
-
"version": "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:
|
|
56
|
+
"sourceDigest": "sha256:4e12229e82ad289a206a1869e7811343c54502c154d87fd652c435de2263d0d2"
|
|
56
57
|
}
|
|
57
58
|
}
|
package/src/effective-input.mjs
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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) => [
|
|
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 {
|
|
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('
|
|
248
|
-
assert.throws(() => effectiveInputKeys({ root: '.', services: ['s'], platform: 'linux/arm64', baseImage: DIGEST, dockerfile: 'D', scripts: ['a/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', () => {
|