import Foundation enum DockerExec { static func run( composeFile: String, service: String, command: [String], timeout: TimeInterval = 60, ) throws { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = [ "docker", "compose", "-f", composeFile, "exec", "-T", "-u", "git", service, ] + command let stderrPipe = Pipe() process.standardError = stderrPipe process.standardOutput = FileHandle.nullDevice let didExit = DispatchSemaphore(value: 0) process.terminationHandler = { _ in didExit.signal() } try process.run() // Enforce a timeout so a hung `docker compose exec` (a known Docker-on-macOS flake) // fails fast and can be retried, instead of blocking the whole seed indefinitely. if didExit.wait(timeout: .now() + timeout) == .timedOut { process.terminate() _ = didExit.wait(timeout: .now() + 5) throw SeedError.dockerExecTimedOut( command: command.joined(separator: " "), seconds: timeout, ) } if process.terminationStatus != 0 { let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() let stderr = String(data: stderrData, encoding: .utf8) ?? "" throw SeedError.dockerExecFailed( command: command.joined(separator: " "), exitCode: process.terminationStatus, stderr: stderr, ) } } }