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 }) {
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

86
dist/post.js generated vendored
View File

@ -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,

View File

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

View File

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

View File

@ -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", [
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: "",
});
});

View File

@ -59,52 +59,54 @@ 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
error: signal
? `Docker Compose logs command failed with signal ${signal}`
: exitCode !== null && exitCode !== 0
? `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
*/
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<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)) {
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
*/