From 9eaa1016fb4dad43d93ebbefcd9fc4c79e393aa1 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Thu, 8 Oct 2026 20:10:15 +0100 Subject: [PATCH] Enforce import boundaries with Biome Turn on suspicious/noImportCycles and, for packages/modules, style/noRestrictedImports so modules only import core through the testcontainers entry point (the core test helper stays allowed). Suppress the existing GenericContainer <-> reaper/port-forwarder/builder cycle per import, since Ryuk and sshd are GenericContainers and every edge is only used at runtime. Fix the two deep imports in the weaviate and mongodb tests without changing the public API. --- AGENTS.md | 3 ++ biome.json | 31 ++++++++++++++++++- .../src/mongodb-atlas-local-container.test.ts | 2 +- .../weaviate/src/weaviate-container.test.ts | 3 +- .../generic-container-builder.ts | 2 ++ .../generic-container/generic-container.ts | 4 +++ .../started-generic-container.ts | 1 + .../src/port-forwarder/port-forwarder.ts | 2 ++ packages/testcontainers/src/reaper/reaper.ts | 1 + 9 files changed, 45 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 043db2c2d..dd9a51eaa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,9 @@ It captures practical rules that prevent avoidable CI and PR churn. - Run the focused test against the pre-fix implementation and confirm it fails for the expected reason. - Apply the implementation change, rerun the same test, and confirm it passes. - Report the red-green evidence in the PR verification summary. +- Import boundaries are enforced by Biome (`biome.json`): + - Modules import core only from the `testcontainers` entry point. The one allowed exception is the core test helper (`../../../testcontainers/src/utils/test-helper`). + - `noImportCycles` is an error. Prefer breaking a new cycle (for example with `import type` or a leaf module) over adding a `biome-ignore` suppression. - Test-only helper files under `src` (for example `*-test-utils.ts`) must be explicitly excluded from package `tsconfig.build.json` so they are not emitted into `build` and accidentally published. - For substantial changes to GitHub Actions, runner images, Node/npm versions, or release/publish automation, consider running the manual `Node.js Package` workflow as a dry-run publish sanity check. - Select the PR branch as the workflow ref to test publish workflow changes before merging. diff --git a/biome.json b/biome.json index f54aac69e..ea444d964 100644 --- a/biome.json +++ b/biome.json @@ -22,9 +22,38 @@ "linter": { "enabled": true, "rules": { - "preset": "recommended" + "preset": "recommended", + "suspicious": { + "noImportCycles": "error" + } } }, + "overrides": [ + { + "includes": ["packages/modules/**"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": [ + "testcontainers/**", + "**/testcontainers/src/**", + "!**/testcontainers/src/utils/test-helper" + ], + "message": "Modules must import from the public \"testcontainers\" entry point, not its internals." + } + ] + } + } + } + } + } + } + ], "assist": { "enabled": true, "actions": { diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts index fa6dd7a3e..4dfb1a228 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -1,5 +1,5 @@ import mongoose from "mongoose"; -import { IntervalRetry } from "../../../testcontainers/src/common"; +import { IntervalRetry } from "testcontainers"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; diff --git a/packages/modules/weaviate/src/weaviate-container.test.ts b/packages/modules/weaviate/src/weaviate-container.test.ts index 5db9d65ea..dee64e9e7 100644 --- a/packages/modules/weaviate/src/weaviate-container.test.ts +++ b/packages/modules/weaviate/src/weaviate-container.test.ts @@ -1,4 +1,3 @@ -import type { Environment } from "testcontainers/src/types"; import weaviate from "weaviate-ts-client"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { WeaviateContainer } from "./weaviate-container"; @@ -38,7 +37,7 @@ describe("WeaviateContainer", { timeout: 100_000 }, () => { "text2vec-huggingface", "generative-openai", ]; - const environment: Environment = { + const environment = { ENABLE_MODULES: enableModules.join(","), BACKUP_FILESYSTEM_PATH: "/tmp/backups", }; diff --git a/packages/testcontainers/src/generic-container/generic-container-builder.ts b/packages/testcontainers/src/generic-container/generic-container-builder.ts index a2778e27a..d06a7867e 100644 --- a/packages/testcontainers/src/generic-container/generic-container-builder.ts +++ b/packages/testcontainers/src/generic-container/generic-container-builder.ts @@ -3,11 +3,13 @@ import type { ImageBuildOptions } from "dockerode"; import { log, RandomUuid, type Uuid } from "../common"; import { getAuthConfig, getContainerRuntimeClient, ImageName } from "../container-runtime"; import type { AuthConfig } from "../container-runtime/auth/types"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper } from "../reaper/reaper"; import type { BuildArgs, RegistryConfig } from "../types"; import { getDockerfileImages } from "../utils/dockerfile-parser"; import { createLabels, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { type ImagePullPolicy, PullPolicy } from "../utils/pull-policy"; +// biome-ignore lint/suspicious/noImportCycles: fromDockerfile() and build() construct each other; only dereferenced at runtime import { GenericContainer } from "./generic-container"; export type BuildOptions = { diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index c44793ff7..80574f001 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -7,7 +7,9 @@ import { containerLog, hash, log, toNanos } from "../common"; import { type ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types"; import type { StartedNetwork } from "../network/network"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { PortForwarderInstance, SSHD_IMAGE } from "../port-forwarder/port-forwarder"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper, getReaperImage } from "../reaper/reaper"; import type { StartedTestContainer, TestContainer } from "../test-container"; import type { @@ -34,8 +36,10 @@ import { type ImagePullPolicy, PullPolicy } from "../utils/pull-policy"; import { selectWaitStrategy } from "../wait-strategies/utils/wait-strategy-selector"; import { waitForContainer } from "../wait-strategies/wait-for-container"; import type { WaitStrategy } from "../wait-strategies/wait-strategy"; +// biome-ignore lint/suspicious/noImportCycles: fromDockerfile() and build() construct each other; only dereferenced at runtime import { GenericContainerBuilder } from "./generic-container-builder"; import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { StartedGenericContainer } from "./started-generic-container"; const reusableContainerCreationLock = new AsyncLock(); diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index e68e7018b..3bc607f82 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -6,6 +6,7 @@ import type Dockerode from "dockerode"; import type { ContainerInspectInfo } from "dockerode"; import { containerLog, log } from "../common"; import { type ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper } from "../reaper/reaper"; import type { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container"; import type { diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder.ts b/packages/testcontainers/src/port-forwarder/port-forwarder.ts index 4eb7bffd7..ba64dc8a8 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder.ts @@ -3,7 +3,9 @@ import type { ContainerInfo } from "dockerode"; import { createSshConnection, type SshConnection } from "ssh-remote-port-forward"; import { log, withFileLock } from "../common"; import { type ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { GenericContainer } from "../generic-container/generic-container"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper } from "../reaper/reaper"; import { LABEL_TESTCONTAINERS_SESSION_ID, LABEL_TESTCONTAINERS_SSHD } from "../utils/labels"; import type { PortWithOptionalBinding } from "../utils/port"; diff --git a/packages/testcontainers/src/reaper/reaper.ts b/packages/testcontainers/src/reaper/reaper.ts index 483313fb9..aa6b1152c 100644 --- a/packages/testcontainers/src/reaper/reaper.ts +++ b/packages/testcontainers/src/reaper/reaper.ts @@ -3,6 +3,7 @@ import { userInfo } from "node:os"; import type { ContainerInfo } from "dockerode"; import { IntervalRetry, log, RandomUuid, withFileLock } from "../common"; import { type ContainerRuntimeClient, ImageName } from "../container-runtime"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { GenericContainer } from "../generic-container/generic-container"; import { LABEL_TESTCONTAINERS_RYUK, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { Wait } from "../wait-strategies/wait";