fix: restore post-job compose logs header

Co-authored-by: neilime <314088+neilime@users.noreply.github.com>
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-08-16 09:50:46 +00:00 committed by Emilien Escalle
parent 75b024b03b
commit 6f330cd1dc
6 changed files with 223 additions and 149 deletions

85
dist/index.js generated vendored
View File

@ -31403,16 +31403,12 @@ var DockerComposeService = class {
} }
} }
async logs({ services, ...optionsInputs }) { async logs({ services, ...optionsInputs }) {
const commandArgs = this.getDockerComposeCommandArgs("logs", { const options = this.getCommonOptions(optionsInputs);
dockerFlags: optionsInputs.dockerFlags, const { executablePath, executableArgs } = this.getDockerComposeCommandExecution("logs", services, options);
composeFlags: optionsInputs.composeFlags,
composeFiles: optionsInputs.composeFiles,
commandArgs: services
});
return new Promise((resolve2) => { return new Promise((resolve2) => {
let settled = false; let settled = false;
const childProcess = spawn2("docker", commandArgs, { const childProcess = spawn2(executablePath, executableArgs, {
cwd: optionsInputs.cwd cwd: options.cwd
}); });
childProcess.on("error", (error2) => { childProcess.on("error", (error2) => {
if (settled) { if (settled) {
@ -31433,10 +31429,10 @@ var DockerComposeService = class {
return; return;
} }
childProcess.stdout.on("data", (chunk) => { childProcess.stdout.on("data", (chunk) => {
optionsInputs.serviceLogger(chunk.toString()); options.callback?.(Buffer.from(chunk), "stdout");
}); });
childProcess.stderr.on("data", (chunk) => { childProcess.stderr.on("data", (chunk) => {
optionsInputs.serviceLogger(chunk.toString()); options.callback?.(Buffer.from(chunk), "stderr");
}); });
childProcess.on("close", (exitCode, signal) => { childProcess.on("close", (exitCode, signal) => {
if (settled) { 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 * Formats docker-compose errors into proper Error objects with readable messages
*/ */
formatDockerComposeError(error2) { 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) { if (error2 instanceof Error) {
return error2; return error2.message;
} }
if (this.isDockerComposeResult(error2)) { if (this.isDockerComposeResult(error2)) {
const parts = []; const parts = [];
@ -31510,12 +31531,12 @@ var DockerComposeService = class {
parts.push("\nStandard output:"); parts.push("\nStandard output:");
parts.push(error2.out.trim()); parts.push(error2.out.trim());
} }
return new Error(parts.join("\n")); return parts.join("\n");
} }
if (typeof error2 === "string") { 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 * Type guard to check if an object is a docker-compose result

86
dist/post.js generated vendored
View File

@ -27140,16 +27140,12 @@ var DockerComposeService = class {
} }
} }
async logs({ services, ...optionsInputs }) { async logs({ services, ...optionsInputs }) {
const commandArgs = this.getDockerComposeCommandArgs("logs", { const options = this.getCommonOptions(optionsInputs);
dockerFlags: optionsInputs.dockerFlags, const { executablePath, executableArgs } = this.getDockerComposeCommandExecution("logs", services, options);
composeFlags: optionsInputs.composeFlags,
composeFiles: optionsInputs.composeFiles,
commandArgs: services
});
return new Promise((resolve) => { return new Promise((resolve) => {
let settled = false; let settled = false;
const childProcess = spawn("docker", commandArgs, { const childProcess = spawn(executablePath, executableArgs, {
cwd: optionsInputs.cwd cwd: options.cwd
}); });
childProcess.on("error", (error2) => { childProcess.on("error", (error2) => {
if (settled) { if (settled) {
@ -27170,10 +27166,10 @@ var DockerComposeService = class {
return; return;
} }
childProcess.stdout.on("data", (chunk) => { childProcess.stdout.on("data", (chunk) => {
optionsInputs.serviceLogger(chunk.toString()); options.callback?.(Buffer.from(chunk), "stdout");
}); });
childProcess.stderr.on("data", (chunk) => { childProcess.stderr.on("data", (chunk) => {
optionsInputs.serviceLogger(chunk.toString()); options.callback?.(Buffer.from(chunk), "stderr");
}); });
childProcess.on("close", (exitCode, signal) => { childProcess.on("close", (exitCode, signal) => {
if (settled) { 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 * Formats docker-compose errors into proper Error objects with readable messages
*/ */
formatDockerComposeError(error2) { 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) { if (error2 instanceof Error) {
return error2; return error2.message;
} }
if (this.isDockerComposeResult(error2)) { if (this.isDockerComposeResult(error2)) {
const parts = []; const parts = [];
@ -27247,12 +27268,12 @@ var DockerComposeService = class {
parts.push("\nStandard output:"); parts.push("\nStandard output:");
parts.push(error2.out.trim()); parts.push(error2.out.trim());
} }
return new Error(parts.join("\n")); return parts.join("\n");
} }
if (typeof error2 === "string") { 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 * Type guard to check if an object is a docker-compose result
@ -27392,6 +27413,7 @@ async function run() {
const dockerComposeService = new DockerComposeService(); const dockerComposeService = new DockerComposeService();
const inputs = inputService.getInputs(); const inputs = inputService.getInputs();
try { try {
loggerService.debug("docker compose logs:");
const { error: error2 } = await dockerComposeService.logs({ const { error: error2 } = await dockerComposeService.logs({
dockerFlags: inputs.dockerFlags, dockerFlags: inputs.dockerFlags,
composeFiles: inputs.composeFiles, composeFiles: inputs.composeFiles,

View File

@ -134,6 +134,7 @@ describe("run", () => {
await run(); await run();
// Assert // Assert
expect(debugMock).toHaveBeenCalledWith("docker compose logs:");
expect(debugMock).toHaveBeenCalledWith( expect(debugMock).toHaveBeenCalledWith(
"docker compose error:\ntest logs error", "docker compose error:\ntest logs error",
); );

View File

@ -16,6 +16,8 @@ export async function run(): Promise<void> {
const inputs = inputService.getInputs(); const inputs = inputService.getInputs();
try { try {
loggerService.debug("docker compose logs:");
const { error } = await dockerComposeService.logs({ const { error } = await dockerComposeService.logs({
dockerFlags: inputs.dockerFlags, dockerFlags: inputs.dockerFlags,
composeFiles: inputs.composeFiles, composeFiles: inputs.composeFiles,

View File

@ -1,8 +1,8 @@
import { EventEmitter } from "node:events";
import type { import type {
IDockerComposeOptions, IDockerComposeOptions,
IDockerComposeResult, IDockerComposeResult,
} from "docker-compose"; } from "docker-compose";
import { EventEmitter } from "node:events";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock docker-compose before importing the module under test // Mock docker-compose before importing the module under test
@ -357,13 +357,13 @@ describe("DockerComposeService", () => {
}); });
describe("logs", () => { 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 debugMock = vi.fn();
const logsInputs = { const logsInputs = {
dockerFlags: [] as string[], dockerFlags: ["--context", "dev"] as string[],
composeFiles: ["docker-compose.yml"], composeFiles: ["docker-compose.yml"],
services: ["helloworld2", "helloworld3"], services: ["helloworld2", "helloworld3"],
composeFlags: [] as string[], composeFlags: ["--profile", "ci"] as string[],
cwd: "/current/working/dir", cwd: "/current/working/dir",
serviceLogger: debugMock, serviceLogger: debugMock,
}; };
@ -380,22 +380,31 @@ describe("DockerComposeService", () => {
const logsPromise = service.logs(logsInputs); const logsPromise = service.logs(logsInputs);
expect(spawnMock).toHaveBeenCalledWith("docker", [ expect(spawnMock).toHaveBeenCalledWith(
"docker",
[
"--context",
"dev",
"compose", "compose",
"--profile",
"ci",
"-f", "-f",
"docker-compose.yml", "docker-compose.yml",
"logs", "logs",
"helloworld2", "helloworld2",
"helloworld3", "helloworld3",
], { ],
{
cwd: "/current/working/dir", cwd: "/current/working/dir",
}); },
);
stdout.emit("data", Buffer.from("logs")); stdout.emit("data", Buffer.from("logs"));
stderr.emit("data", Buffer.from("error logs")); stderr.emit("data", Buffer.from("error logs"));
childProcess.emit("close", 0); childProcess.emit("close", 0);
await expect(logsPromise).resolves.toEqual({ error: "", output: "" }); await expect(logsPromise).resolves.toEqual({ error: "", output: "" });
expect(debugMock).toHaveBeenNthCalledWith(1, "logs"); expect(debugMock).toHaveBeenNthCalledWith(1, "logs");
expect(debugMock).toHaveBeenNthCalledWith(2, "error logs"); expect(debugMock).toHaveBeenNthCalledWith(2, "error logs");
}); });
@ -420,19 +429,6 @@ describe("DockerComposeService", () => {
const logsPromise = service.logs(logsInputs); 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); childProcess.emit("close", 1);
await expect(logsPromise).resolves.toEqual({ await expect(logsPromise).resolves.toEqual({
@ -516,7 +512,8 @@ describe("DockerComposeService", () => {
spawnMock.mockReturnValue(childProcess); spawnMock.mockReturnValue(childProcess);
await expect(service.logs(logsInputs)).resolves.toEqual({ 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: "", output: "",
}); });
}); });

View File

@ -59,52 +59,54 @@ export class DockerComposeService {
error: string; error: string;
output: string; output: string;
}> { }> {
const commandArgs = this.getDockerComposeCommandArgs("logs", { const options = this.getCommonOptions(optionsInputs);
dockerFlags: optionsInputs.dockerFlags, const { executablePath, executableArgs } =
composeFlags: optionsInputs.composeFlags, this.getDockerComposeCommandExecution("logs", services, options);
composeFiles: optionsInputs.composeFiles,
commandArgs: services,
});
return new Promise((resolve) => { return new Promise((resolve) => {
let settled = false; let settled = false;
const childProcess = spawn("docker", commandArgs, { const childProcess = spawn(executablePath, executableArgs, {
cwd: optionsInputs.cwd, cwd: options.cwd,
}); });
childProcess.on("error", (error) => { childProcess.on("error", (error) => {
if (settled) { if (settled) {
return; return;
} }
settled = true; settled = true;
resolve({ resolve({
error: `Unable to collect docker compose logs: ${error.message}`, error: `Unable to collect docker compose logs: ${error.message}`,
output: "", output: "",
}); });
}); });
if (!childProcess.stdout || !childProcess.stderr) { if (!childProcess.stdout || !childProcess.stderr) {
settled = true; settled = true;
resolve({ resolve({
error: "Unable to collect docker compose logs: stdout/stderr unavailable", error:
"Unable to collect docker compose logs: stdout/stderr unavailable",
output: "", output: "",
}); });
return; return;
} }
childProcess.stdout.on("data", (chunk: Buffer | string) => { childProcess.stdout.on("data", (chunk: Buffer | string) => {
optionsInputs.serviceLogger(chunk.toString()); options.callback?.(Buffer.from(chunk), "stdout");
}); });
childProcess.stderr.on("data", (chunk: Buffer | string) => { childProcess.stderr.on("data", (chunk: Buffer | string) => {
optionsInputs.serviceLogger(chunk.toString()); options.callback?.(Buffer.from(chunk), "stderr");
}); });
childProcess.on("close", (exitCode, signal) => { childProcess.on("close", (exitCode, signal) => {
if (settled) { if (settled) {
return; return;
} }
settled = true; settled = true;
resolve({ resolve({
error: error: signal
signal
? `Docker Compose logs command failed with signal ${signal}` ? `Docker Compose logs command failed with signal ${signal}`
: exitCode !== null && exitCode !== 0 : exitCode !== null && exitCode !== 0
? `Docker Compose logs command failed with exit code ${exitCode}` ? `Docker Compose logs command failed with exit code ${exitCode}`
@ -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 * Formats docker-compose errors into proper Error objects with readable messages
*/ */
private formatDockerComposeError(error: unknown): Error { private formatDockerComposeError(error: unknown): Error {
// If it's already an Error, return it return new Error(this.getDockerComposeErrorMessage(error));
if (error instanceof Error) { }
return 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<IDockerComposeOptions["executable"]>["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)) { if (this.isDockerComposeResult(error)) {
const parts: string[] = []; const parts: string[] = [];
// Add exit code information
if (error.exitCode !== null) { if (error.exitCode !== null) {
parts.push( parts.push(
`Docker Compose command failed with exit code ${error.exitCode}`, `Docker Compose command failed with exit code ${error.exitCode}`,
@ -183,30 +219,25 @@ export class DockerComposeService {
parts.push("Docker Compose command failed"); parts.push("Docker Compose command failed");
} }
// Add error stream output if available
if (error.err?.trim()) { if (error.err?.trim()) {
parts.push("\nError output:"); parts.push("\nError output:");
parts.push(error.err.trim()); parts.push(error.err.trim());
} }
// Add standard output if available and different from error output
if (error.out?.trim() && error.out !== error.err) { if (error.out?.trim() && error.out !== error.err) {
parts.push("\nStandard output:"); parts.push("\nStandard output:");
parts.push(error.out.trim()); parts.push(error.out.trim());
} }
return new Error(parts.join("\n")); return parts.join("\n");
} }
// Handle string errors
if (typeof error === "string") { if (typeof error === "string") {
return new Error(error); return error;
} }
// Fallback for unknown error types return JSON.stringify(error);
return new Error(JSON.stringify(error));
} }
/** /**
* Type guard to check if an object is a docker-compose result * Type guard to check if an object is a docker-compose result
*/ */