Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ services:
interval: 2s
timeout: 3s
retries: 30
start_period: 20s
start_period: 60s
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { afterAll, beforeAll, describe } from 'vitest';

type DefineTests = () => void;

const SLOW_START_MS = 60_000;

interface DockerOptions {
/**
* The working directory to run docker compose in
Expand Down Expand Up @@ -94,9 +96,28 @@ async function runDockerCompose(options: DockerOptions): Promise<VoidFunction> {
});
};

const output = (result: ReturnType<typeof spawnSync>): string =>
`${result.stderr?.toString() ?? ''}${result.stdout?.toString() ?? ''}`;
// Surface container logs to make healthcheck failures easier to diagnose in CI
const containerLogs = (): string => spawnSync('docker', composeArgs('logs'), { cwd }).stdout?.toString() ?? '';

// ensure we're starting fresh
close();

// Pull on its own, so a slow image pull on a runner without cached images can be told apart from
// a slow container start.
const pullStartedAt = Date.now();
const pull = spawnSync('docker', composeArgs('pull', '--policy', 'missing', '--quiet'), {
cwd,
stdio: process.env.DEBUG ? 'inherit' : 'pipe',
});
const pullMs = Date.now() - pullStartedAt;
if (pull.status !== 0) {
// `up` pulls missing images too, so only report this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

as in don't kill the test yet because we get a second try when docker compose up runs? maybe this could be clarified a bit

// eslint-disable-next-line no-console
console.warn(`[docker compose] pull failed after ${pullMs}ms (exit ${pull.status}) in ${cwd}\n${output(pull)}`);
}

const composeUp = (): ReturnType<typeof spawnSync> =>
spawnSync('docker', composeArgs('up', '-d', '--wait'), {
cwd,
Expand All @@ -108,22 +129,33 @@ async function runDockerCompose(options: DockerOptions): Promise<VoidFunction> {
// found" right after the network was created). A clean teardown plus retry
// clears these, while genuine healthcheck failures stay red on every attempt.
const maxAttempts = 3;
const upStartedAt = Date.now();
let attemptStartedAt = upStartedAt;
let result = composeUp();
for (let attempt = 1; attempt < maxAttempts && result.status !== 0; attempt++) {
// eslint-disable-next-line no-console
console.warn(
`[docker compose] up --wait attempt ${attempt} failed after ${Date.now() - attemptStartedAt}ms ` +
`(exit ${result.status}) in ${cwd}\n${output(result)}\n--- container logs ---\n${containerLogs()}`,
);
close();
attemptStartedAt = Date.now();
result = composeUp();
}
const upMs = Date.now() - upStartedAt;
Comment on lines 131 to +145

@nicohrubec nicohrubec Oct 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
const maxAttempts = 3;
const upStartedAt = Date.now();
let attemptStartedAt = upStartedAt;
let result = composeUp();
for (let attempt = 1; attempt < maxAttempts && result.status !== 0; attempt++) {
// eslint-disable-next-line no-console
console.warn(
`[docker compose] up --wait attempt ${attempt} failed after ${Date.now() - attemptStartedAt}ms ` +
`(exit ${result.status}) in ${cwd}\n${output(result)}\n--- container logs ---\n${containerLogs()}`,
);
close();
attemptStartedAt = Date.now();
result = composeUp();
}
const upMs = Date.now() - upStartedAt;
const maxAttempts = 3;
const upStartedAt = Date.now();
let result: ReturnType<typeof spawnSync> | undefined;
for (let attempt = 1; attempt <= maxAttempts && result?.status !== 0; attempt++) {
const attemptStartedAt = Date.now();
result = composeUp();
if (result.status !== 0 && attempt < maxAttempts) {
// eslint-disable-next-line no-console
console.warn(
`[docker compose] up --wait attempt ${attempt} failed after ${Date.now() - attemptStartedAt}ms ` +
`(exit ${result.status}) in ${cwd}\n${output(result)}\n--- container logs ---\n${containerLogs()}`,
);
close();
}
}
const upMs = Date.now() - upStartedAt;

just a suggestion, I think this is a bit nicer to read and understand because it demonstrates the flow a bit better but no strong opinions


if (result.status !== 0) {
const stderr = result.stderr?.toString() ?? '';
const stdout = result.stdout?.toString() ?? '';
// Surface container logs to make healthcheck failures easier to diagnose in CI
const logs = spawnSync('docker', composeArgs('logs'), { cwd }).stdout?.toString() ?? '';
const logs = containerLogs();
close();
throw new Error(
`docker compose up --wait failed (exit ${result.status})\n${stderr}${stdout}\n--- container logs ---\n${logs}`,
`docker compose up --wait failed (exit ${result.status})\n${output(result)}\n--- container logs ---\n${logs}`,
);
}

if (pullMs + upMs > SLOW_START_MS) {
// eslint-disable-next-line no-console
console.warn(`[docker compose] slow start in ${cwd}: pull ${pullMs}ms, up --wait ${upMs}ms`);
}

return close;
}
Loading