From 0bc1a577f985949496d2d5ee6212f600e4ee4c9d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 1 Jul 2026 19:09:57 +0200 Subject: [PATCH 1/2] test(node): Attempt to unflake docker-based node integration tests --- .../node-integration-tests/scripts/clean.js | 8 +++++++- .../utils/runner/createRunner.ts | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/dev-packages/node-integration-tests/scripts/clean.js b/dev-packages/node-integration-tests/scripts/clean.js index e6fdb4c4f6e8..6d4510cd4568 100644 --- a/dev-packages/node-integration-tests/scripts/clean.js +++ b/dev-packages/node-integration-tests/scripts/clean.js @@ -1,10 +1,16 @@ const { execSync } = require('child_process'); +const { createHash } = require('crypto'); const globby = require('globby'); const { dirname, join } = require('path'); const cwd = join(__dirname, '..'); const paths = globby.sync(['suites/**/docker-compose.yml'], { cwd }).map(path => join(cwd, dirname(path))); +// Must stay in sync with `runDockerCompose` in utils/runner/createRunner.ts: +// the runner starts each suite under a unique, path-derived project name, so we +// have to target the same name here or the teardown misses the containers. +const projectNameFor = suiteDir => `sentry-it-${createHash('sha1').update(suiteDir).digest('hex').slice(0, 12)}`; + // eslint-disable-next-line no-console console.log('Cleaning up docker containers and volumes...'); @@ -12,7 +18,7 @@ for (const path of paths) { try { // eslint-disable-next-line no-console console.log(`docker compose down @ ${path}`); - execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path }); + execSync(`docker compose -p ${projectNameFor(path)} down --volumes`, { stdio: 'inherit', cwd: path }); } catch { // } diff --git a/dev-packages/node-integration-tests/utils/runner/createRunner.ts b/dev-packages/node-integration-tests/utils/runner/createRunner.ts index 5e576f4ff94e..2d450c10cd37 100644 --- a/dev-packages/node-integration-tests/utils/runner/createRunner.ts +++ b/dev-packages/node-integration-tests/utils/runner/createRunner.ts @@ -15,6 +15,7 @@ import type { import { normalize } from '@sentry/core'; import { createBasicSentryServer } from '@sentry-internal/test-utils'; import { execSync, spawn, spawnSync } from 'child_process'; +import { createHash } from 'crypto'; import { existsSync } from 'fs'; import { join } from 'path'; import { inspect } from 'util'; @@ -604,8 +605,20 @@ export function createRunner(...paths: string[]) { */ async function runDockerCompose(options: DockerOptions): Promise { const cwd = join(...options.workingDirectory); + + // Docker Compose derives the project name from the compose file's directory + // basename by default. Several suites live in directories that share a + // basename (e.g. `tracing/mysql2` and `tracing/knex/mysql2`), so they collide + // on the same project + network when running in parallel: one suite's + // teardown removes the shared `_default` network while a sibling is + // still starting, producing "network _default not found". Deriving a + // unique, stable project name from the full working directory isolates every + // suite from each other. + const projectName = `sentry-it-${createHash('sha1').update(cwd).digest('hex').slice(0, 12)}`; + const composeArgs = (...args: string[]): string[] => ['compose', '-p', projectName, ...args]; + const close = (): void => { - spawnSync('docker', ['compose', 'down', '--volumes'], { + spawnSync('docker', composeArgs('down', '--volumes'), { cwd, stdio: process.env.DEBUG ? 'inherit' : undefined, }); @@ -615,7 +628,7 @@ async function runDockerCompose(options: DockerOptions): Promise { close(); const composeUp = (): ReturnType => - spawnSync('docker', ['compose', 'up', '-d', '--wait'], { + spawnSync('docker', composeArgs('up', '-d', '--wait'), { cwd, stdio: process.env.DEBUG ? 'inherit' : 'pipe', }); @@ -635,7 +648,7 @@ async function runDockerCompose(options: DockerOptions): Promise { 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', ['compose', 'logs'], { cwd }).stdout?.toString() ?? ''; + const logs = spawnSync('docker', composeArgs('logs'), { cwd }).stdout?.toString() ?? ''; close(); throw new Error( `docker compose up --wait failed (exit ${result.status})\n${stderr}${stdout}\n--- container logs ---\n${logs}`, From 5ce16b2e8098cda72fdb205bedb9cd61349c8308 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 6 Jul 2026 15:51:40 +0200 Subject: [PATCH 2/2] fix issue from review --- .../node-integration-tests/scripts/clean.js | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/dev-packages/node-integration-tests/scripts/clean.js b/dev-packages/node-integration-tests/scripts/clean.js index 6d4510cd4568..fc5a09092a19 100644 --- a/dev-packages/node-integration-tests/scripts/clean.js +++ b/dev-packages/node-integration-tests/scripts/clean.js @@ -1,24 +1,51 @@ const { execSync } = require('child_process'); -const { createHash } = require('crypto'); -const globby = require('globby'); -const { dirname, join } = require('path'); -const cwd = join(__dirname, '..'); -const paths = globby.sync(['suites/**/docker-compose.yml'], { cwd }).map(path => join(cwd, dirname(path))); - -// Must stay in sync with `runDockerCompose` in utils/runner/createRunner.ts: -// the runner starts each suite under a unique, path-derived project name, so we -// have to target the same name here or the teardown misses the containers. -const projectNameFor = suiteDir => `sentry-it-${createHash('sha1').update(suiteDir).digest('hex').slice(0, 12)}`; +// The runner (utils/runner/createRunner.ts) starts each suite's docker compose +// stack under a project name prefixed with `sentry-it-`, derived by hashing the +// working directory. Some suites (e.g. the Prisma tests) run from a temporary +// directory whose path — and therefore whose derived project name — isn't known +// ahead of time and no longer exists on disk by the time this script runs (the +// `clean` npm script removes `tmp_*` dirs first). Reconstructing project names +// from compose file paths therefore misses those stacks and leaks containers. +// +// Instead, ask Docker for every `sentry-it-*` project it still knows about and +// tear those down by name. `docker compose -p down` operates purely from +// the containers' labels, so it succeeds even when the original compose file is +// gone. +const PROJECT_PREFIX = 'sentry-it-'; // eslint-disable-next-line no-console console.log('Cleaning up docker containers and volumes...'); -for (const path of paths) { +function listSentryProjects() { + let output; + try { + output = execSync('docker compose ls --all --format json', { encoding: 'utf8' }); + } catch { + return []; + } + + let projects; + try { + projects = JSON.parse(output); + } catch { + return []; + } + + if (!Array.isArray(projects)) { + return []; + } + + return projects + .map(project => project && project.Name) + .filter(name => typeof name === 'string' && name.startsWith(PROJECT_PREFIX)); +} + +for (const name of listSentryProjects()) { try { // eslint-disable-next-line no-console - console.log(`docker compose down @ ${path}`); - execSync(`docker compose -p ${projectNameFor(path)} down --volumes`, { stdio: 'inherit', cwd: path }); + console.log(`docker compose -p ${name} down --volumes`); + execSync(`docker compose -p ${name} down --volumes`, { stdio: 'inherit' }); } catch { // }