diff --git a/dist/index.js b/dist/index.js index 75e73cb..c62f780 100644 --- a/dist/index.js +++ b/dist/index.js @@ -31403,16 +31403,12 @@ var DockerComposeService = class { } } async logs({ services, ...optionsInputs }) { - const commandArgs = this.getDockerComposeCommandArgs("logs", { - dockerFlags: optionsInputs.dockerFlags, - composeFlags: optionsInputs.composeFlags, - composeFiles: optionsInputs.composeFiles, - commandArgs: services - }); + const options = this.getCommonOptions(optionsInputs); + const { executablePath, executableArgs } = this.getDockerComposeCommandExecution("logs", services, options); return new Promise((resolve2) => { let settled = false; - const childProcess = spawn2("docker", commandArgs, { - cwd: optionsInputs.cwd + const childProcess = spawn2(executablePath, executableArgs, { + cwd: options.cwd }); childProcess.on("error", (error2) => { if (settled) { @@ -31433,10 +31429,10 @@ var DockerComposeService = class { return; } childProcess.stdout.on("data", (chunk) => { - optionsInputs.serviceLogger(chunk.toString()); + options.callback?.(Buffer.from(chunk), "stdout"); }); childProcess.stderr.on("data", (chunk) => { - optionsInputs.serviceLogger(chunk.toString()); + options.callback?.(Buffer.from(chunk), "stderr"); }); childProcess.on("close", (exitCode, signal) => { if (settled) { @@ -31468,30 +31464,55 @@ var DockerComposeService = class { } }; } - /** - * Builds docker CLI arguments in the order expected by `docker compose`. - */ - getDockerComposeCommandArgs(command, { - dockerFlags, - composeFlags, - composeFiles, - commandArgs - }) { - return [ - ...dockerFlags, - "compose", - ...composeFlags, - ...composeFiles.flatMap((composeFile) => ["-f", composeFile]), - command, - ...commandArgs - ]; - } /** * Formats docker-compose errors into proper Error objects with readable messages */ formatDockerComposeError(error2) { + return new Error(this.getDockerComposeErrorMessage(error2)); + } + getDockerComposeCommandExecution(command, commandArgs, options) { + const composeArgs = [ + ...this.getComposeOptionArgs(options.composeOptions), + ...this.getConfigArgs(options.config), + command, + ...this.getComposeOptionArgs(options.commandOptions), + ...commandArgs + ]; + if (options.executable?.standalone) { + return { + executablePath: options.executable.executablePath ?? "docker-compose", + executableArgs: composeArgs + }; + } + return { + executablePath: options.executable?.executablePath ?? "docker", + executableArgs: [ + ...this.getComposeOptionArgs(options.executable?.options), + "compose", + ...composeArgs + ] + }; + } + getConfigArgs(config) { + if (typeof config === "undefined") { + return []; + } + if (typeof config === "string") { + return ["-f", config]; + } + return config.flatMap((item) => ["-f", item]); + } + getComposeOptionArgs(composeOptions) { + if (!composeOptions) { + return []; + } + return composeOptions.flatMap( + (option) => Array.isArray(option) ? option : [option] + ); + } + getDockerComposeErrorMessage(error2) { if (error2 instanceof Error) { - return error2; + return error2.message; } if (this.isDockerComposeResult(error2)) { const parts = []; @@ -31510,12 +31531,12 @@ var DockerComposeService = class { parts.push("\nStandard output:"); parts.push(error2.out.trim()); } - return new Error(parts.join("\n")); + return parts.join("\n"); } if (typeof error2 === "string") { - return new Error(error2); + return error2; } - return new Error(JSON.stringify(error2)); + return JSON.stringify(error2); } /** * Type guard to check if an object is a docker-compose result diff --git a/dist/post.js b/dist/post.js index a07d167..748eb79 100644 --- a/dist/post.js +++ b/dist/post.js @@ -27140,16 +27140,12 @@ var DockerComposeService = class { } } async logs({ services, ...optionsInputs }) { - const commandArgs = this.getDockerComposeCommandArgs("logs", { - dockerFlags: optionsInputs.dockerFlags, - composeFlags: optionsInputs.composeFlags, - composeFiles: optionsInputs.composeFiles, - commandArgs: services - }); + const options = this.getCommonOptions(optionsInputs); + const { executablePath, executableArgs } = this.getDockerComposeCommandExecution("logs", services, options); return new Promise((resolve) => { let settled = false; - const childProcess = spawn("docker", commandArgs, { - cwd: optionsInputs.cwd + const childProcess = spawn(executablePath, executableArgs, { + cwd: options.cwd }); childProcess.on("error", (error2) => { if (settled) { @@ -27170,10 +27166,10 @@ var DockerComposeService = class { return; } childProcess.stdout.on("data", (chunk) => { - optionsInputs.serviceLogger(chunk.toString()); + options.callback?.(Buffer.from(chunk), "stdout"); }); childProcess.stderr.on("data", (chunk) => { - optionsInputs.serviceLogger(chunk.toString()); + options.callback?.(Buffer.from(chunk), "stderr"); }); childProcess.on("close", (exitCode, signal) => { if (settled) { @@ -27205,30 +27201,55 @@ var DockerComposeService = class { } }; } - /** - * Builds docker CLI arguments in the order expected by `docker compose`. - */ - getDockerComposeCommandArgs(command, { - dockerFlags, - composeFlags, - composeFiles, - commandArgs - }) { - return [ - ...dockerFlags, - "compose", - ...composeFlags, - ...composeFiles.flatMap((composeFile) => ["-f", composeFile]), - command, - ...commandArgs - ]; - } /** * Formats docker-compose errors into proper Error objects with readable messages */ formatDockerComposeError(error2) { + return new Error(this.getDockerComposeErrorMessage(error2)); + } + getDockerComposeCommandExecution(command, commandArgs, options) { + const composeArgs = [ + ...this.getComposeOptionArgs(options.composeOptions), + ...this.getConfigArgs(options.config), + command, + ...this.getComposeOptionArgs(options.commandOptions), + ...commandArgs + ]; + if (options.executable?.standalone) { + return { + executablePath: options.executable.executablePath ?? "docker-compose", + executableArgs: composeArgs + }; + } + return { + executablePath: options.executable?.executablePath ?? "docker", + executableArgs: [ + ...this.getComposeOptionArgs(options.executable?.options), + "compose", + ...composeArgs + ] + }; + } + getConfigArgs(config) { + if (typeof config === "undefined") { + return []; + } + if (typeof config === "string") { + return ["-f", config]; + } + return config.flatMap((item) => ["-f", item]); + } + getComposeOptionArgs(composeOptions) { + if (!composeOptions) { + return []; + } + return composeOptions.flatMap( + (option) => Array.isArray(option) ? option : [option] + ); + } + getDockerComposeErrorMessage(error2) { if (error2 instanceof Error) { - return error2; + return error2.message; } if (this.isDockerComposeResult(error2)) { const parts = []; @@ -27247,12 +27268,12 @@ var DockerComposeService = class { parts.push("\nStandard output:"); parts.push(error2.out.trim()); } - return new Error(parts.join("\n")); + return parts.join("\n"); } if (typeof error2 === "string") { - return new Error(error2); + return error2; } - return new Error(JSON.stringify(error2)); + return JSON.stringify(error2); } /** * Type guard to check if an object is a docker-compose result @@ -27392,6 +27413,7 @@ async function run() { const dockerComposeService = new DockerComposeService(); const inputs = inputService.getInputs(); try { + loggerService.debug("docker compose logs:"); const { error: error2 } = await dockerComposeService.logs({ dockerFlags: inputs.dockerFlags, composeFiles: inputs.composeFiles, diff --git a/src/post-runner.test.ts b/src/post-runner.test.ts index 0ec6538..83384db 100644 --- a/src/post-runner.test.ts +++ b/src/post-runner.test.ts @@ -134,6 +134,7 @@ describe("run", () => { await run(); // Assert + expect(debugMock).toHaveBeenCalledWith("docker compose logs:"); expect(debugMock).toHaveBeenCalledWith( "docker compose error:\ntest logs error", ); diff --git a/src/post-runner.ts b/src/post-runner.ts index b34fee4..1d26246 100644 --- a/src/post-runner.ts +++ b/src/post-runner.ts @@ -16,6 +16,8 @@ export async function run(): Promise { const inputs = inputService.getInputs(); try { + loggerService.debug("docker compose logs:"); + const { error } = await dockerComposeService.logs({ dockerFlags: inputs.dockerFlags, composeFiles: inputs.composeFiles, diff --git a/src/services/docker-compose.service.test.ts b/src/services/docker-compose.service.test.ts index f76b163..5b695d5 100644 --- a/src/services/docker-compose.service.test.ts +++ b/src/services/docker-compose.service.test.ts @@ -1,8 +1,8 @@ +import { EventEmitter } from "node:events"; import type { IDockerComposeOptions, IDockerComposeResult, } from "docker-compose"; -import { EventEmitter } from "node:events"; import { beforeEach, describe, expect, it, vi } from "vitest"; // Mock docker-compose before importing the module under test @@ -357,13 +357,13 @@ describe("DockerComposeService", () => { }); describe("logs", () => { - it("should stream logs with correct command arguments", async () => { + it("should stream logs with wrapper-compatible command arguments", async () => { const debugMock = vi.fn(); const logsInputs = { - dockerFlags: [] as string[], + dockerFlags: ["--context", "dev"] as string[], composeFiles: ["docker-compose.yml"], services: ["helloworld2", "helloworld3"], - composeFlags: [] as string[], + composeFlags: ["--profile", "ci"] as string[], cwd: "/current/working/dir", serviceLogger: debugMock, }; @@ -380,22 +380,31 @@ describe("DockerComposeService", () => { const logsPromise = service.logs(logsInputs); - expect(spawnMock).toHaveBeenCalledWith("docker", [ - "compose", - "-f", - "docker-compose.yml", - "logs", - "helloworld2", - "helloworld3", - ], { - cwd: "/current/working/dir", - }); + expect(spawnMock).toHaveBeenCalledWith( + "docker", + [ + "--context", + "dev", + "compose", + "--profile", + "ci", + "-f", + "docker-compose.yml", + "logs", + "helloworld2", + "helloworld3", + ], + { + cwd: "/current/working/dir", + }, + ); stdout.emit("data", Buffer.from("logs")); stderr.emit("data", Buffer.from("error logs")); childProcess.emit("close", 0); await expect(logsPromise).resolves.toEqual({ error: "", output: "" }); + expect(debugMock).toHaveBeenNthCalledWith(1, "logs"); expect(debugMock).toHaveBeenNthCalledWith(2, "error logs"); }); @@ -420,19 +429,6 @@ describe("DockerComposeService", () => { const logsPromise = service.logs(logsInputs); - expect(spawnMock).toHaveBeenCalledWith("docker", [ - "--context", - "dev", - "compose", - "--profile", - "ci", - "-f", - "docker-compose.yml", - "logs", - ], { - cwd: "/current/working/dir", - }); - childProcess.emit("close", 1); await expect(logsPromise).resolves.toEqual({ @@ -516,7 +512,8 @@ describe("DockerComposeService", () => { spawnMock.mockReturnValue(childProcess); await expect(service.logs(logsInputs)).resolves.toEqual({ - error: "Unable to collect docker compose logs: stdout/stderr unavailable", + error: + "Unable to collect docker compose logs: stdout/stderr unavailable", output: "", }); }); diff --git a/src/services/docker-compose.service.ts b/src/services/docker-compose.service.ts index aec003f..429854f 100644 --- a/src/services/docker-compose.service.ts +++ b/src/services/docker-compose.service.ts @@ -59,56 +59,58 @@ export class DockerComposeService { error: string; output: string; }> { - const commandArgs = this.getDockerComposeCommandArgs("logs", { - dockerFlags: optionsInputs.dockerFlags, - composeFlags: optionsInputs.composeFlags, - composeFiles: optionsInputs.composeFiles, - commandArgs: services, - }); + const options = this.getCommonOptions(optionsInputs); + const { executablePath, executableArgs } = + this.getDockerComposeCommandExecution("logs", services, options); return new Promise((resolve) => { let settled = false; - const childProcess = spawn("docker", commandArgs, { - cwd: optionsInputs.cwd, + const childProcess = spawn(executablePath, executableArgs, { + cwd: options.cwd, }); childProcess.on("error", (error) => { if (settled) { return; } + settled = true; resolve({ error: `Unable to collect docker compose logs: ${error.message}`, output: "", }); }); + if (!childProcess.stdout || !childProcess.stderr) { settled = true; resolve({ - error: "Unable to collect docker compose logs: stdout/stderr unavailable", + error: + "Unable to collect docker compose logs: stdout/stderr unavailable", output: "", }); return; } childProcess.stdout.on("data", (chunk: Buffer | string) => { - optionsInputs.serviceLogger(chunk.toString()); + options.callback?.(Buffer.from(chunk), "stdout"); }); + childProcess.stderr.on("data", (chunk: Buffer | string) => { - optionsInputs.serviceLogger(chunk.toString()); + options.callback?.(Buffer.from(chunk), "stderr"); }); + childProcess.on("close", (exitCode, signal) => { if (settled) { return; } + settled = true; resolve({ - error: - signal - ? `Docker Compose logs command failed with signal ${signal}` - : exitCode !== null && exitCode !== 0 - ? `Docker Compose logs command failed with exit code ${exitCode}` - : "", + error: signal + ? `Docker Compose logs command failed with signal ${signal}` + : exitCode !== null && exitCode !== 0 + ? `Docker Compose logs command failed with exit code ${exitCode}` + : "", output: "", }); }); @@ -134,47 +136,81 @@ export class DockerComposeService { }; } - /** - * Builds docker CLI arguments in the order expected by `docker compose`. - */ - private getDockerComposeCommandArgs( - command: "logs", - { - dockerFlags, - composeFlags, - composeFiles, - commandArgs, - }: { - dockerFlags: string[]; - composeFlags: string[]; - composeFiles: string[]; - commandArgs: string[]; - }, - ): string[] { - return [ - ...dockerFlags, - "compose", - ...composeFlags, - ...composeFiles.flatMap((composeFile) => ["-f", composeFile]), - command, - ...commandArgs, - ]; - } - /** * Formats docker-compose errors into proper Error objects with readable messages */ private formatDockerComposeError(error: unknown): Error { - // If it's already an Error, return it - if (error instanceof Error) { - return error; + return new Error(this.getDockerComposeErrorMessage(error)); + } + + private getDockerComposeCommandExecution( + command: string, + commandArgs: string[], + options: IDockerComposeOptions, + ): { + executablePath: string; + executableArgs: string[]; + } { + const composeArgs = [ + ...this.getComposeOptionArgs(options.composeOptions), + ...this.getConfigArgs(options.config), + command, + ...this.getComposeOptionArgs(options.commandOptions), + ...commandArgs, + ]; + + if (options.executable?.standalone) { + return { + executablePath: options.executable.executablePath ?? "docker-compose", + executableArgs: composeArgs, + }; + } + + return { + executablePath: options.executable?.executablePath ?? "docker", + executableArgs: [ + ...this.getComposeOptionArgs(options.executable?.options), + "compose", + ...composeArgs, + ], + }; + } + + private getConfigArgs(config: IDockerComposeOptions["config"]): string[] { + if (typeof config === "undefined") { + return []; + } + + if (typeof config === "string") { + return ["-f", config]; + } + + return config.flatMap((item) => ["-f", item]); + } + + private getComposeOptionArgs( + composeOptions: + | IDockerComposeOptions["composeOptions"] + | IDockerComposeOptions["commandOptions"] + | NonNullable["options"], + ): string[] { + if (!composeOptions) { + return []; + } + + return composeOptions.flatMap((option) => + Array.isArray(option) ? option : [option], + ); + } + + private getDockerComposeErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; } - // Handle docker-compose result objects if (this.isDockerComposeResult(error)) { const parts: string[] = []; - // Add exit code information if (error.exitCode !== null) { parts.push( `Docker Compose command failed with exit code ${error.exitCode}`, @@ -183,30 +219,25 @@ export class DockerComposeService { parts.push("Docker Compose command failed"); } - // Add error stream output if available if (error.err?.trim()) { parts.push("\nError output:"); parts.push(error.err.trim()); } - // Add standard output if available and different from error output if (error.out?.trim() && error.out !== error.err) { parts.push("\nStandard output:"); parts.push(error.out.trim()); } - return new Error(parts.join("\n")); + return parts.join("\n"); } - // Handle string errors if (typeof error === "string") { - return new Error(error); + return error; } - // Fallback for unknown error types - return new Error(JSON.stringify(error)); + return JSON.stringify(error); } - /** * Type guard to check if an object is a docker-compose result */