diff --git a/build.gradle.kts b/build.gradle.kts index 5af6d6b..a3f9c3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ description = "Flamingock CLI for executing changes in applications" val jacksonVersion = "2.16.0" val picocliVersion = "4.7.5" -val flamingockVersion = "1.1.0" +val flamingockVersion = "1.3.0" repositories { mavenLocal() // For local development with unpublished versions diff --git a/src/main/java/io/flamingock/cli/executor/command/ApplyCommand.java b/src/main/java/io/flamingock/cli/executor/command/ApplyCommand.java index 5a707a5..f9737b7 100644 --- a/src/main/java/io/flamingock/cli/executor/command/ApplyCommand.java +++ b/src/main/java/io/flamingock/cli/executor/command/ApplyCommand.java @@ -21,8 +21,11 @@ import io.flamingock.cli.executor.orchestration.ExecutionOptions; import io.flamingock.cli.executor.output.ConsoleFormatter; import io.flamingock.cli.executor.output.ExecutionResultFormatter; +import io.flamingock.cli.executor.output.PendingChangesFormatter; +import io.flamingock.cli.executor.output.PipelineAbortedFormatter; import io.flamingock.cli.executor.util.VersionProvider; import io.flamingock.internal.common.core.operation.OperationType; +import io.flamingock.internal.common.core.response.ResponseError; import io.flamingock.internal.common.core.response.data.ExecuteResponseData; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; @@ -131,7 +134,6 @@ public Integer call() { if (result.isSuccess()) { if (!quiet) { - // Print detailed execution summary ExecuteResponseData data = result.getData(); if (data != null) { ExecutionResultFormatter.print(data); @@ -141,15 +143,32 @@ public Integer call() { } return 0; } else { - // Print execution summary if available (even on failure, shows what was applied) - if (!quiet && result.getData() != null) { - ExecutionResultFormatter.print(result.getData()); + if (!quiet) { + if (result.getData() != null) { + ExecutionResultFormatter.print(result.getData()); + } + ResponseError error = new ResponseError( + result.getErrorCode(), + result.getErrorMessage(), + false + ); + printEnvelopeError(error); } - ConsoleFormatter.printFailure(result.getErrorCode(), result.getErrorMessage()); return result.getExitCode(); } } + private static void printEnvelopeError(ResponseError error) { + String code = error.getCode(); + if ("LOCK_ERROR".equals(code)) { + PipelineAbortedFormatter.print(error); + } else if ("PENDING_CHANGES".equals(code)) { + PendingChangesFormatter.print(error); + } else { + ConsoleFormatter.printFailure(error.getCode(), error.getMessage()); + } + } + private FlamingockExecutorCli getRootCommand() { return parent != null ? parent.getParent() : null; } diff --git a/src/main/java/io/flamingock/cli/executor/output/ExecutionResultFormatter.java b/src/main/java/io/flamingock/cli/executor/output/ExecutionResultFormatter.java index 5653678..0576181 100644 --- a/src/main/java/io/flamingock/cli/executor/output/ExecutionResultFormatter.java +++ b/src/main/java/io/flamingock/cli/executor/output/ExecutionResultFormatter.java @@ -126,7 +126,6 @@ private static String formatStatus(ExecutionStatus status) { case FAILED: return "FAILED"; case PARTIAL: - return "PARTIAL"; case NO_CHANGES: return "NO CHANGES"; default: diff --git a/src/main/java/io/flamingock/cli/executor/output/PendingChangesFormatter.java b/src/main/java/io/flamingock/cli/executor/output/PendingChangesFormatter.java new file mode 100644 index 0000000..6731ba4 --- /dev/null +++ b/src/main/java/io/flamingock/cli/executor/output/PendingChangesFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.cli.executor.output; + +import io.flamingock.internal.common.core.response.ResponseError; + +/** + * Renders the "pending changes detected" failure scenario from an envelope-level {@link ResponseError}. + */ +public final class PendingChangesFormatter { + + private static final String SEPARATOR = "--------------------------------------------------------------------------------"; + + private PendingChangesFormatter() { + } + + public static String format(ResponseError error) { + StringBuilder sb = new StringBuilder("\n"); + sb.append(SEPARATOR).append("\n"); + sb.append("PENDING CHANGES DETECTED").append("\n"); + sb.append(SEPARATOR).append("\n"); + if (error != null && error.getMessage() != null) { + sb.append(String.format(" Message: %s%n", error.getMessage())); + } + sb.append(SEPARATOR).append("\n"); + return sb.toString(); + } + + public static void print(ResponseError error) { + System.out.print(format(error)); + } +} diff --git a/src/main/java/io/flamingock/cli/executor/output/PipelineAbortedFormatter.java b/src/main/java/io/flamingock/cli/executor/output/PipelineAbortedFormatter.java new file mode 100644 index 0000000..5f264ec --- /dev/null +++ b/src/main/java/io/flamingock/cli/executor/output/PipelineAbortedFormatter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.cli.executor.output; + +import io.flamingock.internal.common.core.response.ResponseError; + +/** + * Renders the "pipeline aborted" failure scenario from an envelope-level {@link ResponseError}. + */ +public final class PipelineAbortedFormatter { + + private static final String SEPARATOR = "--------------------------------------------------------------------------------"; + + private PipelineAbortedFormatter() { + } + + public static String format(ResponseError error) { + StringBuilder sb = new StringBuilder("\n"); + sb.append(SEPARATOR).append("\n"); + sb.append("PIPELINE ABORTED").append("\n"); + sb.append(SEPARATOR).append("\n"); + if (error != null) { + if (error.getCode() != null) { + sb.append(String.format(" Type: %s%n", error.getCode())); + } + if (error.getMessage() != null) { + sb.append(String.format(" Message: %s%n", error.getMessage())); + } + } + sb.append(SEPARATOR).append("\n"); + return sb.toString(); + } + + public static void print(ResponseError error) { + System.out.print(format(error)); + } +} diff --git a/src/main/java/io/flamingock/cli/executor/output/TableFormatter.java b/src/main/java/io/flamingock/cli/executor/output/TableFormatter.java index 930e918..bb2c1f7 100644 --- a/src/main/java/io/flamingock/cli/executor/output/TableFormatter.java +++ b/src/main/java/io/flamingock/cli/executor/output/TableFormatter.java @@ -183,7 +183,7 @@ private void printDataRow(AuditEntryDto entry, List columns, boolea private String getColumnValue(AuditEntryDto entry, int columnIndex) { switch (columnIndex) { case 0: // Change ID - return entry.getTaskId(); + return entry.getChangeId(); case 2: // Author return entry.getAuthor(); case 3: // Time @@ -196,7 +196,7 @@ private String getColumnValue(AuditEntryDto entry, int columnIndex) { private String getExtendedColumnValue(AuditEntryDto entry, int columnIndex) { switch (columnIndex) { case 0: // Change ID - return entry.getTaskId(); + return entry.getChangeId(); case 2: // Exec ID return truncate(entry.getExecutionId(), EXECUTION_ID_WIDTH); case 3: // Author diff --git a/src/test/java/io/flamingock/cli/executor/result/ResponseResultReaderTest.java b/src/test/java/io/flamingock/cli/executor/result/ResponseResultReaderTest.java index d9aa04c..bae418f 100644 --- a/src/test/java/io/flamingock/cli/executor/result/ResponseResultReaderTest.java +++ b/src/test/java/io/flamingock/cli/executor/result/ResponseResultReaderTest.java @@ -103,14 +103,14 @@ void shouldParseValidAuditListResponse() throws IOException { " \"@type\": \"audit_list\",\n" + " \"entries\": [\n" + " {\n" + - " \"taskId\": \"change-001\",\n" + + " \"changeId\": \"change-001\",\n" + " \"author\": \"developer\",\n" + " \"state\": \"APPLIED\",\n" + " \"stageId\": \"stage-1\",\n" + " \"executionMillis\": 100\n" + " },\n" + " {\n" + - " \"taskId\": \"change-002\",\n" + + " \"changeId\": \"change-002\",\n" + " \"author\": \"developer\",\n" + " \"state\": \"APPLIED\",\n" + " \"stageId\": \"stage-1\",\n" + @@ -132,7 +132,7 @@ void shouldParseValidAuditListResponse() throws IOException { assertNotNull(result.getData()); assertNotNull(result.getData().getEntries()); assertEquals(2, result.getData().getEntries().size()); - assertEquals("change-001", result.getData().getEntries().get(0).getTaskId()); + assertEquals("change-001", result.getData().getEntries().get(0).getChangeId()); assertEquals("developer", result.getData().getEntries().get(0).getAuthor()); assertEquals("APPLIED", result.getData().getEntries().get(0).getState()); assertEquals(250, result.getDurationMs()); @@ -151,6 +151,63 @@ void shouldReturnEmptyWhenFileNotFound() { assertFalse(result.isPresent()); } + @Test + @DisplayName("Should parse envelope-level lock failure response") + void shouldParseLockFailureResponse() throws IOException { + String json = "{\n" + + " \"success\": false,\n" + + " \"operation\": \"EXECUTE_APPLY\",\n" + + " \"timestamp\": \"2026-02-09T10:00:00Z\",\n" + + " \"durationMs\": 50,\n" + + " \"data\": null,\n" + + " \"error\": {\n" + + " \"code\": \"LOCK_ERROR\",\n" + + " \"message\": \"lock not acquired\",\n" + + " \"recoverable\": true\n" + + " }\n" + + "}"; + + Path responseFile = tempDir.resolve("response.json"); + Files.write(responseFile, json.getBytes(StandardCharsets.UTF_8)); + + Optional envelope = reader.read(responseFile); + + assertTrue(envelope.isPresent()); + assertFalse(envelope.get().isSuccess()); + assertNotNull(envelope.get().getError()); + assertEquals("LOCK_ERROR", envelope.get().getError().getCode()); + assertEquals("lock not acquired", envelope.get().getError().getMessage()); + assertTrue(envelope.get().getError().isRecoverable()); + } + + @Test + @DisplayName("Should parse envelope-level pending changes failure response") + void shouldParsePendingChangesFailureResponse() throws IOException { + String json = "{\n" + + " \"success\": false,\n" + + " \"operation\": \"EXECUTE_VALIDATE\",\n" + + " \"timestamp\": \"2026-02-09T10:00:00Z\",\n" + + " \"durationMs\": 10,\n" + + " \"data\": null,\n" + + " \"error\": {\n" + + " \"code\": \"PENDING_CHANGES\",\n" + + " \"message\": \"pending changes detected\",\n" + + " \"recoverable\": false\n" + + " }\n" + + "}"; + + Path responseFile = tempDir.resolve("response.json"); + Files.write(responseFile, json.getBytes(StandardCharsets.UTF_8)); + + Optional envelope = reader.read(responseFile); + + assertTrue(envelope.isPresent()); + assertFalse(envelope.get().isSuccess()); + assertNotNull(envelope.get().getError()); + assertEquals("PENDING_CHANGES", envelope.get().getError().getCode()); + assertEquals("pending changes detected", envelope.get().getError().getMessage()); + } + @Test @DisplayName("Should handle corrupt JSON gracefully") void shouldHandleCorruptJson() throws IOException {