Skip to content

Commit 81c4124

Browse files
Merge main and address healthcheck review feedback
2 parents 27f20fb + cec8a5f commit 81c4124

167 files changed

Lines changed: 13643 additions & 16314 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Close inactive issues
2+
3+
on:
4+
schedule:
5+
- cron: "30 1 * * *"
6+
7+
jobs:
8+
close-issues:
9+
runs-on: ubuntu-22.04
10+
permissions:
11+
issues: write
12+
pull-requests: write
13+
steps:
14+
- uses: actions/stale@v10
15+
with:
16+
days-before-issue-stale: 90
17+
days-before-issue-close: 30
18+
stale-issue-label: "stale"
19+
exempt-issue-labels: "never-stale"
20+
stale-issue-message: "This issue is stale because it has been open for 90 days with no activity."
21+
close-issue-message: "This issue was closed because it has been inactive for 30 days since being marked as stale."
22+
days-before-pr-stale: -1
23+
days-before-pr-close: -1
24+
repo-token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/dependency-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ permissions:
77
jobs:
88
dependency-review:
99
name: Run
10-
runs-on: ubuntu-latest
10+
runs-on: ubuntu-22.04
1111
steps:
1212
- name: "Checkout Repository"
1313
uses: actions/checkout@v5

.github/workflows/test-template.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ jobs:
5454
workspace: "${{ inputs.workspace }}"
5555

5656
- name: Run tests
57-
run: npm run test:ci -- --coverage.include=${{ steps.npm-install.outputs.workspace_path }} ${{ steps.npm-install.outputs.workspace_path }}
57+
run: npm run test:ci -- --coverage.include="${{ steps.npm-install.outputs.workspace_path }}/**/*.ts" ${{ steps.npm-install.outputs.workspace_path }}
5858
env:
5959
CI: true

AGENTS.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# AGENTS.md
2+
3+
## Purpose
4+
5+
This is a working guide for contributors and coding agents in this repository.
6+
It captures practical rules that prevent avoidable CI and PR churn.
7+
8+
## Instruction precedence
9+
10+
- Repository-specific instructions in this file override generic coding-agent defaults, skills, and templates.
11+
- If a generic workflow conflicts with this file, follow this file.
12+
13+
## Repository Layout
14+
15+
- This repository is an npm workspaces monorepo.
16+
- root package: `testcontainers-monorepo`
17+
- workspaces: `packages/testcontainers` and `packages/modules/*`
18+
- shared lockfile: root `package-lock.json` (workspace installs update this single file)
19+
- For workspace-scoped dependency changes, prefer targeted commands to reduce lockfile churn:
20+
- `npm install -w @testcontainers/<module>`
21+
- `npm uninstall -w @testcontainers/<module> <package>`
22+
23+
## Development Expectations
24+
25+
- If a public API changes, update the relevant docs in the same PR.
26+
- If new types are made part of the public API, export them from the package's `index.ts` in the same PR.
27+
- If new learnings or misunderstandings are discovered, propose an `AGENTS.md` update in the same PR.
28+
- In docs Markdown, keep `<!--codeinclude-->` blocks tight with no blank lines between the markers and the include line, or the rendered snippet will contain blank lines between code lines.
29+
- Tests should verify observable behavior changes, not only internal/config state.
30+
- Example: for a security option, assert a real secure/insecure behavior difference.
31+
- 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.
32+
- Vitest runs tests concurrently by default (`sequence.concurrent: true` in `vitest.config.ts`).
33+
- Tests that rely on shared/global mocks (for example `vi.spyOn` on shared loggers/singletons) can be flaky due to interleaving or automatic mock resets.
34+
- Prefer asserting observable behavior instead of shared global mock state when possible.
35+
- If a test must depend on shared/global mock state, use `it.sequential(...)` or `describe.sequential(...)`.
36+
37+
## Permission and Escalation
38+
39+
- `npm install` requires escalated permissions for outbound network access to npm registries.
40+
- `npm test` commands should be run with escalation so tests can access the Docker socket.
41+
42+
### Escalation hygiene
43+
44+
- Use specific commands and clear justifications.
45+
- Prefer narrow reruns rather than broad full-suite reruns when iterating.
46+
47+
## PR Process
48+
49+
1. Start from `main`.
50+
2. Create a branch prefixed with `codex/`.
51+
3. Implement scoped changes only.
52+
4. Run required checks: `npm run format`, `npm run lint`, and targeted tests.
53+
5. Verify git diff only contains intended files.
54+
6. Never commit, push, or post on GitHub (issues, PRs, or comments) without first sharing the proposed diff/message and getting explicit user approval.
55+
7. Commit with focused message(s), using `git commit --no-verify`.
56+
- Never bypass signing (for example, do not use `--no-gpg-sign`).
57+
- If signing fails (for example, passphrase/key issues), stop and ask the user to resolve signing, then retry.
58+
8. Push branch. Ask for explicit user permission before any force push.
59+
9. Open PR against `main` using a human-readable title (no `feat(...)` / `fix(...)` prefixes, and no agent-identifying prefixes or suffixes).
60+
- Default to a ready-for-review PR. Only open or keep a PR in draft when the user explicitly asks for a draft.
61+
- When using `gh` to create/edit PR descriptions, prefer `--body-file <path>` over inline `--body`; this avoids shell command substitution issues when the body contains backticks.
62+
10. Add labels for both change type and semantic version impact.
63+
11. Ensure PR body includes:
64+
- summary of changes
65+
- verification commands run
66+
- test results summary
67+
- if semver impact is not `major`, evidence that the change is not breaking
68+
- `Closes #<issue>` only when the PR is intended to close a specific issue
69+
70+
## Labels
71+
72+
### Change type
73+
74+
- `enhancement`
75+
- `bug`
76+
- `dependencies`
77+
- `documentation`
78+
- `maintenance`
79+
80+
### Semver impact
81+
82+
- `major`
83+
- `minor`
84+
- `patch`
85+
86+
### Common mappings
87+
88+
- backward-compatible feature: `enhancement` + `minor`
89+
- backward-compatible bug fix: `bug` + `patch`
90+
- breaking change: type label + `major`
91+
- docs-only change: `documentation` + usually `patch`
92+
- dependency update: `dependencies` + impact label based on user-facing effect
93+
94+
## Lockfile Hygiene
95+
96+
- Recheck `package-lock.json` after `npm install` for unrelated drift and revert unrelated changes.

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Then access the docs at [http://localhost:8000](http://localhost:8000).
3131

3232
#### Using Python
3333

34-
* Ensure that you have Python 3.8.0 or higher.
34+
* Ensure that you have Python 3.11.0 or higher.
3535
* Set up a virtualenv and run `pip install -r requirements.txt` in the `testcontainers-node` root directory.
3636
* Once Python dependencies have been installed, run `mkdocs serve` to start a local auto-updating MkDocs server.
3737

docs/features/compose.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,25 @@ Provide a list of service names to only start those services:
2929

3030
```js
3131
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
32-
.up(["redis-1", "postgres-1"]);
32+
.up(["redis", "postgres"]);
3333
```
3434

3535
### With wait strategy
3636

37+
`withWaitStrategy` expects **container names**, not service names. With Docker Compose v2, the default container name for the first replica is usually `<service>-1`.
38+
3739
```js
3840
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
3941
.withWaitStrategy("redis-1", Wait.forLogMessage("Ready to accept connections"))
4042
.withWaitStrategy("postgres-1", Wait.forHealthCheck())
41-
.up();
43+
.up(["redis", "postgres"]);
4244
```
4345

4446
### With a default wait strategy
4547

46-
By default Testcontainers uses the "listening ports" wait strategy for all containers. If you'd like to override
47-
the default wait strategy for all services, you can do so:
48+
By default, Testcontainers waits for a service health check when one is defined in the Compose service or image. If no health check is defined, or the service disables health checks, it waits for listening ports.
49+
50+
If you'd like to override the default wait strategy for all services, you can do so:
4851

4952
```js
5053
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
@@ -142,6 +145,19 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
142145
.up();
143146
```
144147

148+
### With auto cleanup disabled
149+
150+
By default Testcontainers registers the compose project with Ryuk so the stack is torn down automatically when the process exits. You can disable that registration for a specific compose stack:
151+
152+
```js
153+
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
154+
.withProjectName("test")
155+
.withAutoCleanup(false)
156+
.up();
157+
```
158+
159+
This only disables automatic cleanup. Explicit calls to `.down()`, `.stop()`, or `await using` disposal still tear the stack down as usual.
160+
145161
### With custom client options
146162

147163
See [docker-compose](https://github.com/PDMLab/docker-compose/) library.
@@ -187,7 +203,7 @@ await environment.stop();
187203

188204
## Interacting with the containers
189205

190-
Interact with the containers in your compose environment as you would any other Generic Container. Note that the container name suffix has changed from `_` to `-` between docker-compose v1 and v2 respectively.
206+
Interact with the containers in your compose environment as you would any other Generic Container. Compose-managed container names use the `<service-name>-<index>` format.
191207

192208
```js
193209
const container = environment.getContainer("alpine-1");

docs/features/containers.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ const container = await new GenericContainer("alpine")
137137
tar: nodeReadable,
138138
target: "/some/nested/remotedir"
139139
}])
140+
.withCopyToContainerOptions({
141+
copyUIDGID: true
142+
})
140143
.start();
141144
```
142145

@@ -157,9 +160,13 @@ container.copyContentToContainer([{
157160
content: "hello world",
158161
target: "/remote/file2.txt"
159162
}])
160-
container.copyArchiveToContainer(nodeReadable, "/some/nested/remotedir");
163+
container.copyArchiveToContainer(nodeReadable, "/some/nested/remotedir", {
164+
copyUIDGID: true
165+
});
161166
```
162167

168+
When copying files, symbolic links in `source` are followed and the linked file content is copied into the container.
169+
163170
An optional `mode` can be specified in octal for setting file permissions:
164171

165172
```js
@@ -182,6 +189,21 @@ const container = await new GenericContainer("alpine")
182189
.start();
183190
```
184191

192+
Archive copy options can also be specified:
193+
194+
```js
195+
const container = await new GenericContainer("alpine")
196+
.withCopyToContainerOptions({
197+
copyUIDGID: true,
198+
noOverwriteDirNonDir: true
199+
})
200+
.start();
201+
```
202+
203+
- `copyUIDGID`: preserve UID/GID from tar archive entries.
204+
Note: Podman may ignore this for archive copy in some cases, see [containers/podman#27538](https://github.com/containers/podman/issues/27538).
205+
- `noOverwriteDirNonDir`: fail if extraction would replace a file with a directory (or vice versa).
206+
185207
### Copy archive from container
186208

187209
Files and directories can be fetched from a started or stopped container as a tar archive. The archive is returned as a readable stream:
@@ -245,6 +267,16 @@ const container = await new GenericContainer("alpine")
245267
.start();
246268
```
247269

270+
### With security options
271+
272+
See [Security options](https://docs.docker.com/engine/reference/run/#security-configuration).
273+
274+
```js
275+
const container = await new GenericContainer("alpine")
276+
.withSecurityOpt("no-new-privileges")
277+
.start();
278+
```
279+
248280
### With added capabilities
249281

250282
See [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html).
@@ -368,6 +400,16 @@ const container = await new GenericContainer("alpine")
368400
await container.stop({ remove: true }); // The container is stopped *AND* removed
369401
```
370402

403+
You can also disable automatic Ryuk cleanup for a specific container while leaving it enabled for the rest of the test session:
404+
405+
```js
406+
const container = await new GenericContainer("alpine")
407+
.withAutoCleanup(false)
408+
.start();
409+
```
410+
411+
This only affects automatic cleanup when the process exits unexpectedly or the container is otherwise left running. Explicit calls to `.stop()` still use the normal stop and removal behavior, so combine this with `.withAutoRemove(false)` or `.stop({ remove: false })` if you also want explicit stops to keep the container.
412+
371413
Keep in mind that disabling ryuk (set `TESTCONTAINERS_RYUK_DISABLED` to `true`) **and** disabling automatic removal of containers will make containers persist after you're done working with them.
372414

373415

docs/features/wait-strategies.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ const container = await new GenericContainer("alpine")
1010
.start();
1111
```
1212

13+
## Default wait strategy
14+
15+
By default, Testcontainers waits for a container health check when one is defined by the image or configured with `withHealthCheck`. If no health check is defined, or the image disables health checks with `HEALTHCHECK NONE`, it waits up to 60 seconds for mapped network ports to be bound.
16+
17+
You can override this selection with `withWaitStrategy`.
18+
1319
## Listening ports
1420

15-
The default wait strategy used by Testcontainers. It will wait up to 60 seconds for the container's mapped network ports to be bound.
21+
Wait up to 60 seconds for the container's mapped network ports to be bound.
1622

1723
```js
1824
const { GenericContainer } = require("testcontainers");
@@ -75,10 +81,10 @@ const container = await new GenericContainer("alpine")
7581
.start();
7682
```
7783

78-
Define your own health check. Note that time units are in seconds:
84+
Define your own health check. Testcontainers uses this as the default wait strategy unless you explicitly set another wait strategy. Note that time units are in milliseconds:
7985

8086
```js
81-
const { GenericContainer, Wait } = require("testcontainers");
87+
const { GenericContainer } = require("testcontainers");
8288

8389
const container = await new GenericContainer("alpine")
8490
.withHealthCheck({
@@ -88,7 +94,6 @@ const container = await new GenericContainer("alpine")
8894
retries: 5,
8995
startPeriod: 1000,
9096
})
91-
.withWaitStrategy(Wait.forHealthCheck())
9297
.start();
9398
```
9499

docs/modules/azurite.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,17 @@ Choose an image from the [container registry](https://hub.docker.com/r/microsoft
5959
<!--codeinclude-->
6060
[](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:customPorts
6161
<!--/codeinclude-->
62+
63+
### With HTTPS (PEM certificate)
64+
65+
<!--codeinclude-->
66+
[Code](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:httpsWithPem
67+
[`azurite-test-utils`](../../packages/modules/azurite/src/azurite-test-utils.ts) inside_block:azuriteTestUtils
68+
<!--/codeinclude-->
69+
70+
### With OAuth (basic)
71+
72+
<!--codeinclude-->
73+
[Code](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:withOAuth
74+
[`azurite-test-utils`](../../packages/modules/azurite/src/azurite-test-utils.ts) inside_block:azuriteTestUtils
75+
<!--/codeinclude-->

docs/modules/localstack.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,27 @@ These examples use the following libraries:
1616

1717
Choose an image from the [container registry](https://hub.docker.com/r/localstack/localstack) and substitute `IMAGE`.
1818

19+
!!! note "Authentication requirements"
20+
Starting on March 23, 2026, LocalStack moved to authenticated image releases. Older pinned tags may continue to work without `LOCALSTACK_AUTH_TOKEN`, but newer releases require it.
21+
22+
Prefer pinning a specific image tag instead of using `latest`. If the image tag you use requires authentication, pass `LOCALSTACK_AUTH_TOKEN` when starting the container:
23+
24+
```typescript
25+
const token = process.env.LOCALSTACK_AUTH_TOKEN;
26+
27+
if (!token) {
28+
throw new Error("LOCALSTACK_AUTH_TOKEN must be set for authenticated LocalStack images");
29+
}
30+
31+
const container = await new LocalstackContainer("localstack/localstack:IMAGE")
32+
.withEnvironment({ LOCALSTACK_AUTH_TOKEN: token })
33+
.start();
34+
```
35+
36+
Refer to the [LocalStack announcement](https://blog.localstack.cloud/localstack-single-image-next-steps/) for the current rollout details.
37+
1938
### Create a S3 bucket
2039

2140
<!--codeinclude-->
2241
[](../../packages/modules/localstack/src/localstack-container.test.ts) inside_block:localstackCreateS3Bucket
2342
<!--/codeinclude-->
24-

0 commit comments

Comments
 (0)