Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,25 +146,60 @@ jobs:
env:
DEBUG: "testcontainers*"

test:
test-docker:
if: ${{ needs.detect-modules.outputs.modules_count != '0' }}
name: Tests
name: Tests (Docker)
needs:
- detect-modules
- lint
- compile
strategy:
fail-fast: false
max-parallel: 20
matrix:
Comment on lines 156 to 158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cap the split test matrices

On repository-wide changes, package detection selects all 43 workspaces, so these three independent matrices can collectively start up to 215 runner jobs (86 Docker, 86 Podman, and 43 Bun), whereas the previous combined matrix was limited by max-parallel: 20. Restore an aggregate concurrency cap so this workflow cannot monopolize the organization’s runner capacity.

Useful? React with 👍 / 👎.

module: ${{ fromJSON(needs.detect-modules.outputs.modules) }}
node-version: [22.x, 24.x]
container-runtime: [docker, podman]
uses: ./.github/workflows/test-template.yml
with:
runner: ubuntu-24.04
node-version: ${{ matrix.node-version }}
container-runtime: ${{ matrix.container-runtime }}
container-runtime: docker
workspace: "${{ matrix.module }}"

test-podman:
if: ${{ needs.detect-modules.outputs.modules_count != '0' }}
name: Tests (Podman)
needs:
- detect-modules
- lint
- compile
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.detect-modules.outputs.modules) }}
node-version: [22.x, 24.x]
uses: ./.github/workflows/test-template.yml
with:
runner: ubuntu-24.04
node-version: ${{ matrix.node-version }}
container-runtime: podman
workspace: "${{ matrix.module }}"

test-bun:
if: ${{ needs.detect-modules.outputs.modules_count != '0' }}
name: Tests (Bun)
needs:
- detect-modules
- lint
- compile
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.detect-modules.outputs.modules) }}
uses: ./.github/workflows/test-template.yml
with:
runner: ubuntu-24.04
bun-version: canary
container-runtime: docker
workspace: "${{ matrix.module }}"

end:
Expand All @@ -175,7 +210,9 @@ jobs:
- lint
- compile
- smoke-test
- test
- test-docker
- test-podman
- test-bun
runs-on: ubuntu-24.04
steps:
- name: Check if any jobs failed
Expand Down
29 changes: 27 additions & 2 deletions .github/workflows/test-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ on:
required: true
type: string
node-version:
required: true
required: false
type: string
default: 24.x
bun-version:
required: false
type: string
default: ""
container-runtime:
required: true
type: string
Expand All @@ -18,6 +23,7 @@ jobs:
test:
name: "Run"
runs-on: ${{ inputs.runner }}
timeout-minutes: 20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the 20-minute timeout to Bun jobs

This reusable template serves the Docker and Podman matrices as well as Bun, so the new limit also shortens every existing Node job. In a slow run, packages/modules/chromadb/src/chromadb-container.test.ts permits 360 seconds per attempt while vitest.config.ts enables three CI retries, meaning a test can legitimately need up to 24 minutes before setup time; the job can therefore be cancelled during a retry that would otherwise pass. Apply the short timeout only to Bun, where it guards the documented hang, or raise the shared timeout enough to preserve the existing retry budget.

Useful? React with 👍 / 👎.

steps:
- name: Code checkout
uses: actions/checkout@v7
Expand All @@ -34,7 +40,26 @@ jobs:
node-version: ${{ inputs.node-version }}
workspace: "${{ inputs.workspace }}"

- name: Run tests
- name: Install Bun ${{ inputs.bun-version }}
if: ${{ inputs.bun-version != '' }}
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ inputs.bun-version }}

- name: Run tests with Node
if: ${{ inputs.bun-version == '' }}
run: npm run test:ci -- --coverage.include="${{ steps.npm-install.outputs.workspace_path }}/**/*.ts" ${{ steps.npm-install.outputs.workspace_path }}
env:
CI: true

# Vitest's V8 coverage provider requires node:inspector coverage APIs that Bun does not support.
# https://github.com/oven-sh/bun/issues/4145
- name: Run tests with Bun
if: ${{ inputs.bun-version != '' }}
run: bun run --bun vitest run ${{ steps.npm-install.outputs.workspace_path }}
env:
CI: true
BUN_CI: true
# Concurrent Ryuk usage leaves Bun alive after the core suite completes.
# https://github.com/oven-sh/bun/issues/23776
TESTCONTAINERS_RYUK_DISABLED: ${{ inputs.workspace == 'testcontainers' && 'true' || '' }}
2 changes: 2 additions & 0 deletions packages/modules/couchbase/src/couchbase-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const ENTERPRISE_IMAGE = getImage(__dirname, 0);
const COMMUNITY_IMAGE = getImage(__dirname, 1);

describe("CouchbaseContainer", { timeout: 180_000 }, () => {
// https://github.com/oven-sh/bun/issues/12730
if (process.env.BUN_CI) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Bun-safe Couchbase tests enabled

When BUN_CI is set, this suite-wide return also removes the community-edition service validation, analytics startup, and fixed-host-port tests, none of which invoke the incompatible couchbase.Cluster client path. Guard only the client-dependent tests so Bun continues verifying these observable container behaviors.

AGENTS.md reference: AGENTS.md:L29-L30

Useful? React with 👍 / 👎.

const flushBucketAndCheckExists = async (
cluster: Cluster,
bucket: Bucket,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { ElasticsearchContainer } from "./elasticsearch-container";
const IMAGE = getImage(__dirname);
const images = ["elasticsearch:7.17.28", "elasticsearch:8.18.1", IMAGE];

// https://github.com/oven-sh/bun/issues/24824
describe("ElasticsearchContainer", { timeout: 180_000 }, () => {
if (process.env.BUN_CI) return;
it.each(images)("should create an index with %s", async (image) => {
// createIndex {
await using container = await new ElasticsearchContainer(image).start();
Expand Down
97 changes: 50 additions & 47 deletions packages/modules/k3s/src/k3s-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,67 @@ const KUBECTL_IMAGE = getImage(__dirname, 1);
describe("K3sContainer", { timeout: 120_000 }, () => {
// K3sContainer runs as a privileged container
if (!process.env["CI_ROOTLESS"]) {
it("should start and have listable node", async () => {
// k3sListNodes {
await using container = await new K3sContainer(IMAGE).start();
// https://github.com/oven-sh/bun/issues/7332
if (!process.env.BUN_CI)
it("should start and have listable node", async () => {
// k3sListNodes {
await using container = await new K3sContainer(IMAGE).start();

const kubeConfig = new k8s.KubeConfig();
kubeConfig.loadFromString(container.getKubeConfig());
const kubeConfig = new k8s.KubeConfig();
kubeConfig.loadFromString(container.getKubeConfig());

const client = kubeConfig.makeApiClient(k8s.CoreV1Api);
const nodeList = await client.listNode();
const client = kubeConfig.makeApiClient(k8s.CoreV1Api);
const nodeList = await client.listNode();

expect(nodeList.items).toHaveLength(1);
// }
});
expect(nodeList.items).toHaveLength(1);
// }
});

it("should start a pod", async () => {
// k3sStartPod {
await using container = await new K3sContainer(IMAGE).start();
if (!process.env.BUN_CI)
it("should start a pod", async () => {
// k3sStartPod {
await using container = await new K3sContainer(IMAGE).start();

const kubeConfig = new k8s.KubeConfig();
kubeConfig.loadFromString(container.getKubeConfig());
const kubeConfig = new k8s.KubeConfig();
kubeConfig.loadFromString(container.getKubeConfig());

const pod = {
metadata: {
name: "helloworld",
},
spec: {
containers: [
{
name: "helloworld",
image: "testcontainers/helloworld:1.1.0",
ports: [
{
containerPort: 8080,
},
],
readinessProbe: {
tcpSocket: {
port: 8080,
const pod = {
metadata: {
name: "helloworld",
},
spec: {
containers: [
{
name: "helloworld",
image: "testcontainers/helloworld:1.1.0",
ports: [
{
containerPort: 8080,
},
],
readinessProbe: {
tcpSocket: {
port: 8080,
},
},
},
},
],
},
};
],
},
};

const client = kubeConfig.makeApiClient(k8s.CoreV1Api);
await client.createNamespacedPod({ namespace: "default", body: pod });
const client = kubeConfig.makeApiClient(k8s.CoreV1Api);
await client.createNamespacedPod({ namespace: "default", body: pod });

await vi.waitFor(async () => {
const { status } = await client.readNamespacedPodStatus({ namespace: "default", name: "helloworld" });
await vi.waitFor(async () => {
const { status } = await client.readNamespacedPodStatus({ namespace: "default", name: "helloworld" });

return (
status?.phase === "Running" &&
status?.conditions?.some((cond) => cond.type === "Ready" && cond.status === "True")
);
}, 60_000);
// }
});
return (
status?.phase === "Running" &&
status?.conditions?.some((cond) => cond.type === "Ready" && cond.status === "True")
);
}, 60_000);
// }
});

it("should expose kubeconfig for a network alias", async () => {
// k3sAliasedKubeConfig {
Expand Down
2 changes: 2 additions & 0 deletions packages/modules/kafka/src/kafka-container-7.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { assertMessageProducedAndConsumed } from "./test-helper";
const IMAGE = "confluentinc/cp-kafka:7.9.1";

describe("KafkaContainer", { timeout: 240_000 }, () => {
// https://github.com/oven-sh/bun/issues/19337
if (process.env.BUN_CI) return;
Comment on lines +10 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run Kafka tests that do not use the native client

In the Bun job, this suite-wide return also skips the reuse check, Docker-network Kafka CLI check, and unsupported-version validation tests, none of which call assertMessageProducedAndConsumed or load the incompatible native client. Limit the guard to tests that invoke that helper so Bun continues exercising the compatible container behavior.

AGENTS.md reference: AGENTS.md:L29-L30

Useful? React with 👍 / 👎.

it("should connect using in-built zoo-keeper", async () => {
// connectBuiltInZK {
await using container = await new KafkaContainer(IMAGE).start();
Expand Down
2 changes: 2 additions & 0 deletions packages/modules/kafka/src/kafka-container-latest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { assertMessageProducedAndConsumed } from "./test-helper";
const IMAGE = getImage(__dirname);

describe("KafkaContainer", { timeout: 240_000 }, () => {
// https://github.com/oven-sh/bun/issues/19337
if (process.env.BUN_CI) return;
Comment on lines +11 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the Bun-safe Kafka latest checks

When BUN_CI is set, this return suppresses the reuse test and the SASL-in-network test that uses the containerized Kafka CLI; neither path imports the incompatible native Kafka client. Guard only the tests that call assertMessageProducedAndConsumed so these observable behaviors remain covered under Bun.

AGENTS.md reference: AGENTS.md:L29-L30

Useful? React with 👍 / 👎.

const certificatesDir = path.resolve(__dirname, "..", "test-certs");

it("should connect", async () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/modules/kafka/src/test-helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GlobalConfig, KafkaJS } from "@confluentinc/kafka-javascript";
import type { GlobalConfig, KafkaJS } from "@confluentinc/kafka-javascript";
import { StartedKafkaContainer } from "./kafka-container";

// kafkaTestHelper {
Expand All @@ -7,10 +7,12 @@ export async function assertMessageProducedAndConsumed(
additionalKafkaConfig: Partial<KafkaJS.KafkaConfig> = {},
additionalGlobalConfig: Partial<GlobalConfig> = {}
) {
// Static loading crashes Bun before BUN_CI can skip the dependent tests.
const { KafkaJS: KafkaJSClient } = await import("@confluentinc/kafka-javascript");
const brokers = [`${container.getHost()}:${container.getMappedPort(9093)}`];
const kafka = new KafkaJS.Kafka({
const kafka = new KafkaJSClient.Kafka({
kafkaJS: {
logLevel: KafkaJS.logLevel.ERROR,
logLevel: KafkaJSClient.logLevel.ERROR,
brokers,
...additionalKafkaConfig,
},
Expand Down
14 changes: 10 additions & 4 deletions packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import mongoose from "mongoose";
import { IntervalRetry } from "../../../testcontainers/src/common";
import { getImage } from "../../../testcontainers/src/utils/test-helper";
import { MongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container";
Expand All @@ -24,6 +23,9 @@ const ATLAS_SEARCH_INDEX = {
};

describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => {
// Static loading uses node:v8 APIs unsupported by Bun before this suite can return.
// https://github.com/oven-sh/bun/issues/32501
if (process.env.BUN_CI) return;
Comment on lines 25 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain MongoDB Atlas tests that do not import Mongoose

In the Bun matrix, this return skips the three connection-string tests before the first dynamic mongoose import, even though the linked node:v8 incompatibility cannot affect those paths. Guard only the Mongoose-dependent tests so Bun still verifies the container's connection-string behavior.

AGENTS.md reference: AGENTS.md:L29-L30

Useful? React with 👍 / 👎.

it("should provide a connection string", async () => {
// connectAtlasLocal {
await using container = await new MongoDBAtlasLocalContainer(IMAGE).start();
Expand Down Expand Up @@ -61,7 +63,9 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => {
it("should connect to mongodb atlas local", async () => {
await using container = await new MongoDBAtlasLocalContainer(IMAGE).start();

const db = mongoose.createConnection(container.getConnectionString(), { directConnection: true });
const db = (await import("mongoose")).default.createConnection(container.getConnectionString(), {
directConnection: true,
});

const obj = { value: 1 };
const collection = db.collection("test");
Expand All @@ -79,7 +83,9 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => {
.withPassword("customPassword")
.start();

const db = mongoose.createConnection(container.getDatabaseConnectionString(), { directConnection: true });
const db = (await import("mongoose")).default.createConnection(container.getDatabaseConnectionString(), {
directConnection: true,
});

const obj = { value: 1 };
const collection = db.collection("test");
Expand All @@ -95,7 +101,7 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => {
// createAtlasIndexAndSearchIt {
await using atlasLocalContainer = await new MongoDBAtlasLocalContainer(IMAGE).start();

const db = mongoose.createConnection(atlasLocalContainer.getConnectionString(), {
const db = (await import("mongoose")).default.createConnection(atlasLocalContainer.getConnectionString(), {
dbName: "test",
directConnection: true,
});
Expand Down
12 changes: 9 additions & 3 deletions packages/modules/mongodb/src/mongodb-container.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import mongoose from "mongoose";
import { getImage } from "../../../testcontainers/src/utils/test-helper";
import { MongoDBContainer } from "./mongodb-container";

const IMAGE = getImage(__dirname);

describe("MongoDBContainer", { timeout: 240_000 }, () => {
// Static loading uses node:v8 APIs unsupported by Bun before this suite can return.
// https://github.com/oven-sh/bun/issues/32501
if (process.env.BUN_CI) return;
it.each([IMAGE, "mongo:6.0.25", "mongo:4.4.29"])("should work with %s", async (image) => {
// connectMongo {
await using container = await new MongoDBContainer(image).start();

const db = mongoose.createConnection(container.getConnectionString(), { directConnection: true });
const db = (await import("mongoose")).default.createConnection(container.getConnectionString(), {
directConnection: true,
});

const obj = { value: 1 };
const collection = db.collection("test");
Expand All @@ -30,7 +34,9 @@ describe("MongoDBContainer", { timeout: 240_000 }, () => {
.start();
// }

const db = mongoose.createConnection(container.getConnectionString(), { directConnection: true });
const db = (await import("mongoose")).default.createConnection(container.getConnectionString(), {
directConnection: true,
});

const result = await db.collection("test").insertOne({ title: "test" });
const resultId = result.insertedId.toString();
Expand Down
Loading
Loading