Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
6f330cd1dc 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>
2026-08-18 16:29:33 +02:00
copilot-swe-agent[bot]
75b024b03b fix: avoid buffering compose logs during cleanup
Co-authored-by: neilime <314088+neilime@users.noreply.github.com>
2026-08-18 15:39:05 +02:00
copilot-swe-agent[bot]
26f805aa21 fix: stream compose logs during post cleanup
Co-authored-by: neilime <314088+neilime@users.noreply.github.com>
2026-08-18 15:39:05 +02:00
Emilien Escalle
e56276d1d3 fix: lint issues
Some checks failed
Internal - Main - Continuous Integration / ci (push) Has been cancelled
Need fix to Issue / main (push) Has been cancelled
Prepare release / release (push) Has been cancelled
Internal - Main - Continuous Integration / prepare-docs (push) Has been cancelled
Internal - Main - Continuous Integration / sync-docs (push) Has been cancelled
Mark stale issues and pull requests / main (push) Has been cancelled
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
2026-08-18 15:24:47 +02:00
7 changed files with 226 additions and 153 deletions

3
.github/linters/.codespellrc vendored Normal file
View File

@ -0,0 +1,3 @@
[codespell]
# Skip third-party/generated paths and French-locale content files.
skip = .git,*.svg,**/package-lock.json,**/dist/**,**/node_modules/**,**/coverage/**,**/junit.xml

View File

@ -21,7 +21,7 @@ jobs:
- name: Build dist/ Directory
id: package
run: ${{ steps.setup-node.outputs.run-script-command }} package
run: npm run package
# This will fail the workflow if the PR wasn't created by Dependabot.
- name: Compare Directories

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

85
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

6
package-lock.json generated
View File

@ -3145,9 +3145,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.17",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{

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
*/