mirror of
https://github.com/hoverkraft-tech/compose-action.git
synced 2026-08-31 23:52:49 +08:00
fix: stream compose logs during post cleanup
Co-authored-by: neilime <314088+neilime@users.noreply.github.com>
This commit is contained in:
parent
e56276d1d3
commit
26f805aa21
53
dist/index.js
generated
vendored
53
dist/index.js
generated
vendored
@ -26549,7 +26549,7 @@ var require_dist2 = __commonJS({
|
|||||||
return (0, exports.restartMany)([service], options);
|
return (0, exports.restartMany)([service], options);
|
||||||
};
|
};
|
||||||
exports.restartOne = restartOne;
|
exports.restartOne = restartOne;
|
||||||
var logs2 = function(services, options = {}) {
|
var logs = function(services, options = {}) {
|
||||||
const args = Array.isArray(services) ? services : [services];
|
const args = Array.isArray(services) ? services : [services];
|
||||||
if (options.follow) {
|
if (options.follow) {
|
||||||
args.unshift("--follow");
|
args.unshift("--follow");
|
||||||
@ -26559,7 +26559,7 @@ var require_dist2 = __commonJS({
|
|||||||
}
|
}
|
||||||
return (0, exports.execCompose)("logs", args, options);
|
return (0, exports.execCompose)("logs", args, options);
|
||||||
};
|
};
|
||||||
exports.logs = logs2;
|
exports.logs = logs;
|
||||||
var port = async function(service, containerPort, options) {
|
var port = async function(service, containerPort, options) {
|
||||||
const args = [service, containerPort];
|
const args = [service, containerPort];
|
||||||
try {
|
try {
|
||||||
@ -31374,6 +31374,7 @@ function info(message) {
|
|||||||
|
|
||||||
// src/services/docker-compose.service.ts
|
// src/services/docker-compose.service.ts
|
||||||
var import_docker_compose = __toESM(require_dist2(), 1);
|
var import_docker_compose = __toESM(require_dist2(), 1);
|
||||||
|
import { spawn as spawn2 } from "node:child_process";
|
||||||
var DockerComposeService = class {
|
var DockerComposeService = class {
|
||||||
async up({ upFlags, services, ...optionsInputs }) {
|
async up({ upFlags, services, ...optionsInputs }) {
|
||||||
const options = {
|
const options = {
|
||||||
@ -31402,15 +31403,30 @@ var DockerComposeService = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async logs({ services, ...optionsInputs }) {
|
async logs({ services, ...optionsInputs }) {
|
||||||
const options = {
|
const commandArgs = this.getDockerComposeCommandArgs("logs", {
|
||||||
...this.getCommonOptions(optionsInputs),
|
dockerFlags: optionsInputs.dockerFlags,
|
||||||
follow: false
|
composeFlags: optionsInputs.composeFlags,
|
||||||
};
|
composeFiles: optionsInputs.composeFiles,
|
||||||
const { err, out } = await (0, import_docker_compose.logs)(services, options);
|
commandArgs: services
|
||||||
return {
|
});
|
||||||
error: err,
|
return await new Promise((resolve2, reject) => {
|
||||||
output: out
|
const childProcess = spawn2("docker", commandArgs, {
|
||||||
};
|
cwd: optionsInputs.cwd
|
||||||
|
});
|
||||||
|
childProcess.on("error", reject);
|
||||||
|
childProcess.stdout.on("data", (chunk) => {
|
||||||
|
optionsInputs.serviceLogger(chunk.toString());
|
||||||
|
});
|
||||||
|
childProcess.stderr.on("data", (chunk) => {
|
||||||
|
optionsInputs.serviceLogger(chunk.toString());
|
||||||
|
});
|
||||||
|
childProcess.on("close", (exitCode) => {
|
||||||
|
resolve2({
|
||||||
|
error: exitCode && exitCode !== 0 ? `Docker Compose logs command failed with exit code ${exitCode}` : "",
|
||||||
|
output: ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
getCommonOptions({
|
getCommonOptions({
|
||||||
dockerFlags,
|
dockerFlags,
|
||||||
@ -31430,6 +31446,21 @@ var DockerComposeService = class {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
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
|
||||||
*/
|
*/
|
||||||
|
|||||||
81
dist/post.js
generated
vendored
81
dist/post.js
generated
vendored
@ -26549,7 +26549,7 @@ var require_dist2 = __commonJS({
|
|||||||
return (0, exports.restartMany)([service], options);
|
return (0, exports.restartMany)([service], options);
|
||||||
};
|
};
|
||||||
exports.restartOne = restartOne;
|
exports.restartOne = restartOne;
|
||||||
var logs2 = function(services, options = {}) {
|
var logs = function(services, options = {}) {
|
||||||
const args = Array.isArray(services) ? services : [services];
|
const args = Array.isArray(services) ? services : [services];
|
||||||
if (options.follow) {
|
if (options.follow) {
|
||||||
args.unshift("--follow");
|
args.unshift("--follow");
|
||||||
@ -26559,7 +26559,7 @@ var require_dist2 = __commonJS({
|
|||||||
}
|
}
|
||||||
return (0, exports.execCompose)("logs", args, options);
|
return (0, exports.execCompose)("logs", args, options);
|
||||||
};
|
};
|
||||||
exports.logs = logs2;
|
exports.logs = logs;
|
||||||
var port = async function(service, containerPort, options) {
|
var port = async function(service, containerPort, options) {
|
||||||
const args = [service, containerPort];
|
const args = [service, containerPort];
|
||||||
try {
|
try {
|
||||||
@ -27111,6 +27111,7 @@ function info(message) {
|
|||||||
|
|
||||||
// src/services/docker-compose.service.ts
|
// src/services/docker-compose.service.ts
|
||||||
var import_docker_compose = __toESM(require_dist2(), 1);
|
var import_docker_compose = __toESM(require_dist2(), 1);
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
var DockerComposeService = class {
|
var DockerComposeService = class {
|
||||||
async up({ upFlags, services, ...optionsInputs }) {
|
async up({ upFlags, services, ...optionsInputs }) {
|
||||||
const options = {
|
const options = {
|
||||||
@ -27139,15 +27140,30 @@ var DockerComposeService = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async logs({ services, ...optionsInputs }) {
|
async logs({ services, ...optionsInputs }) {
|
||||||
const options = {
|
const commandArgs = this.getDockerComposeCommandArgs("logs", {
|
||||||
...this.getCommonOptions(optionsInputs),
|
dockerFlags: optionsInputs.dockerFlags,
|
||||||
follow: false
|
composeFlags: optionsInputs.composeFlags,
|
||||||
};
|
composeFiles: optionsInputs.composeFiles,
|
||||||
const { err, out } = await (0, import_docker_compose.logs)(services, options);
|
commandArgs: services
|
||||||
return {
|
});
|
||||||
error: err,
|
return await new Promise((resolve, reject) => {
|
||||||
output: out
|
const childProcess = spawn("docker", commandArgs, {
|
||||||
};
|
cwd: optionsInputs.cwd
|
||||||
|
});
|
||||||
|
childProcess.on("error", reject);
|
||||||
|
childProcess.stdout.on("data", (chunk) => {
|
||||||
|
optionsInputs.serviceLogger(chunk.toString());
|
||||||
|
});
|
||||||
|
childProcess.stderr.on("data", (chunk) => {
|
||||||
|
optionsInputs.serviceLogger(chunk.toString());
|
||||||
|
});
|
||||||
|
childProcess.on("close", (exitCode) => {
|
||||||
|
resolve({
|
||||||
|
error: exitCode && exitCode !== 0 ? `Docker Compose logs command failed with exit code ${exitCode}` : "",
|
||||||
|
output: ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
getCommonOptions({
|
getCommonOptions({
|
||||||
dockerFlags,
|
dockerFlags,
|
||||||
@ -27167,6 +27183,21 @@ var DockerComposeService = class {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
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
|
||||||
*/
|
*/
|
||||||
@ -27335,20 +27366,24 @@ async function run() {
|
|||||||
const inputService = new InputService();
|
const inputService = new InputService();
|
||||||
const dockerComposeService = new DockerComposeService();
|
const dockerComposeService = new DockerComposeService();
|
||||||
const inputs = inputService.getInputs();
|
const inputs = inputService.getInputs();
|
||||||
const { error: error2, output } = await dockerComposeService.logs({
|
try {
|
||||||
dockerFlags: inputs.dockerFlags,
|
const { error: error2 } = await dockerComposeService.logs({
|
||||||
composeFiles: inputs.composeFiles,
|
dockerFlags: inputs.dockerFlags,
|
||||||
composeFlags: inputs.composeFlags,
|
composeFiles: inputs.composeFiles,
|
||||||
cwd: inputs.cwd,
|
composeFlags: inputs.composeFlags,
|
||||||
services: inputs.services,
|
cwd: inputs.cwd,
|
||||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel)
|
services: inputs.services,
|
||||||
});
|
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel)
|
||||||
if (error2) {
|
});
|
||||||
loggerService.debug(`docker compose error:
|
if (error2) {
|
||||||
|
loggerService.debug(`docker compose error:
|
||||||
${error2}`);
|
${error2}`);
|
||||||
|
}
|
||||||
|
} catch (error2) {
|
||||||
|
loggerService.warn(
|
||||||
|
`Unable to collect docker compose logs before cleanup: ${error2 instanceof Error ? error2.message : JSON.stringify(error2)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
loggerService.debug(`docker compose logs:
|
|
||||||
${output}`);
|
|
||||||
await dockerComposeService.down({
|
await dockerComposeService.down({
|
||||||
dockerFlags: inputs.dockerFlags,
|
dockerFlags: inputs.dockerFlags,
|
||||||
composeFiles: inputs.composeFiles,
|
composeFiles: inputs.composeFiles,
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const { DockerComposeService } = await import(
|
|||||||
describe("run", () => {
|
describe("run", () => {
|
||||||
let infoMock: ReturnType<typeof vi.spyOn>;
|
let infoMock: ReturnType<typeof vi.spyOn>;
|
||||||
let debugMock: ReturnType<typeof vi.spyOn>;
|
let debugMock: ReturnType<typeof vi.spyOn>;
|
||||||
|
let warnMock: ReturnType<typeof vi.spyOn>;
|
||||||
let getInputsMock: ReturnType<typeof vi.spyOn>;
|
let getInputsMock: ReturnType<typeof vi.spyOn>;
|
||||||
let serviceDownMock: ReturnType<typeof vi.spyOn>;
|
let serviceDownMock: ReturnType<typeof vi.spyOn>;
|
||||||
let serviceLogsMock: ReturnType<typeof vi.spyOn>;
|
let serviceLogsMock: ReturnType<typeof vi.spyOn>;
|
||||||
@ -55,12 +56,15 @@ describe("run", () => {
|
|||||||
debugMock = vi
|
debugMock = vi
|
||||||
.spyOn(LoggerService.prototype, "debug")
|
.spyOn(LoggerService.prototype, "debug")
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
warnMock = vi
|
||||||
|
.spyOn(LoggerService.prototype, "warn")
|
||||||
|
.mockImplementation(() => {});
|
||||||
getInputsMock = vi.spyOn(InputService.prototype, "getInputs");
|
getInputsMock = vi.spyOn(InputService.prototype, "getInputs");
|
||||||
serviceDownMock = vi.spyOn(DockerComposeService.prototype, "down");
|
serviceDownMock = vi.spyOn(DockerComposeService.prototype, "down");
|
||||||
serviceLogsMock = vi.spyOn(DockerComposeService.prototype, "logs");
|
serviceLogsMock = vi.spyOn(DockerComposeService.prototype, "logs");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bring down docker compose service(s) and log output", async () => {
|
it("should bring down docker compose service(s)", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
getInputsMock.mockImplementation(() => ({
|
getInputsMock.mockImplementation(() => ({
|
||||||
dockerFlags: [],
|
dockerFlags: [],
|
||||||
@ -75,7 +79,7 @@ describe("run", () => {
|
|||||||
serviceLogLevel: LogLevel.Debug,
|
serviceLogLevel: LogLevel.Debug,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
serviceLogsMock.mockResolvedValue({ error: "", output: "test logs" });
|
serviceLogsMock.mockResolvedValue({ error: "", output: "" });
|
||||||
serviceDownMock.mockResolvedValue();
|
serviceDownMock.mockResolvedValue();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -100,12 +104,11 @@ describe("run", () => {
|
|||||||
serviceLogger: debugMock,
|
serviceLogger: debugMock,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(debugMock).toHaveBeenCalledWith("docker compose logs:\ntest logs");
|
|
||||||
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
||||||
expect(setFailedMock).not.toHaveBeenCalled();
|
expect(setFailedMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log docker composer errors if any", async () => {
|
it("should log docker compose command errors if any", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
getInputsMock.mockImplementation(() => ({
|
getInputsMock.mockImplementation(() => ({
|
||||||
dockerFlags: [],
|
dockerFlags: [],
|
||||||
@ -134,12 +137,36 @@ describe("run", () => {
|
|||||||
expect(debugMock).toHaveBeenCalledWith(
|
expect(debugMock).toHaveBeenCalledWith(
|
||||||
"docker compose error:\ntest logs error",
|
"docker compose error:\ntest logs error",
|
||||||
);
|
);
|
||||||
expect(debugMock).toHaveBeenCalledWith(
|
|
||||||
"docker compose logs:\ntest logs output",
|
|
||||||
);
|
|
||||||
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should continue cleanup when collecting logs fails", async () => {
|
||||||
|
getInputsMock.mockImplementation(() => ({
|
||||||
|
dockerFlags: [],
|
||||||
|
composeFiles: ["docker-compose.yml"],
|
||||||
|
services: [],
|
||||||
|
composeFlags: [],
|
||||||
|
upFlags: [],
|
||||||
|
downFlags: [],
|
||||||
|
cwd: "/current/working/dir",
|
||||||
|
composeVersion: null,
|
||||||
|
githubToken: null,
|
||||||
|
serviceLogLevel: LogLevel.Debug,
|
||||||
|
}));
|
||||||
|
|
||||||
|
serviceLogsMock.mockRejectedValue(new Error("Test logs error"));
|
||||||
|
serviceDownMock.mockResolvedValue();
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(warnMock).toHaveBeenCalledWith(
|
||||||
|
"Unable to collect docker compose logs before cleanup: Test logs error",
|
||||||
|
);
|
||||||
|
expect(serviceDownMock).toHaveBeenCalled();
|
||||||
|
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
||||||
|
expect(setFailedMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("should set failed when an error occurs", async () => {
|
it("should set failed when an error occurs", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
getInputsMock.mockImplementation(() => {
|
getInputsMock.mockImplementation(() => {
|
||||||
|
|||||||
@ -15,21 +15,25 @@ export async function run(): Promise<void> {
|
|||||||
|
|
||||||
const inputs = inputService.getInputs();
|
const inputs = inputService.getInputs();
|
||||||
|
|
||||||
const { error, output } = await dockerComposeService.logs({
|
try {
|
||||||
dockerFlags: inputs.dockerFlags,
|
const { error } = await dockerComposeService.logs({
|
||||||
composeFiles: inputs.composeFiles,
|
dockerFlags: inputs.dockerFlags,
|
||||||
composeFlags: inputs.composeFlags,
|
composeFiles: inputs.composeFiles,
|
||||||
cwd: inputs.cwd,
|
composeFlags: inputs.composeFlags,
|
||||||
services: inputs.services,
|
cwd: inputs.cwd,
|
||||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
services: inputs.services,
|
||||||
});
|
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
loggerService.debug(`docker compose error:\n${error}`);
|
loggerService.debug(`docker compose error:\n${error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
loggerService.warn(
|
||||||
|
`Unable to collect docker compose logs before cleanup: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loggerService.debug(`docker compose logs:\n${output}`);
|
|
||||||
|
|
||||||
await dockerComposeService.down({
|
await dockerComposeService.down({
|
||||||
dockerFlags: inputs.dockerFlags,
|
dockerFlags: inputs.dockerFlags,
|
||||||
composeFiles: inputs.composeFiles,
|
composeFiles: inputs.composeFiles,
|
||||||
|
|||||||
@ -73,7 +73,7 @@ describe("post", () => {
|
|||||||
serviceLogLevel: LogLevel.Debug,
|
serviceLogLevel: LogLevel.Debug,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
serviceLogsMock.mockResolvedValue({ error: "", output: "test logs" });
|
serviceLogsMock.mockResolvedValue({ error: "", output: "" });
|
||||||
serviceDownMock.mockResolvedValueOnce();
|
serviceDownMock.mockResolvedValueOnce();
|
||||||
|
|
||||||
await import("./post.js");
|
await import("./post.js");
|
||||||
@ -97,10 +97,6 @@ describe("post", () => {
|
|||||||
serviceLogger: debugMock,
|
serviceLogger: debugMock,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(debugMock).toHaveBeenNthCalledWith(
|
|
||||||
1,
|
|
||||||
"docker compose logs:\ntest logs",
|
|
||||||
);
|
|
||||||
expect(infoMock).toHaveBeenNthCalledWith(1, "docker compose is down");
|
expect(infoMock).toHaveBeenNthCalledWith(1, "docker compose is down");
|
||||||
|
|
||||||
expect(setFailedMock).not.toHaveBeenCalled();
|
expect(setFailedMock).not.toHaveBeenCalled();
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
IDockerComposeLogOptions,
|
|
||||||
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
|
||||||
@ -17,19 +17,16 @@ const upManyMock =
|
|||||||
>();
|
>();
|
||||||
const downMock =
|
const downMock =
|
||||||
vi.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
vi.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
||||||
const logsMock =
|
const spawnMock = vi.fn();
|
||||||
vi.fn<
|
|
||||||
(
|
|
||||||
services: string[],
|
|
||||||
options: IDockerComposeLogOptions,
|
|
||||||
) => Promise<IDockerComposeResult>
|
|
||||||
>();
|
|
||||||
|
|
||||||
vi.doMock("docker-compose", () => ({
|
vi.doMock("docker-compose", () => ({
|
||||||
upAll: upAllMock,
|
upAll: upAllMock,
|
||||||
upMany: upManyMock,
|
upMany: upManyMock,
|
||||||
down: downMock,
|
down: downMock,
|
||||||
logs: logsMock,
|
}));
|
||||||
|
|
||||||
|
vi.doMock("node:child_process", () => ({
|
||||||
|
spawn: spawnMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Dynamic import after mock setup
|
// Dynamic import after mock setup
|
||||||
@ -360,7 +357,7 @@ describe("DockerComposeService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("logs", () => {
|
describe("logs", () => {
|
||||||
it("should call logs with correct options", async () => {
|
it("should stream logs with correct command arguments", async () => {
|
||||||
const debugMock = vi.fn();
|
const debugMock = vi.fn();
|
||||||
const logsInputs = {
|
const logsInputs = {
|
||||||
dockerFlags: [] as string[],
|
dockerFlags: [] as string[],
|
||||||
@ -371,20 +368,76 @@ describe("DockerComposeService", () => {
|
|||||||
serviceLogger: debugMock,
|
serviceLogger: debugMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
logsMock.mockResolvedValue({ exitCode: 0, err: "", out: "logs" });
|
const stdout = new EventEmitter();
|
||||||
|
const stderr = new EventEmitter();
|
||||||
|
const childProcess = new EventEmitter() as EventEmitter & {
|
||||||
|
stdout: EventEmitter;
|
||||||
|
stderr: EventEmitter;
|
||||||
|
};
|
||||||
|
childProcess.stdout = stdout;
|
||||||
|
childProcess.stderr = stderr;
|
||||||
|
spawnMock.mockReturnValue(childProcess);
|
||||||
|
|
||||||
await service.logs(logsInputs);
|
const logsPromise = service.logs(logsInputs);
|
||||||
|
|
||||||
expect(logsMock).toHaveBeenCalledWith(["helloworld2", "helloworld3"], {
|
expect(spawnMock).toHaveBeenCalledWith("docker", [
|
||||||
composeOptions: [],
|
"compose",
|
||||||
config: ["docker-compose.yml"],
|
"-f",
|
||||||
|
"docker-compose.yml",
|
||||||
|
"logs",
|
||||||
|
"helloworld2",
|
||||||
|
"helloworld3",
|
||||||
|
], {
|
||||||
cwd: "/current/working/dir",
|
cwd: "/current/working/dir",
|
||||||
executable: {
|
});
|
||||||
executablePath: "docker",
|
|
||||||
options: [],
|
stdout.emit("data", Buffer.from("logs"));
|
||||||
},
|
stderr.emit("data", Buffer.from("error logs"));
|
||||||
follow: false,
|
childProcess.emit("close", 0);
|
||||||
callback: expect.any(Function),
|
|
||||||
|
await expect(logsPromise).resolves.toEqual({ error: "", output: "" });
|
||||||
|
expect(debugMock).toHaveBeenNthCalledWith(1, "logs");
|
||||||
|
expect(debugMock).toHaveBeenNthCalledWith(2, "error logs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a non-fatal error message when logs command fails", async () => {
|
||||||
|
const logsInputs = {
|
||||||
|
dockerFlags: ["--context", "dev"] as string[],
|
||||||
|
composeFiles: ["docker-compose.yml"] as string[],
|
||||||
|
services: [] as string[],
|
||||||
|
composeFlags: ["--profile", "ci"] as string[],
|
||||||
|
cwd: "/current/working/dir",
|
||||||
|
serviceLogger: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const childProcess = new EventEmitter() as EventEmitter & {
|
||||||
|
stdout: EventEmitter;
|
||||||
|
stderr: EventEmitter;
|
||||||
|
};
|
||||||
|
childProcess.stdout = new EventEmitter();
|
||||||
|
childProcess.stderr = new EventEmitter();
|
||||||
|
spawnMock.mockReturnValue(childProcess);
|
||||||
|
|
||||||
|
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({
|
||||||
|
error: "Docker Compose logs command failed with exit code 1",
|
||||||
|
output: "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
import {
|
import {
|
||||||
down,
|
down,
|
||||||
type IDockerComposeLogOptions,
|
|
||||||
type IDockerComposeOptions,
|
type IDockerComposeOptions,
|
||||||
type IDockerComposeResult,
|
type IDockerComposeResult,
|
||||||
logs,
|
|
||||||
upAll,
|
upAll,
|
||||||
upMany,
|
upMany,
|
||||||
} from "docker-compose";
|
} from "docker-compose";
|
||||||
@ -60,17 +59,35 @@ export class DockerComposeService {
|
|||||||
error: string;
|
error: string;
|
||||||
output: string;
|
output: string;
|
||||||
}> {
|
}> {
|
||||||
const options: IDockerComposeLogOptions = {
|
const commandArgs = this.getDockerComposeCommandArgs("logs", {
|
||||||
...this.getCommonOptions(optionsInputs),
|
dockerFlags: optionsInputs.dockerFlags,
|
||||||
follow: false,
|
composeFlags: optionsInputs.composeFlags,
|
||||||
};
|
composeFiles: optionsInputs.composeFiles,
|
||||||
|
commandArgs: services,
|
||||||
|
});
|
||||||
|
|
||||||
const { err, out } = await logs(services, options);
|
return await new Promise((resolve, reject) => {
|
||||||
|
const childProcess = spawn("docker", commandArgs, {
|
||||||
|
cwd: optionsInputs.cwd,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
childProcess.on("error", reject);
|
||||||
error: err,
|
childProcess.stdout.on("data", (chunk: Buffer) => {
|
||||||
output: out,
|
optionsInputs.serviceLogger(chunk.toString());
|
||||||
};
|
});
|
||||||
|
childProcess.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
optionsInputs.serviceLogger(chunk.toString());
|
||||||
|
});
|
||||||
|
childProcess.on("close", (exitCode) => {
|
||||||
|
resolve({
|
||||||
|
error:
|
||||||
|
exitCode && exitCode !== 0
|
||||||
|
? `Docker Compose logs command failed with exit code ${exitCode}`
|
||||||
|
: "",
|
||||||
|
output: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCommonOptions({
|
private getCommonOptions({
|
||||||
@ -92,6 +109,30 @@ export class DockerComposeService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDockerComposeCommandArgs(
|
||||||
|
command: string,
|
||||||
|
{
|
||||||
|
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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user