@specific.dev/spectest 0.76.1 → 0.77.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/dist/daemon.js +113 -67
- package/dist/harness/build-context.d.ts +35 -11
- package/dist/harness/build-context.js +26 -8
- package/dist/index.d.ts +49 -21
- package/dist/index.js +37 -26
- package/package.json +1 -1
- package/src/daemon.ts +125 -78
- package/src/dockerfile-path.test.ts +61 -18
- package/src/harness/build-context.test.ts +0 -0
- package/src/harness/build-context.ts +54 -15
- package/src/index.ts +82 -36
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// over HTTP.
|
|
6
6
|
|
|
7
7
|
import { strict as nodeAssert } from "node:assert";
|
|
8
|
-
import {
|
|
8
|
+
import { statSync } from "node:fs";
|
|
9
9
|
|
|
10
10
|
import { recordAssertion, safeSerialize } from "./recorder.js";
|
|
11
11
|
import { adoptNullishTag, readRaw, readTag } from "./inspect.js";
|
|
@@ -1138,17 +1138,33 @@ export type ServiceImage =
|
|
|
1138
1138
|
* Path of a Dockerfile in your repo, relative to the project root
|
|
1139
1139
|
* (where `spectest/` lives) — `"Dockerfile"`, `"apps/api/Dockerfile"`.
|
|
1140
1140
|
* This is the preferred form: the test environment builds the same
|
|
1141
|
-
* Dockerfile production does, so the two cannot drift. The
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1141
|
+
* Dockerfile production does, so the two cannot drift. The file is
|
|
1142
|
+
* read from the repo at build time; a missing file fails the
|
|
1143
|
+
* environment load and names the path. Mutually exclusive with
|
|
1144
|
+
* `content`.
|
|
1145
|
+
*
|
|
1146
|
+
* Ignore rules come from the repo the way `docker build` reads them:
|
|
1147
|
+
* a `<Dockerfile>.dockerignore` beside this file, else the context
|
|
1148
|
+
* directory's `.dockerignore`. `exclude` adds to whichever applied.
|
|
1147
1149
|
*/
|
|
1148
1150
|
path: string;
|
|
1149
1151
|
content?: never;
|
|
1152
|
+
/**
|
|
1153
|
+
* Build context directory, relative to the project root. Defaults to
|
|
1154
|
+
* the project root, as with `docker build -f apps/api/Dockerfile .`.
|
|
1155
|
+
* `"apps/api"` is `docker build apps/api`: `COPY` / `ADD` resolve
|
|
1156
|
+
* against that directory, and so do the ignore rules.
|
|
1157
|
+
*/
|
|
1158
|
+
context?: string;
|
|
1150
1159
|
/** Extra glob patterns to exclude from the build context. */
|
|
1151
1160
|
exclude?: readonly string[];
|
|
1161
|
+
/**
|
|
1162
|
+
* Stage to build, as `docker build --target <stage>`. Lets a
|
|
1163
|
+
* production Dockerfile keep a final stage the tests never need (an
|
|
1164
|
+
* ops layer, a slow install) while the environment stops at the
|
|
1165
|
+
* stage before it.
|
|
1166
|
+
*/
|
|
1167
|
+
target?: string;
|
|
1152
1168
|
/**
|
|
1153
1169
|
* `--build-arg` values for the Dockerfile's `ARG`s — `{ NODE_ENV: "test" }`.
|
|
1154
1170
|
* Build-time only: they are not in the container's environment (use
|
|
@@ -1161,16 +1177,24 @@ export type ServiceImage =
|
|
|
1161
1177
|
| {
|
|
1162
1178
|
type: "dockerfile";
|
|
1163
1179
|
/**
|
|
1164
|
-
* Dockerfile contents,
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1180
|
+
* Dockerfile contents, built as if the file sat outside the build
|
|
1181
|
+
* context (it is never written into your repo). The context is the
|
|
1182
|
+
* project root (where `spectest/` lives) unless `context` says
|
|
1183
|
+
* otherwise, and `COPY` / `ADD` resolve against it. Prefer `path`,
|
|
1184
|
+
* which points at the Dockerfile your repo already has. Mutually
|
|
1185
|
+
* exclusive with `path`.
|
|
1169
1186
|
*/
|
|
1170
1187
|
content: string;
|
|
1171
1188
|
path?: never;
|
|
1189
|
+
/**
|
|
1190
|
+
* Build context directory, relative to the project root. Defaults to
|
|
1191
|
+
* the project root. The context's own `.dockerignore` applies.
|
|
1192
|
+
*/
|
|
1193
|
+
context?: string;
|
|
1172
1194
|
/** Extra glob patterns to exclude from the build context. */
|
|
1173
1195
|
exclude?: readonly string[];
|
|
1196
|
+
/** Stage to build, as `docker build --target <stage>`. */
|
|
1197
|
+
target?: string;
|
|
1174
1198
|
/**
|
|
1175
1199
|
* `--build-arg` values for the Dockerfile's `ARG`s — `{ NODE_ENV: "test" }`.
|
|
1176
1200
|
* Build-time only: they are not in the container's environment (use
|
|
@@ -1203,20 +1227,27 @@ function validateBuildArgs(serviceName: string, buildArgs: Readonly<Record<strin
|
|
|
1203
1227
|
}
|
|
1204
1228
|
}
|
|
1205
1229
|
|
|
1230
|
+
/** A Dockerfile stage name, as `--target` accepts it: no whitespace. */
|
|
1231
|
+
const BUILD_TARGET_RE = /^[^\s]+$/;
|
|
1232
|
+
|
|
1206
1233
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1234
|
+
* Check a service's `image` at config time, so a mistake fails the
|
|
1235
|
+
* environment load with a message that names it, instead of a build
|
|
1236
|
+
* minutes later. Nothing is rewritten: a `path` Dockerfile stays a path,
|
|
1237
|
+
* and the harness reads it — and the ignore file beside it — from the
|
|
1238
|
+
* repo when it builds (`daemon.ts::resolveDockerfileBuild`). The wire
|
|
1239
|
+
* config therefore carries `path`, `context` and `target` as written; the
|
|
1240
|
+
* control plane only tells a Dockerfile build from a registry pull, and
|
|
1241
|
+
* the warm-template hash covers the files because they are part of the
|
|
1242
|
+
* project tree.
|
|
1212
1243
|
*
|
|
1213
|
-
* Runs
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1244
|
+
* Runs in `defineEnvironment`, and in the daemon's runtime `startService`
|
|
1245
|
+
* twin, inside the VM, where the repo sits at the project root. Existence
|
|
1246
|
+
* checks go through the project-file resolver, so the same rules as
|
|
1247
|
+
* `ctx.readProjectFile` apply (a `spectest/.envignore`d path is refused
|
|
1217
1248
|
* instead of read stale).
|
|
1218
1249
|
*/
|
|
1219
|
-
export function
|
|
1250
|
+
export function validateServiceImage(serviceName: string, image: ServiceImage): ServiceImage {
|
|
1220
1251
|
if (image.type !== "dockerfile") return image;
|
|
1221
1252
|
validateBuildArgs(serviceName, image.buildArgs);
|
|
1222
1253
|
const hasPath = typeof image.path === "string";
|
|
@@ -1231,20 +1262,35 @@ export function resolveServiceImage(serviceName: string, image: ServiceImage): S
|
|
|
1231
1262
|
`service "${serviceName}" image { type: "dockerfile" } needs \`path\` (a Dockerfile in your repo, relative to the project root) or \`content\``,
|
|
1232
1263
|
);
|
|
1233
1264
|
}
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1265
|
+
if (hasPath) {
|
|
1266
|
+
const file = resolveExistingProjectPath(image.path as string, `service "${serviceName}" Dockerfile`);
|
|
1267
|
+
if (!statSync(file).isFile()) {
|
|
1268
|
+
throw new Error(
|
|
1269
|
+
`service "${serviceName}" Dockerfile ${JSON.stringify(image.path)} is not a file — \`path\` names the Dockerfile itself, \`context\` names the directory to build from`,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (image.context !== undefined) {
|
|
1274
|
+
if (typeof image.context !== "string" || image.context === "") {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
`service "${serviceName}" image context must be a directory path relative to the project root (\`"."\` for the root)`,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
const dir = resolveExistingProjectPath(image.context, `service "${serviceName}" build context`);
|
|
1280
|
+
if (!statSync(dir).isDirectory()) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
`service "${serviceName}" build context ${JSON.stringify(image.context)} is not a directory`,
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (image.target !== undefined) {
|
|
1287
|
+
if (typeof image.target !== "string" || !BUILD_TARGET_RE.test(image.target)) {
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
`service "${serviceName}" image target must be a Dockerfile stage name (\`FROM … AS <name>\`); got ${JSON.stringify(image.target)}`,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1243
1292
|
}
|
|
1244
|
-
|
|
1245
|
-
if (image.exclude !== undefined) lowered.exclude = image.exclude;
|
|
1246
|
-
if (image.buildArgs !== undefined) lowered.buildArgs = image.buildArgs;
|
|
1247
|
-
return lowered;
|
|
1293
|
+
return image;
|
|
1248
1294
|
}
|
|
1249
1295
|
|
|
1250
1296
|
// Coverage adapters live in `coverage.ts`; the type is re-declared here
|
|
@@ -2389,7 +2435,7 @@ export function defineEnvironment<
|
|
|
2389
2435
|
for (const [key, svc] of Object.entries(expanded)) {
|
|
2390
2436
|
expanded[key] = applyCoverageAdapters(key, {
|
|
2391
2437
|
...svc,
|
|
2392
|
-
image:
|
|
2438
|
+
image: validateServiceImage(key, svc.image),
|
|
2393
2439
|
});
|
|
2394
2440
|
}
|
|
2395
2441
|
const config: EnvironmentConfig<S> = {
|