|
5 | 5 | import fs from "node:fs"; |
6 | 6 | import path from "node:path"; |
7 | 7 | import { fileURLToPath } from "node:url"; |
| 8 | +import { laneResources, laneWeight } from "./lib/docker-e2e-plan.mjs"; |
| 9 | +import { allReleasePathLanes, mainLanes, tailLanes } from "./lib/docker-e2e-scenarios.mjs"; |
8 | 10 |
|
9 | 11 | const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
10 | 12 | const errors = []; |
| 13 | +const packageJson = JSON.parse(readText("package.json")); |
| 14 | +const packageScripts = new Set(Object.keys(packageJson.scripts ?? {})); |
11 | 15 |
|
12 | 16 | function readText(relativePath) { |
13 | 17 | return fs.readFileSync(path.join(ROOT_DIR, relativePath), "utf8"); |
@@ -43,9 +47,67 @@ if (/^\s*(?:COPY|ADD)\s+\.\s+\/app(?:\s|$)/imu.test(dockerfile)) { |
43 | 47 | errors.push("scripts/e2e/Dockerfile: do not copy the source checkout into /app"); |
44 | 48 | } |
45 | 49 |
|
| 50 | +function validateUniqueLanes(label, lanes) { |
| 51 | + const seen = new Set(); |
| 52 | + for (const lane of lanes) { |
| 53 | + if (seen.has(lane.name)) { |
| 54 | + errors.push(`${label}: duplicate Docker E2E lane '${lane.name}'`); |
| 55 | + } |
| 56 | + seen.add(lane.name); |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +function validateLane(label, lane) { |
| 61 | + if (!lane.name || typeof lane.name !== "string") { |
| 62 | + errors.push(`${label}: Docker E2E lane is missing a string name`); |
| 63 | + } |
| 64 | + if (!lane.command || typeof lane.command !== "string") { |
| 65 | + errors.push(`${label}: Docker E2E lane '${lane.name}' is missing a string command`); |
| 66 | + return; |
| 67 | + } |
| 68 | + if (lane.e2eImageKind && lane.e2eImageKind !== "bare" && lane.e2eImageKind !== "functional") { |
| 69 | + errors.push( |
| 70 | + `${label}: Docker E2E lane '${lane.name}' has invalid image kind '${lane.e2eImageKind}'`, |
| 71 | + ); |
| 72 | + } |
| 73 | + if (lane.live && lane.e2eImageKind) { |
| 74 | + errors.push(`${label}: live Docker E2E lane '${lane.name}' must not require a package image`); |
| 75 | + } |
| 76 | + if (!lane.live && !lane.e2eImageKind) { |
| 77 | + errors.push(`${label}: package Docker E2E lane '${lane.name}' must declare an e2e image kind`); |
| 78 | + } |
| 79 | + if (laneWeight(lane) < 1) { |
| 80 | + errors.push(`${label}: Docker E2E lane '${lane.name}' must have positive weight`); |
| 81 | + } |
| 82 | + if (!laneResources(lane).includes("docker")) { |
| 83 | + errors.push(`${label}: Docker E2E lane '${lane.name}' must include the docker resource`); |
| 84 | + } |
| 85 | + |
| 86 | + for (const match of lane.command.matchAll(/\bpnpm\s+([^\s]+)/gu)) { |
| 87 | + const script = match[1]; |
| 88 | + if (!packageScripts.has(script)) { |
| 89 | + errors.push( |
| 90 | + `${label}: Docker E2E lane '${lane.name}' references missing package script '${script}'`, |
| 91 | + ); |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +const releasePathLanes = allReleasePathLanes({ includeOpenWebUI: true }); |
| 97 | +for (const [label, lanes] of [ |
| 98 | + ["release-path", releasePathLanes], |
| 99 | + ["main", mainLanes], |
| 100 | + ["tail", tailLanes], |
| 101 | +]) { |
| 102 | + validateUniqueLanes(label, lanes); |
| 103 | + for (const lane of lanes) { |
| 104 | + validateLane(label, lane); |
| 105 | + } |
| 106 | +} |
| 107 | + |
46 | 108 | if (errors.length > 0) { |
47 | 109 | console.error(errors.join("\n")); |
48 | 110 | process.exit(1); |
49 | 111 | } |
50 | 112 |
|
51 | | -console.log("Docker E2E package boundary guard passed."); |
| 113 | +console.log("Docker E2E package boundary/catalog guard passed."); |
0 commit comments