@xemahq/repo-build-tooling 0.26.0 → 0.26.2
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 +2 -2
- package/package.json +2 -2
- package/src/composed-image-install.mjs +7 -1
- package/src/composed-image-install.test.mjs +1 -0
- package/src/composed-image-prebuilt-dockerfile.mjs +2 -2
- package/src/composed-image-proof.mjs +7 -6
- package/src/composed-image-proof.test.mjs +13 -0
- package/src/composed-image-stage-tree.mjs +1 -1
- package/src/service-image-stage.mjs +24 -14
- package/src/service-image.test.mjs +23 -13
- package/src/staged-image-proof.mjs +5 -3
package/README.md
CHANGED
|
@@ -39,9 +39,9 @@ pnpm exec xema-scrub-swagger-paths dist
|
|
|
39
39
|
|
|
40
40
|
`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).
|
|
41
41
|
|
|
42
|
-
`dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image that only COPIES a tree the CI runner staged: no `RUN pnpm install`, no corepack, no secret, no network (every RUN is `--network=none`). It requires `--build-source`, `--base <ref@sha256:…>` (the digest-pinned image the stages start FROM; a reference without a digest is refused) and `--node-major <n>` (that base's Node major, rendered as the `dev.xema.composed-image.node-major` label, which the stage compares with the runner's Node); `--copy` and `--stage-dir` are optional and `--cache-id` is refused (there is no cache). The proof stage COPIES the root build inputs (`pnpm-lock.yaml`, root `package.json`, `pnpm-workspace.yaml`, `.npmrc`, `.pnpmfile.cjs`, `turbo.json`, `tsconfig*.json`, whichever exist) into `/proof/context/` and the `--copy` directories into `/proof/copied/`, which puts
|
|
42
|
+
`dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image that only COPIES a tree the CI runner staged: no `RUN pnpm install`, no corepack, no secret, no network (every RUN is `--network=none`). It requires `--build-source`, `--base <ref@sha256:…>` (the digest-pinned image the stages start FROM; a reference without a digest is refused) and `--node-major <n>` (that base's Node major, rendered as the `dev.xema.composed-image.node-major` label, which the stage compares with the runner's Node); `--copy` and `--stage-dir` are optional and `--cache-id` is refused (there is no cache). The proof stage COPIES the root build inputs (`pnpm-lock.yaml`, root `package.json`, `pnpm-workspace.yaml`, `.npmrc`, `.pnpmfile.cjs`, `turbo.json`, `tsconfig*.json`, whichever exist) into `/proof/context/` and the `--copy` directories into `/proof/copied/`, which puts the inputs that decided the staged tree in the image's content tag, and runs `prove`, `prisma-check` and `load --each`; the final stage COPIES the two staged layers and the proof's verdict. The `--filter <package>...` words in the `prove` line are literal: the content hash reads them to narrow the lockfile.
|
|
43
43
|
|
|
44
|
-
`stage-tree <dir> --platform linux/<amd64|arm64> [--output <rel>]…` is the runner half, run after the build and the job's own `pnpm install --frozen-lockfile`. It checks the members' build outputs (as `stage` does), then runs an OFFLINE, frozen, `--ignore-scripts` production install of the members' closures into a staging tree from the job's pnpm store, selected for the target platform (`--os=linux --cpu=<arch> --libc=glibc`, workspace packages injected, the re-resolved lockfile checked to pin nothing new); copies in only each package's `package.json` and declared `files` (plus the built outputs among them), the `--copy` directories, the biome manifests and `image-build-inputs.json`; writes the member index and host link; fetches the platform's Prisma schema engine (
|
|
44
|
+
`stage-tree <dir> --platform linux/<amd64|arm64> [--output <rel>]…` is the runner half, run after the build and the job's own `pnpm install --frozen-lockfile`. It checks the members' build outputs (as `stage` does), then runs an OFFLINE, frozen, `--ignore-scripts` production install of the members' closures into a staging tree from the job's pnpm store, selected for the target platform (`--os=linux --cpu=<arch> --libc=glibc`, workspace packages injected, the re-resolved lockfile checked to pin nothing new); copies in only each package's `package.json` and declared `files` (plus the built outputs among them), the `--copy` directories, the biome manifests and `image-build-inputs.json`; writes the member index and host link; fetches the platform's Prisma schema engine (checked against a sha256 COMMITTED in this tool per engines commit and binaryTarget, never against the checksum Prisma publishes beside it; an engines commit or target not in the table is refused; never cached) when the closure carries the prisma CLI; and refuses a dependency that needs its build script on the target, a package whose native code holds no ELF for the platform, an uncommitted change to a tracked file and an untracked root build input. Output: `<dir>/layers/store` (the registry packages, stable until the lockfile changes), `<dir>/layers/app` (everything else), `<dir>/proof/xema-composed-image.json` (commit, sha256 of each root build input and `--copy` directory, the literal filters, members, natives, engine, tool version, platform) and `<dir>/tool`. `--also` is not accepted. A refused stage leaves nothing behind. The in-image `prove` binds the stage to the build: it refuses unless the manifest names `BUILD_SHA`, the context's root build inputs and `--copy` directories, this tool version, the platform the build runs on and the Dockerfile's filters, and every addon loads and the engine runs there. It does not detect a stage altered on the runner after `stage-tree` wrote it (manifest and layers come from the same job); the image digest and provenance cover what ships. The lockfile re-resolve runs `--prefer-offline` (its pins are checked unchanged; the production install is `--offline`). `stage <dir> [--output <rel>]… [--also <pkg>]…` remains as the outputs-only stage (`<dir>/out`, `<dir>/tool`) for per-service prebuilt images that still read it. `pnpm-context <dir> --subject <svc>…` builds the offline `xema-pnpm` context for those images (`staged.json`, `corepack/pnpm.tgz`); it prints `XEMA_PNPM_CONTEXT=<dir>` and `XEMA_PNPM_STORE=<store root>` and refuses a non-empty `<dir>`. `load --each` loads every member in its own process. Run `xema-composed-image` with no arguments for the full usage.
|
|
45
45
|
|
|
46
46
|
Every step refuses rather than skips. The package has no dependencies.
|
|
47
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/repo-build-tooling",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
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)",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"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/prepush-sanity.test.mjs && node --test src/service-image.test.mjs && node --test src/install-git-hooks.test.mjs"
|
|
55
55
|
},
|
|
56
56
|
"xemaRelease": {
|
|
57
|
-
"sourceDigest": "sha256:
|
|
57
|
+
"sourceDigest": "sha256:3968389805a3386f7699a5663964650254125b4277fe1c89a740296b1a66ebef"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -14,7 +14,13 @@ import { assertPinsUnchanged } from './composed-image-in-image.mjs';
|
|
|
14
14
|
/** The install-flag words that make pnpm copy workspace packages into the store layout. */
|
|
15
15
|
export const injectFlags = () => ['--config.inject-workspace-packages=true', '--config.dedupe-injected-deps=false'];
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* The `--lockfile-only` re-resolve for `filters` (`['--filter', '<pkg>...', ...]`) as `[file, ...args]`.
|
|
19
|
+
* `--prefer-offline`, not `--offline`: the re-resolve needs registry metadata for ranges the committed lockfile
|
|
20
|
+
* already pins, and a runner's metadata cache is cold or stale (a frozen install does not fill it), so `--offline`
|
|
21
|
+
* fails with ERR_PNPM_NO_OFFLINE_META. Registry access here cannot change what ships: `assertPinsUnchanged` fails
|
|
22
|
+
* the stage if the re-resolve moves any pin, and the production install after it is `--offline --frozen-lockfile`.
|
|
23
|
+
*/
|
|
18
24
|
export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-only', '--prefer-offline', ...injectFlags(), ...filters];
|
|
19
25
|
|
|
20
26
|
/** The production install for `filters` on `linux/<arch>` as `[file, ...args]`. */
|
|
@@ -25,6 +25,7 @@ test('the re-resolve then the production install run in the tree, offline and fr
|
|
|
25
25
|
const install = calls[1].line.join(' ');
|
|
26
26
|
for (const word of ['install --prod --offline --ignore-scripts', '--frozen-lockfile', '--os=linux --cpu=arm64 --libc=glibc', '--config.inject-workspace-packages=true', '--filter @t/a... --filter @t/b...']) assert.ok(install.includes(word), word);
|
|
27
27
|
assert.doesNotMatch(install, /--prefer-offline/);
|
|
28
|
+
assert.ok(calls[0].line.includes('--prefer-offline') && !calls[0].line.includes('--offline'));
|
|
28
29
|
assert.ok(calls[0].line.includes('--lockfile-only') && !calls[0].line.includes('--frozen-lockfile'));
|
|
29
30
|
});
|
|
30
31
|
|
|
@@ -83,9 +83,9 @@ export function renderPrebuiltDockerfile({ root = process.cwd(), imageDir = COMP
|
|
|
83
83
|
#
|
|
84
84
|
# Stages:
|
|
85
85
|
# proof The staged tree and the repository's inputs meet. The root build inputs that decide the
|
|
86
|
-
# staged
|
|
86
|
+
# staged tree are COPIED into \`/${PROOF_DIRECTORY}/${CONTEXT_DIRECTORY}/\` and the \`--copy\` directories into
|
|
87
87
|
# \`/${COPIED_DIRECTORY}/\`, which puts them in the image's content tag. \`prove\` refuses the build unless the
|
|
88
|
-
# stage
|
|
88
|
+
# stage's manifest names BUILD_SHA and those bytes, this tool and this platform, and
|
|
89
89
|
# the literal \`--filter\` words below are the ones it staged. Then the Prisma clients run and every
|
|
90
90
|
# member loads ALONE as \`node\`, production, on the target arch.
|
|
91
91
|
# (final) the same two COPYs of the staged tree, plus the proof's verdict, so the image cannot exist without it.
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
// with the staged tree at <root>, the stage manifest in <proof dir>, the repository's root build inputs COPYed
|
|
4
4
|
// into <proof dir>/context and its `--copy` directories into <proof dir>/copied.
|
|
5
5
|
//
|
|
6
|
-
// The image's content tag is computed from the build context, not from the stage, so the stage must be
|
|
7
|
-
// to
|
|
8
|
-
// the
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// The image's content tag is computed from the build context, not from the stage, so the stage must be BOUND
|
|
7
|
+
// to the build context. `prove` binds it; it does NOT detect a stage altered on the runner after `stage-tree`
|
|
8
|
+
// wrote it (the manifest and the layers come from the same job). It refuses the image unless: the stage claims
|
|
9
|
+
// <commit> (the BUILD_SHA the image is built for, so a stale stage cannot ship); the root build inputs and
|
|
10
|
+
// `--copy` directories in the build context are the ones the stage recorded hashes of (so the Dockerfile COPYs
|
|
11
|
+
// every one of them and the content tag covers them); the `--filter` words the Dockerfile carries (the ones the content hash reads
|
|
11
12
|
// to narrow the lockfile) are the members' filters the stage installed; this tool is the version the stage
|
|
12
13
|
// recorded; this process runs on the manifest's platform; the Node ABI is the stager's when the tree carries
|
|
13
14
|
// addons; every package's addons load here; Prisma computes, HERE, the binaryTarget whose schema engine was
|
|
14
|
-
// staged, and that engine has the
|
|
15
|
+
// staged, and that engine has the sha256 the manifest records and reports the pinned version; every member's dist/main.js
|
|
15
16
|
// exists. It then writes <proof dir>/xema-composed-verified.json, which the final stage ships, so the image
|
|
16
17
|
// cannot be built without the proof.
|
|
17
18
|
//
|
|
@@ -93,3 +93,16 @@ test('the Prisma schema engine is checked against the staged sha256 and version'
|
|
|
93
93
|
const stage = proofStage({ prisma: { enginesVersion: 'v1', binaryTarget: 'debian-openssl-3.0.x', sha256: 'f'.repeat(64) } });
|
|
94
94
|
await assert.rejects(prove(stage), /the stage staged a schema engine, but the app carries no prisma CLI/);
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
test('hashDirectory covers symlinks by path and target', () => {
|
|
98
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xema-hash-'));
|
|
99
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'a');
|
|
100
|
+
const bare = hashDirectory(dir);
|
|
101
|
+
fs.symlinkSync('a.txt', path.join(dir, 'link'));
|
|
102
|
+
const linked = hashDirectory(dir);
|
|
103
|
+
assert.notEqual(linked, bare);
|
|
104
|
+
fs.rmSync(path.join(dir, 'link'));
|
|
105
|
+
fs.symlinkSync('elsewhere', path.join(dir, 'link'));
|
|
106
|
+
assert.notEqual(hashDirectory(dir), linked);
|
|
107
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// package.json + declared `files` (the built outputs among them), the injected workspace
|
|
6
6
|
// copies, the member index and host link, the migrate script
|
|
7
7
|
// <stage>/layers/store/monorepo/node_modules/.pnpm/ the registry packages (stable until the lockfile changes)
|
|
8
|
-
// <stage>/proof/xema-composed-image.json the manifest the in-image `prove`
|
|
8
|
+
// <stage>/proof/xema-composed-image.json the manifest the in-image `prove` binds to BUILD_SHA and the context
|
|
9
9
|
//
|
|
10
10
|
// The install is the runner's OFFLINE production install of the members' closures from the pnpm store the job's own
|
|
11
11
|
// `pnpm install` filled (`composed-image-install.mjs`), into this staging tree rather than into the job's node_modules,
|
|
@@ -91,27 +91,37 @@ async function get(url) {
|
|
|
91
91
|
return Buffer.from(await response.arrayBuffer());
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The sha256 of the schema engine, by engines commit (the lockfile's @prisma/engines-version) and
|
|
96
|
+
* binaryTarget. COMMITTED here, reviewed with the tool: the `.sha256` Prisma publishes next to the
|
|
97
|
+
* binary is served by the same host as the binary, so it is no check on it. An engines commit or
|
|
98
|
+
* binaryTarget not in this table is refused; add its row (computed from a binary you trust) when the
|
|
99
|
+
* lockfile moves to a new engines commit.
|
|
100
|
+
*/
|
|
101
|
+
export const SCHEMA_ENGINE_SHA256 = {
|
|
102
|
+
'3c6e192761c0362d496ed980de936e2f3cebcd3a': {
|
|
103
|
+
'debian-openssl-3.0.x': '94eeddaaa41a25561feeefa56c054c95bc59cf0b219a86189afccf400adf8a8e',
|
|
104
|
+
'linux-arm64-openssl-3.0.x': '5161008220716d697e81f4b7132a0a4b4be6e5be5675a94e937c24b279e637f8',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
100
107
|
|
|
101
108
|
/**
|
|
102
109
|
* The schema engine at `enginesVersion` (the engines commit the lockfile pins, through the
|
|
103
110
|
* deploy's @prisma/engines-version) for `binaryTarget`, fetched from Prisma for every stage and
|
|
104
|
-
* written to `enginesDir`.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
111
|
+
* written to `enginesDir`. The unzipped engine must have the sha256 `checksums` commits for
|
|
112
|
+
* (enginesVersion, binaryTarget); a pair it does not list is refused before any download. There is
|
|
113
|
+
* no cache: Prisma's own downloader keeps one across runs in the home directory, honours mirror
|
|
114
|
+
* variables and retries, so it is not used.
|
|
107
115
|
*/
|
|
108
|
-
export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVersion }) {
|
|
116
|
+
export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVersion, checksums = SCHEMA_ENGINE_SHA256 }) {
|
|
109
117
|
if (!/^[a-f0-9]{40}$/.test(enginesVersion)) throw new Error(`@prisma/engines-version ${enginesVersion} is not an engines commit`);
|
|
118
|
+
const expected = checksums[enginesVersion]?.[binaryTarget];
|
|
119
|
+
if (expected === undefined) {
|
|
120
|
+
throw new Error(`no committed sha256 for the Prisma schema engine at engines commit ${enginesVersion} for ${binaryTarget}: add it to SCHEMA_ENGINE_SHA256 in service-image-stage.mjs`);
|
|
121
|
+
}
|
|
110
122
|
const url = `${PRISMA_ENGINES_URL}/${enginesVersion}/${binaryTarget}/schema-engine`;
|
|
111
|
-
const
|
|
112
|
-
if (sha256(
|
|
113
|
-
const engine = gunzipSync(zipped);
|
|
114
|
-
if (sha256(engine) !== engineSha256) throw new Error(`${url} has sha256 ${sha256(engine)}, Prisma publishes ${engineSha256}`);
|
|
123
|
+
const engine = gunzipSync(await get(`${url}.gz`));
|
|
124
|
+
if (sha256(engine) !== expected) throw new Error(`${url}.gz unzips to sha256 ${sha256(engine)}, the committed sha256 is ${expected}`);
|
|
115
125
|
const file = join(enginesDir, `schema-engine-${binaryTarget}`);
|
|
116
126
|
writeFileSync(file, engine);
|
|
117
127
|
chmodSync(file, 0o755);
|
|
@@ -13,7 +13,7 @@ import { copyTool } from './composed-image-stage.mjs';
|
|
|
13
13
|
import { parseStageArgs } from './service-image.mjs';
|
|
14
14
|
import { parseProveArgs, prove } from './service-image-in-image.mjs';
|
|
15
15
|
import { IMAGE_PLATFORMS, currentPlatform, nativeFormat, platformArch, scanNatives } from './service-image-platform.mjs';
|
|
16
|
-
import { PRISMA_ENGINES_URL, deployArgs, downloadSchemaEngine, pnpmEntryName, stageService } from './service-image-stage.mjs';
|
|
16
|
+
import { PRISMA_ENGINES_URL, SCHEMA_ENGINE_SHA256, deployArgs, downloadSchemaEngine, pnpmEntryName, stageService } from './service-image-stage.mjs';
|
|
17
17
|
|
|
18
18
|
const temps = [];
|
|
19
19
|
after(() => {
|
|
@@ -256,11 +256,12 @@ test('stage fails when the downloader produces no engine', async () => {
|
|
|
256
256
|
);
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
-
test('downloadSchemaEngine fetches the pinned engine, checks
|
|
259
|
+
test('downloadSchemaEngine fetches the pinned engine, checks it against the COMMITTED sha256 and trusts no published checksum', async (t) => {
|
|
260
260
|
const engine = elf(183);
|
|
261
|
-
const zipped = gzipSync(engine);
|
|
262
261
|
const version = 'b'.repeat(40);
|
|
263
|
-
const
|
|
262
|
+
const target = 'linux-arm64-openssl-3.0.x';
|
|
263
|
+
const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine`;
|
|
264
|
+
const checksums = { [version]: { [target]: sha256(engine) } };
|
|
264
265
|
const serve = (files) => {
|
|
265
266
|
const requested = [];
|
|
266
267
|
t.mock.method(globalThis, 'fetch', async (asked) => {
|
|
@@ -270,24 +271,33 @@ test('downloadSchemaEngine fetches the pinned engine, checks both published chec
|
|
|
270
271
|
});
|
|
271
272
|
return requested;
|
|
272
273
|
};
|
|
273
|
-
const published = { [`${url}.gz`]: zipped, [`${url}.gz.sha256`]: `${sha256(zipped)} ./x/schema-engine.gz\n`, [`${url}.sha256`]: `${sha256(engine)} ./x/schema-engine\n` };
|
|
274
274
|
const enginesDir = temp();
|
|
275
|
-
const requested = serve(
|
|
276
|
-
await downloadSchemaEngine({ enginesDir, binaryTarget:
|
|
277
|
-
const file = path.join(enginesDir,
|
|
275
|
+
const requested = serve({ [`${url}.gz`]: gzipSync(engine) });
|
|
276
|
+
await downloadSchemaEngine({ enginesDir, binaryTarget: target, enginesVersion: version, checksums });
|
|
277
|
+
const file = path.join(enginesDir, `schema-engine-${target}`);
|
|
278
278
|
assert.deepEqual(fs.readFileSync(file), engine);
|
|
279
279
|
assert.equal(fs.statSync(file).mode & 0o777, 0o755);
|
|
280
|
-
assert.deepEqual(requested
|
|
280
|
+
assert.deepEqual(requested, [`${url}.gz`]);
|
|
281
281
|
t.mock.restoreAll();
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
// A binary whose published .sha256 agrees with it is still refused when it is not the committed one.
|
|
284
|
+
const other = elf(184);
|
|
285
|
+
serve({ [`${url}.gz`]: gzipSync(other), [`${url}.sha256`]: `${sha256(other)} x\n` });
|
|
286
|
+
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }), /unzips to sha256 .*, the committed sha256 is /);
|
|
285
287
|
t.mock.restoreAll();
|
|
286
|
-
serve({
|
|
287
|
-
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget:
|
|
288
|
+
const unfetched = serve({});
|
|
289
|
+
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: 'c'.repeat(40), checksums }), /no committed sha256 .* engines commit c{40} for linux-arm64/);
|
|
290
|
+
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'debian-openssl-3.0.x', enginesVersion: version, checksums }), /no committed sha256/);
|
|
291
|
+
assert.deepEqual(unfetched, []);
|
|
288
292
|
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'x', enginesVersion: '1.2.3' }), /is not an engines commit/);
|
|
289
293
|
});
|
|
290
294
|
|
|
295
|
+
test('the committed engine table has the amd64 and arm64 rows for the engines commit xema-base pins', () => {
|
|
296
|
+
const rows = SCHEMA_ENGINE_SHA256['3c6e192761c0362d496ed980de936e2f3cebcd3a'];
|
|
297
|
+
assert.deepEqual(Object.keys(rows).sort(), ['debian-openssl-3.0.x', 'linux-arm64-openssl-3.0.x']);
|
|
298
|
+
for (const hex of Object.values(rows)) assert.match(hex, /^[a-f0-9]{64}$/);
|
|
299
|
+
});
|
|
300
|
+
|
|
291
301
|
// ── prove ─────────────────────────────────────────────────────
|
|
292
302
|
|
|
293
303
|
async function staged({ prisma = false } = {}) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// The checks every runner-staged image shares (`xema-service-image`, `xema-composed-image`): the proof stage
|
|
2
2
|
// runs them on the TARGET platform, `--network=none`, against the stage manifest the runner wrote. The image's
|
|
3
|
-
// content tag is computed from the build context, never from the stage, so each check
|
|
4
|
-
//
|
|
3
|
+
// content tag is computed from the build context, never from the stage, so each check binds the stage's
|
|
4
|
+
// manifest to what the context (and so the tag) covers. They do not detect a stage altered on the runner: the
|
|
5
|
+
// manifest and the layers are written by the same job.
|
|
5
6
|
//
|
|
6
7
|
// Loads only `node:` builtins and relative modules that do too: the in-image proofs import it from the
|
|
7
8
|
// bind-mounted tool directory (Prisma's own modules are resolved from the staged tree).
|
|
8
9
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
10
11
|
import { createRequire } from 'node:module';
|
|
11
12
|
import { dirname, join } from 'node:path';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -53,6 +54,7 @@ export function hashDirectory(dir) {
|
|
|
53
54
|
const rel = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
54
55
|
if (entry.isDirectory()) walk(join(current, entry.name), rel);
|
|
55
56
|
else if (entry.isFile()) hash.update(`${rel}\0${sha256File(join(current, entry.name))}\n`);
|
|
57
|
+
else if (entry.isSymbolicLink()) hash.update(`${rel}\0->${readlinkSync(join(current, entry.name))}\n`);
|
|
56
58
|
}
|
|
57
59
|
};
|
|
58
60
|
walk(dir, '');
|