@xemahq/repo-build-tooling 0.25.0 → 0.26.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.
@@ -1,333 +1,136 @@
1
1
  // `xema-composed-image dockerfile --prebuilt --write|--check`: renders
2
- // `<image-dir>/Dockerfile` AND `<image-dir>/Dockerfile.dockerignore` for the image that
3
- // is ASSEMBLED from build outputs made on the CI runner (`xema-composed-image stage`).
4
- // Docker compiles nothing. The full-build template (`composed-image-dockerfile.mjs`)
2
+ // `<image-dir>/Dockerfile` AND `<image-dir>/Dockerfile.dockerignore` for the image that is
3
+ // COPIED from a tree the CI runner staged (`xema-composed-image stage`). Docker installs nothing,
4
+ // compiles nothing and reaches no network. The full-build template (`composed-image-dockerfile.mjs`)
5
5
  // stays available for repositories that still build inside Docker.
6
- import { execFileSync } from 'node:child_process';
7
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
7
  import { dirname, join, posix } from 'node:path';
9
8
 
10
- import { COMPOSED_IMAGE_DIR, IN_IMAGE_ENTRY } from './composed-image-paths.mjs';
9
+ import { COMPOSED_IMAGE_DIR, COMPOSED_MANIFEST_FILE, IN_IMAGE_ENTRY, NODE_MAJOR_LABEL, PROOF_DIRECTORY, STAGE_APP_LAYER, STAGE_PROOF_DIRECTORY, STAGE_STORE_LAYER, VERIFIED_IMAGE_FILE } from './composed-image-paths.mjs';
11
10
  import { toolVersion } from './composed-image-stage.mjs';
12
- import { npmrcUsesToken } from './composed-image-dockerfile.mjs';
13
- import { IGNORED_DIRECTORIES, IGNORED_FILE_PATTERNS, packageSources } from './composed-image-package-files.mjs';
14
- import { memberClosure, memberFilters, memberManifests } from './composed-image-members.mjs';
15
- import { needsPrismaSchemaEngine } from './composed-image-prisma-engine.mjs';
11
+ import { IGNORED_DIRECTORIES, IGNORED_FILE_PATTERNS } from './composed-image-package-files.mjs';
12
+ import { memberFilters } from './composed-image-members.mjs';
13
+ import { CONTEXT_DIRECTORY, rootBuildInputNames } from './staged-image-proof.mjs';
16
14
 
17
15
  export const DEFAULT_STAGE_DIR = '.xema-stage';
18
16
  const DIRECTORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*\/?$/;
19
- const CACHE_ID = /^[a-z0-9][a-z0-9-]*$/;
20
17
  const REPOSITORY = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
21
18
  const DIGEST_REFERENCE = /^[a-z0-9][a-z0-9._/-]*(:[A-Za-z0-9._-]+)?@sha256:[0-9a-f]{64}$/;
19
+ /** The image label that carries the repository its member index names; the stage reads it back from the committed Dockerfile. */
20
+ export const BUILD_SOURCE_LABEL = 'dev.xema.composed-image.build-source';
21
+ /** Where the proof stage puts the `--copy` directories, so the build context's bytes are what the stage hashed. */
22
+ const COPIED_DIRECTORY = `${PROOF_DIRECTORY}/copied`;
22
23
 
23
- function validate({ copy, cacheId, buildSource, stageDir, base }) {
24
+ const NODE_MAJOR = /^[1-9][0-9]*$/;
25
+
26
+ function validate({ copy, buildSource, stageDir, base, nodeMajor }) {
27
+ if (!NODE_MAJOR.test(nodeMajor ?? '')) throw new Error(`--node-major must be the Node major of the --base image (e.g. 22), got "${nodeMajor}"`);
24
28
  if (!DIGEST_REFERENCE.test(base ?? '')) throw new Error(`--base must be an image reference pinned by digest (<ref>@sha256:<64 hex>), got "${base}"`);
25
29
  for (const directory of [...(copy ?? []), stageDir]) {
26
30
  if (!DIRECTORY.test(directory) || directory.startsWith('/') || directory.split('/').includes('..')) {
27
31
  throw new Error(`"${directory}" is not a repository-relative directory`);
28
32
  }
29
33
  }
30
- if (!CACHE_ID.test(cacheId ?? '')) throw new Error(`--cache-id must be lowercase [a-z0-9-], got "${cacheId}"`);
31
34
  if (!REPOSITORY.test(buildSource ?? '')) throw new Error(`--build-source must name a repository, got "${buildSource}"`);
32
35
  }
33
36
 
34
- /** The ignore file: the build context carries sources, never local build output, except the staged outputs. */
37
+ /** The ignore file: the build context carries sources, never local build output, except the staged tree. */
35
38
  export function renderPrebuiltDockerignore({ stageDir = DEFAULT_STAGE_DIR } = {}) {
36
39
  return `# @generated by \`xema-composed-image dockerfile --prebuilt --write\` — DO NOT EDIT.
37
- # Local build output stays out of the context; the staged outputs carry dist and node_modules/.prisma paths, so they are re-included LAST.
40
+ # Local build output stays out of the context; the staged tree carries dist and node_modules paths, so it is re-included LAST.
38
41
  .git
39
42
  ${[...IGNORED_DIRECTORIES.map((d) => `**/${d}`), ...IGNORED_FILE_PATTERNS.map((p) => `**/${p}`)].join('\n')}
40
43
  !${stageDir}/**
41
44
  `;
42
45
  }
43
46
 
44
- /** The `onlyBuiltDependencies:` items of a `pnpm-workspace.yaml` text (block list; comments skipped; ends at the next top-level key). */
45
- export function onlyBuiltDependencies(workspaceYaml) {
46
- const names = [];
47
- let inList = false;
48
- for (const line of workspaceYaml.split('\n')) {
49
- if (!inList) {
50
- inList = /^onlyBuiltDependencies:\s*(#.*)?$/.test(line);
51
- continue;
52
- }
53
- if (/^\s*(#.*)?$/.test(line)) continue;
54
- if (/^\S/.test(line)) break;
55
- const item = /^\s+-\s+(?:'([^']*)'|"([^"]*)"|([^\s#]+))\s*(?:#.*)?$/.exec(line);
56
- if (item === null) throw new Error(`pnpm-workspace.yaml: cannot read onlyBuiltDependencies item "${line.trim()}"`);
57
- names.push(item[1] ?? item[2] ?? item[3]);
58
- }
59
- for (const name of names) {
60
- if (!/^[@A-Za-z0-9._/-]+$/.test(name)) throw new Error(`pnpm-workspace.yaml: onlyBuiltDependencies "${name}" is not a package name`);
61
- }
62
- return names;
63
- }
64
-
65
- /** The install-flag word the rendered RUN keeps as a shell variable; `injectFlags()` is what it expands to. */
66
- export const INJECT_TOKEN = '${INJECT}';
67
- export const injectFlags = () => ['--config.inject-workspace-packages=true', '--config.dedupe-injected-deps=false'];
68
-
69
- /**
70
- * The deps stage's `--lockfile-only` resolve, as words (`INJECT_TOKEN` stands for `injectFlags()`).
71
- * The image runs it `--offline` against the metadata cache `pnpm-context --image-dir` warmed; the warm-up
72
- * runs the SAME words with `--prefer-offline` (`offline: false`). Both read THIS function, so the metadata
73
- * the context holds is exactly what the image resolves.
74
- */
75
- export const lockfileOnlyWords = (filters, { offline = true } = {}) =>
76
- ['pnpm', 'install', '--lockfile-only', offline ? '--offline' : '--prefer-offline', INJECT_TOKEN, ...filters];
77
- /** The same command with the inject words expanded: `[file, ...args]`. `offline: false` is the cache warm-up form. */
78
- export const lockfileOnlyCommand = (filters, options) => lockfileOnlyWords(filters, options).flatMap((w) => (w === INJECT_TOKEN ? injectFlags() : [w]));
79
-
80
- /** The line that brings one `--copy` directory into the deps stage; `depsCopyDirs` is its inverse (pnpm-context reads the directories back from the committed Dockerfile). */
81
- const copyLine = (directory) => `COPY ${directory} ./${directory}`;
82
- export function depsCopyDirs(dockerfileText) {
83
- const deps = /^FROM base AS deps\n([\s\S]*?)^COPY --parents/m.exec(dockerfileText);
84
- if (deps === null) throw new Error('the Dockerfile has no deps stage with COPY --parents — render it: xema-composed-image dockerfile --prebuilt --write');
85
- return [...deps[1].matchAll(/^COPY (\S+\/) \.\/\1$/gm)].map((m) => m[1]);
86
- }
87
-
88
- /** Build-script dependencies the image never runs: Prisma 7 with the pg adapter ships no engine (migrations run in-app), the rest are JS or fetch nothing the image needs. */
89
- export const REBUILD_EXCLUDED = Object.freeze(['prisma', '@prisma/client', '@prisma/engines', '@nestjs/core', 'esbuild', '@swc/core']);
90
-
91
- /**
92
- * The `onlyBuiltDependencies` the image must still `pnpm rebuild`: those in the members' PROD closure
93
- * (`prodPackages`: `[{ name, dir }]`) less `REBUILD_EXCLUDED`. A module that needs `node-gyp` (a `binding.gyp`
94
- * and no optional prebuilt packages) cannot be built here (the install is offline, `--ignore-scripts`, and the base
95
- * has no toolchain), so it is refused: build it on a runner of the target arch and stage the `.node` files.
96
- */
97
- export function rebuildList({ built, prodPackages }) {
98
- const present = new Map(prodPackages.map((p) => [p.name, p.dir]));
99
- const names = built.filter((name) => !REBUILD_EXCLUDED.includes(name) && present.has(name));
100
- for (const name of names) {
101
- const dir = present.get(name);
102
- let optional = 0;
103
- try {
104
- optional = Object.keys(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).optionalDependencies ?? {}).length;
105
- } catch {
106
- /* unreadable manifest: judged by binding.gyp alone */
107
- }
108
- if (existsSync(join(dir, 'binding.gyp')) && optional === 0) {
109
- throw new Error(
110
- `${name} needs node-gyp (binding.gyp, no optional prebuilt packages) and no native artifact is staged for it: ` +
111
- 'the prebuilt composed image installs offline with --ignore-scripts and has no toolchain. ' +
112
- 'Build it on a runner of each target arch (the arm runner for arm64) and stage the .node files; see the arm64 native-artifact note in composed-image-prebuilt-dockerfile.mjs. Refusing to render.',
113
- );
114
- }
115
- }
116
- return names;
117
- }
118
-
119
- const defaultExec = (file, args, { cwd }) => execFileSync(file, args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
120
-
121
- /** `[{ name, dir }]` for every installed package in the members' prod closure (`pnpm ls --prod --parseable`); needs `pnpm install` to have run. */
122
- export function memberProdPackages({ root = process.cwd(), imageDir = COMPOSED_IMAGE_DIR, exec = defaultExec } = {}) {
123
- const words = memberFilters({ root, imageDir }).flatMap((f) => f.split(' '));
124
- const listing = exec('pnpm', ['ls', '-r', '--prod', '--depth', 'Infinity', '--parseable', ...words], { cwd: root });
125
- const seen = new Map();
126
- for (const line of listing.split('\n').map((l) => l.trim()).filter(Boolean)) {
127
- if (!line.includes('node_modules')) continue; // workspace packages themselves
128
- let dir;
129
- try {
130
- dir = realpathSync(line);
131
- const name = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name;
132
- if (typeof name === 'string') seen.set(name, dir);
133
- } catch {
134
- /* not an installed package directory */
135
- }
136
- }
137
- if (seen.size === 0) throw new Error('pnpm ls --prod listed no installed package: run pnpm install --frozen-lockfile before rendering the prebuilt Dockerfile');
138
- return [...seen].map(([name, dir]) => ({ name, dir }));
139
- }
140
-
141
- /** Where the deps stage bind-mounts `<stage dir>/prisma-engines` (`<enginesVersion>/schema-engine-<binaryTarget>`). */
142
- const PRISMA_ENGINES_MOUNT = '/xema-prisma-engines';
47
+ const normalizedCopy = (copy) => [...new Set(copy.map((d) => posix.normalize(`${d}/`)))];
48
+ const copyLine = (directory) => `COPY ${directory} /${COPIED_DIRECTORY}/${directory}`;
143
49
 
144
- /**
145
- * The deps-stage step that puts the Prisma schema engine where Prisma looks, rendered only when the
146
- * prod closure carries the prisma CLI (the one thing that runs the engine). It runs in DEPS so the
147
- * engine lands in `node_modules/.pnpm/@prisma+engines@<v>/node_modules/@prisma/engines/`, which the
148
- * final stage takes from deps (layer 1); assemble's copy excludes the .pnpm store.
149
- */
150
- function prismaSchemaEngineStep({ toolMount, node, stageDir }) {
151
- return `
152
- # The Prisma SCHEMA ENGINE for this image's platform. \`prisma migrate deploy\` runs it (the
153
- # Rust-free client does not replace it), and the prisma CLI downloads it into @prisma/engines
154
- # at RUNTIME when it is missing, which the read-only, non-root container refuses. The install
155
- # above is --ignore-scripts, so @prisma/engines' postinstall never fetched it. The runner
156
- # staged it for every image platform at the lockfile's engines version (Prisma's downloader,
157
- # sha256-checked) in \`${stageDir}/prisma-engines/\`; this copies the one for the platform
158
- # Prisma computes HERE into each @prisma/engines a prisma CLI resolves, and fails the build
159
- # unless it runs and reports that version. Here in deps, so the final stage carries it.
160
- RUN --network=none ${toolMount} \\
161
- --mount=type=bind,source=${stageDir}/prisma-engines,target=${PRISMA_ENGINES_MOUNT} \\
162
- node ${node} prisma-schema-engine ${PRISMA_ENGINES_MOUNT}
163
- `;
50
+ /** What the stage reads back from the committed Dockerfile, so the stage and the render have one definition: the `--copy` directories and the build source. */
51
+ export function parseRenderedDockerfile(dockerfileText) {
52
+ const copy = [...dockerfileText.matchAll(new RegExp(`^COPY (\\S+/) /${COPIED_DIRECTORY}/\\1$`, 'gm'))].map((m) => m[1]);
53
+ const buildSource = new RegExp(`^LABEL ${BUILD_SOURCE_LABEL.replaceAll('.', '\\.')}="([^"]+)"$`, 'm').exec(dockerfileText)?.[1];
54
+ if (buildSource === undefined) throw new Error('the Dockerfile carries no build-source label — render it: xema-composed-image dockerfile --prebuilt --write');
55
+ return { copy, buildSource };
164
56
  }
165
57
 
166
- const parts = (sources) => sources.map((source) => ` ${source} \\`).join('\n');
167
-
168
- /** The prebuilt Dockerfile text. The closure and the members' literal filters are derived from `root` (`memberClosure`); `exec` is injectable for tests. */
169
- export function renderPrebuiltDockerfile({
170
- root = process.cwd(),
171
- imageDir = COMPOSED_IMAGE_DIR,
172
- copy = [],
173
- cacheId,
174
- buildSource,
175
- usesToken,
176
- stageDir = DEFAULT_STAGE_DIR,
177
- base,
178
- exec,
179
- prodPackages = [],
180
- }) {
181
- if (typeof usesToken !== 'boolean') throw new Error('usesToken must be a boolean derived from .npmrc');
182
- validate({ copy, cacheId, buildSource, stageDir, base });
58
+ /** The prebuilt Dockerfile text. The members' literal filters and the root build inputs are derived from `root`. */
59
+ export function renderPrebuiltDockerfile({ root = process.cwd(), imageDir = COMPOSED_IMAGE_DIR, copy = [], buildSource, stageDir = DEFAULT_STAGE_DIR, base, nodeMajor }) {
60
+ validate({ copy, buildSource, stageDir, base, nodeMajor });
183
61
  const toolingVersion = toolVersion();
184
- const workspaceFile = join(root, 'pnpm-workspace.yaml');
185
- const built = existsSync(workspaceFile) ? onlyBuiltDependencies(readFileSync(workspaceFile, 'utf8')) : [];
186
- if (built.length === 0) {
187
- throw new Error('pnpm-workspace.yaml has no onlyBuiltDependencies: — `pnpm rebuild` would have nothing to build; refusing to render');
188
- }
189
- const closure = memberClosure({ root, imageDir, exec });
190
- const rebuild = rebuildList({ built, prodPackages });
191
62
  const filters = memberFilters({ root, imageDir }).join(' ');
192
- const manifests = memberManifests({ root, imageDir });
63
+ const rootInputs = rootBuildInputNames(root);
193
64
  const dirFlag = imageDir === COMPOSED_IMAGE_DIR ? '' : ` --image-dir ${imageDir}`;
194
65
  const toolDir = '/opt/xema-composed-image';
195
66
  const toolMount = `--mount=type=bind,source=${stageDir}/tool,target=${toolDir}`;
196
67
  const node = `${toolDir}/${IN_IMAGE_ENTRY}`;
197
- const store = '--mount=type=bind,from=xema-pnpm-store,target=/root/.pnpm-store,rw';
198
- const metadata = '--mount=type=bind,from=xema-pnpm,source=cache,target=/root/.cache/pnpm,rw';
199
- const secret = usesToken ? ' --mount=type=secret,id=npm_token,required=true \\\n' : '';
200
- const tokenExport = usesToken
201
- ? 'export NPM_TOKEN="$(cat /run/secrets/npm_token)" && export GH_PACKAGES_TOKEN="$NPM_TOKEN" && \\\n '
202
- : '';
203
- const copies = [...new Set(copy.map((d) => posix.normalize(`${d}/`)))].map(copyLine).join('\n');
204
- const manifestSources = closure.map((directory) => `${directory}/package.json`);
205
- const assembleSources = [...closure.flatMap((directory) => packageSources({ root, directory })), ...manifests];
206
- const imageCopy = posix.normalize(`${imageDir}/`);
207
- const text = `# @generated by \`xema-composed-image dockerfile --prebuilt --write\` (@xemahq/repo-build-tooling@${toolingVersion}) — DO NOT EDIT.
68
+ const copies = normalizedCopy(copy).map(copyLine).join('\n');
69
+ const layers = `COPY ${stageDir}/${STAGE_STORE_LAYER}/ /\nCOPY ${stageDir}/${STAGE_APP_LAYER}/ /`;
70
+ const dockerfile = `# @generated by \`xema-composed-image dockerfile --prebuilt --write\` (@xemahq/repo-build-tooling@${toolingVersion}) — DO NOT EDIT.
208
71
  # Re-render with the same options; \`--check\` refuses a stale file (and a stale Dockerfile.dockerignore).
209
72
  #
210
- # The COMPOSED-PROCESS image of this repository, ASSEMBLED from build outputs: ONE image
211
- # per build (repository at commit), every member of \`image-build-inputs.json\` in ONE module
212
- # graph behind the SDK's \`xema-composed-process\` host. Docker compiles nothing. The CI
213
- # runner ran \`turbo run build prisma:generate\` for the members' closures and
214
- # \`xema-composed-image stage ${stageDir}\` gathered the outputs into the build-step
215
- # directory \`${stageDir}/\` (\`out/\` = dist and generated Prisma clients, pure JS/wasm;
216
- # \`tool/\` = this tool, bind-mounted into the steps below and never copied into a layer).
73
+ # The COMPOSED-PROCESS image of this repository, COPIED from a tree the CI runner staged: ONE
74
+ # image per build (repository at commit), every member of \`image-build-inputs.json\` in ONE module
75
+ # graph behind the SDK's \`xema-composed-process\` host. Docker installs nothing, compiles nothing
76
+ # and has no network. The runner built the members (\`turbo run build prisma:generate\`) and
77
+ # \`xema-composed-image stage ${stageDir} --platform linux/<arch>\` produced, from the job's own
78
+ # pnpm store, the production tree of the members' closures for that platform (the install, the
79
+ # injected workspace packages, the member index, the Prisma clients and the platform's schema
80
+ # engine) and wrote it to \`${stageDir}/${STAGE_STORE_LAYER}/\` (the \`.pnpm\` packages, stable until the lockfile changes) and
81
+ # \`${stageDir}/${STAGE_APP_LAYER}/\` (everything else). \`${stageDir}/tool/\` is this tool, bind-mounted into the steps
82
+ # below and never copied into a layer.
217
83
  #
218
84
  # Stages:
219
- # deps arch-native. Inputs are the lockfile and the closure's manifests ONLY, so the
220
- # layer is cached until one of them changes. OFFLINE: every install RUN is
221
- # \`--network=none\` and reads only the CI runner's pnpm store (\`xema-pnpm-store\`
222
- # context), the packed pnpm and the warmed registry-metadata cache (\`xema-pnpm\`
223
- # context, built by \`xema-composed-image pnpm-context --image-dir\`). corepack
224
- # installs pnpm from that tarball, never from the network. Production install with
225
- # the workspace packages INJECTED and \`--ignore-scripts\`. \`pnpm rebuild <names>\`
226
- # runs only for \`onlyBuiltDependencies\` present in the members' PROD closure, less
227
- # prisma, @prisma/client, @prisma/engines (Prisma 7 + pg adapter ships no engine and
228
- # migrations run in-app), @nestjs/core, esbuild and @swc/core; when none is left the
229
- # step is not rendered. The list is rendered, so a change makes \`--check\` stale.
230
- # assemble source of the closure + the staged outputs, the injected copies synced to
231
- # their \`files\`, then the proofs on the target arch: Prisma clients run, the
232
- # member index is written, every member loads ALONE as \`node\`, production.
233
- # (final) \`/monorepo\` of assemble, shipped as THREE layers so a commit that changes only
234
- # members' code re-pushes only the layers that changed (see the final stage).
235
- # Nothing of the tool ships.
236
- #
237
- # NATIVE MODULES. No prod-closure module needs node-gyp today. The renderer REFUSES to render when
238
- # one appears (a \`binding.gyp\` and no optional prebuilt packages) because this image installs with
239
- # --ignore-scripts and carries no compiler. The path then: build the module on a runner of EACH target
240
- # arch natively (the \`arm\` runner for arm64; no QEMU), in the digest-pinned bookworm node image with
241
- # --network=none and the image's node headers, stage \`native/<arch>/<pkg>/*.node\`, and COPY them from
242
- # \`native/\${TARGETARCH}/\` in the deps stage.
85
+ # proof The staged tree and the repository's inputs meet. The root build inputs that decide the
86
+ # staged bytes are COPIED into \`/${PROOF_DIRECTORY}/${CONTEXT_DIRECTORY}/\` and the \`--copy\` directories into
87
+ # \`/${COPIED_DIRECTORY}/\`, which puts them in the image's content tag. \`prove\` refuses the build unless the
88
+ # stage was made at BUILD_SHA from exactly those bytes, with this tool, for this platform, and
89
+ # the literal \`--filter\` words below are the ones it staged. Then the Prisma clients run and every
90
+ # member loads ALONE as \`node\`, production, on the target arch.
91
+ # (final) the same two COPYs of the staged tree, plus the proof's verdict, so the image cannot exist without it.
243
92
  #
244
93
  # The \`--filter <package>...\` words below are LITERAL: the content hash of this image reads
245
94
  # them to narrow the lockfile to the members' closures.
246
- FROM scratch AS xema-pnpm
247
- FROM scratch AS xema-pnpm-store
248
-
249
95
  FROM ${base} AS base
250
- RUN corepack enable && \\
251
- pnpm config set store-dir /root/.pnpm-store
252
96
 
253
- FROM base AS deps
254
- WORKDIR /monorepo
255
- ENV CI=true
256
- # corepack must not download pnpm: the packed tarball comes from the runner.
257
- ENV COREPACK_ENABLE_NETWORK=0
258
- # The runner's store is bind-mounted here. Set it for pnpm explicitly: the base layer's \`pnpm config set\` was
259
- # written by whatever pnpm corepack fetched then, and an offline resolve falls back to the store index.
260
- ENV npm_config_store_dir=/root/.pnpm-store
261
- COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
262
- ${copies === '' ? '' : `${copies}\n`}COPY --parents \\
263
- ${parts(manifestSources)}
264
- ./
265
- RUN --network=none --mount=type=bind,from=xema-pnpm,target=/xema-pnpm \\
266
- test -f /xema-pnpm/staged.json || { \\
267
- echo "the prebuilt composed image needs --build-context xema-pnpm and xema-pnpm-store (CI only); render the full image (no --prebuilt) to build elsewhere" >&2; \\
268
- exit 1; \\
269
- } && corepack install -g /xema-pnpm/corepack/pnpm.tgz
270
- RUN --network=none ${store} \\
271
- ${metadata} \\
272
- ${secret} ${toolMount} \\
273
- ${tokenExport}INJECT="${injectFlags().join(' ')}" && \\
274
- cp pnpm-lock.yaml /tmp/pnpm-lock.committed.yaml && \\
275
- ${lockfileOnlyWords(filters.split(' ')).join(' ')} && \\
276
- node ${node} pins${dirFlag} /tmp/pnpm-lock.committed.yaml && \\
277
- pnpm install --prod --offline --ignore-scripts \${INJECT} ${filters} --frozen-lockfile${rebuild.length === 0 ? '' : ` && \\
278
- pnpm rebuild ${rebuild.join(' ')} ${filters}`}
279
- ${needsPrismaSchemaEngine(prodPackages) ? prismaSchemaEngineStep({ toolMount, node, stageDir }) : ''}
280
- FROM deps AS assemble
281
- COPY --parents \\
282
- ${parts(assembleSources)}
283
- ./
284
- COPY ${imageCopy} ./${imageCopy}
285
- COPY ${stageDir}/out/ ./
97
+ FROM base AS proof
98
+ WORKDIR /${PROOF_DIRECTORY}
286
99
  ARG BUILD_SHA
287
- RUN ${toolMount} \\
100
+ COPY ${stageDir}/${STAGE_PROOF_DIRECTORY}/${COMPOSED_MANIFEST_FILE} /${PROOF_DIRECTORY}/
101
+ COPY ${rootInputs.join(' ')} /${PROOF_DIRECTORY}/${CONTEXT_DIRECTORY}/
102
+ ${copies === '' ? '' : `${copies}\n`}${layers}
103
+ RUN --network=none ${toolMount} \\
104
+ : "\${BUILD_SHA:?BUILD_SHA build-arg is required}" && \\
288
105
  T="node ${node}" && \\
289
- $T sync-injected${dirFlag} && $T prisma-check${dirFlag} && \\
290
- : "\${BUILD_SHA:?BUILD_SHA build-arg is required}" && $T index "\${BUILD_SHA}" --build-source ${buildSource}${dirFlag} && \\
291
- cp ${toolDir}/src/composed-migrate.mjs /monorepo/xema-composed-migrate.mjs
106
+ $T prove /${PROOF_DIRECTORY} --root /monorepo --commit "\${BUILD_SHA}" ${filters}${dirFlag} && \\
107
+ $T prisma-check --root /monorepo${dirFlag}
292
108
 
293
109
  # Every member's module graph loads here, ALONE (one process each), through the host's own
294
110
  # entry reader, as the user and NODE_ENV the image runs with, or the image is never pushed.
295
111
  USER node
296
- RUN ${toolMount} \\
297
- NODE_ENV=production node ${node} load --each${dirFlag}
112
+ RUN --network=none ${toolMount} \\
113
+ NODE_ENV=production node ${node} load --each --root /monorepo${dirFlag}
298
114
 
299
- # The final stage is layered by how often each part changes, so a registry and a node
300
- # already holding the stable layers fetch only the rest. The three copies overlay to
301
- # exactly assemble's /monorepo (proven by a tree diff of both images).
302
- # 1 deps root manifests, the external .pnpm store, and the injected stubs that hold
303
- # only package.json. Stable while the lockfile and the closure's package.json
304
- # files are unchanged.
305
- # 2 assemble members, dist, Prisma clients, member index, migrate script: everything
306
- # but the .pnpm store, whose packages layer 1 already carries.
307
- # 3 injected the workspace copies \`node_modules/.pnpm/*@file+*\`: \`sync-injected\` fills
308
- # them from the members' files, so they change with every commit. \`--parents\`
309
- # keeps their paths under /.
310
115
  FROM base
311
116
  WORKDIR /monorepo
312
- COPY --from=deps /monorepo /monorepo
313
- COPY --from=assemble --exclude=node_modules/.pnpm /monorepo /monorepo
314
- COPY --from=assemble --parents /monorepo/node_modules/.pnpm/*@file+* /
117
+ LABEL ${NODE_MAJOR_LABEL}="${nodeMajor}"
118
+ ${layers}
119
+ COPY --from=proof /${PROOF_DIRECTORY}/${VERIFIED_IMAGE_FILE} /monorepo/${VERIFIED_IMAGE_FILE}
120
+ LABEL ${BUILD_SOURCE_LABEL}="${buildSource}"
315
121
  ENV NODE_ENV=production
316
122
  # Set by the IMAGE, never by a deployment (kernel \`COMPOSED_PROCESS_ENV.MemberIndex\`).
317
123
  ENV XEMA_COMPOSED_MEMBER_INDEX=/monorepo/xema-composed-member-index.json
318
124
  USER node
319
125
  CMD ["node", "/monorepo/xema-composed-process"]
320
126
  `;
321
- return { dockerfile: text, dockerignore: renderPrebuiltDockerignore({ stageDir }), closure };
127
+ return { dockerfile, dockerignore: renderPrebuiltDockerignore({ stageDir }) };
322
128
  }
323
129
 
324
130
  /** `--write` writes both files; `--check` throws when either differs. */
325
- export function syncPrebuiltDockerfile({ mode, root = process.cwd(), imageDir = COMPOSED_IMAGE_DIR, exec, ...options }) {
131
+ export function syncPrebuiltDockerfile({ mode, root = process.cwd(), imageDir = COMPOSED_IMAGE_DIR, ...options }) {
326
132
  if (mode !== 'write' && mode !== 'check') throw new Error(`mode must be write or check, got "${mode}"`);
327
- const npmrc = join(root, '.npmrc');
328
- if (!existsSync(npmrc)) throw new Error(`${npmrc} is absent — the Dockerfile copies it and its token use decides the build secret`);
329
- const prodPackages = options.prodPackages ?? memberProdPackages({ root, imageDir, exec });
330
- const { dockerfile, dockerignore } = renderPrebuiltDockerfile({ ...options, root, imageDir, exec, prodPackages, usesToken: npmrcUsesToken(readFileSync(npmrc, 'utf8')) });
133
+ const { dockerfile, dockerignore } = renderPrebuiltDockerfile({ ...options, root, imageDir });
331
134
  const files = [
332
135
  [posix.join(imageDir, 'Dockerfile'), dockerfile],
333
136
  [posix.join(imageDir, 'Dockerfile.dockerignore'), dockerignore],