@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 +1 -1
- package/package.json +2 -2
- package/src/composed-image-dockerfile.mjs +6 -8
- package/src/composed-image-dockerfile.test.mjs +5 -6
- package/src/composed-image-prebuilt-dockerfile.mjs +4 -5
- package/src/composed-image-prebuilt-dockerfile.test.mjs +1 -1
- package/src/composed-image.mjs +5 -7
- package/src/composed-image.test.mjs +3 -2
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 `--
|
|
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.
|
|
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:
|
|
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
|
-
// (
|
|
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({
|
|
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({
|
|
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({
|
|
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.
|
|
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
|
|
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,
|
|
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({
|
|
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({
|
|
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,
|
|
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', () => {
|
package/src/composed-image.mjs
CHANGED
|
@@ -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 --
|
|
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 --
|
|
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 --
|
|
466
|
-
' dockerfile --prebuilt --write|--check --
|
|
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', '
|
|
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'
|
|
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() {} }), /
|
|
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
|
|