This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 6d21e50f90d1 CAMEL-26930: camel-jbang - camel run exits with 1 when
the integration fails to start (#29913)
6d21e50f90d1 is described below
commit 6d21e50f90d150d05f1055bb4591c9165bb2d827
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 11:37:12 2026 +0100
CAMEL-26930: camel-jbang - camel run exits with 1 when the integration
fails to start (#29913)
When camel run cannot start the integration, it now exits with 1 ("fix the
route") instead of 70, and prints one ERROR line on stderr instead of the stack
trace a second time. KameletMainIntegration wraps the startup failure in a new
IntegrationStartException, which CliExecutionExceptionHandler maps to exit 1;
the stack trace is printed only with --verbose or CAMEL_CLI_DEBUG=true. The
exit codes page and the 4.24 upgrade guide document the change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 7 +++++
.../modules/ROOT/pages/camel-jbang-exit-codes.adoc | 3 +-
.../CliExecutionExceptionHandler.java | 20 +++++++++----
.../camel/dsl/jbang/core/commands/RunTest.java | 6 ++--
.../CliExecutionExceptionHandlerTest.java | 20 +++++++++++++
.../dsl/jbang/run/IntegrationStartException.java | 33 ++++++++++++++++++++++
.../dsl/jbang/run/KameletMainIntegration.java | 7 +++--
.../camel/dsl/jbang/run/PreviousRunLogTest.java | 5 +++-
8 files changed, 89 insertions(+), 12 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index ae9477e56599..028422a474ff 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1452,6 +1452,13 @@ NOTE: The annotation default (`appliesTo = {}`) is not
equivalent to `appliesTo:
An unset annotation value means no filtering (backward compatible — the plugin
is loaded for `run` and `export`),
whereas an explicit empty list in the config restricts the plugin to its own
command only.
+=== camel-jbang - camel run exits with 1 when the integration fails to start
+
+When `camel run` cannot start the integration, such as for a route that cannot
be created or started, it now exits
+with code `1` (fix the route) instead of `70` (internal error), and prints the
error as one `ERROR:` line on stderr.
+The stack trace is in the log of the integration, and is no longer printed a
second time on stderr (add `--verbose`
+for it). Scripts that checked for `70` after a failed start should check for
`1`.
+
=== camel-resilience4j, camel-microprofile-fault-tolerance - a circuit breaker
in a doTry
A circuit breaker in the steps of a `doTry` no longer ends that `doTry` for
the steps after it. Before, when the
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-exit-codes.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-exit-codes.adoc
index 6b204965dbde..05b3306131cc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-exit-codes.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-exit-codes.adoc
@@ -19,7 +19,8 @@ The exit codes and streams on this page apply from Camel 4.24
onwards.
| `1`
| Failed
| The command ran, but the outcome is negative: validation found errors, a
check failed,
- or the integration rejected the action.
+ the integration rejected the action, or `camel run` could not start the
integration (such as a route that cannot
+ be created): the log of the integration has the stack trace.
| `2`
| Usage error
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandler.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandler.java
index d454bca17592..102f21b3f813 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandler.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandler.java
@@ -31,6 +31,7 @@ import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.JsonOutput;
import org.apache.camel.dsl.jbang.core.common.ResourceDoesNotExist;
+import org.apache.camel.dsl.jbang.run.IntegrationStartException;
import org.apache.camel.dsl.jbang.run.common.Printer;
import org.apache.camel.main.download.DownloadException;
@@ -40,11 +41,12 @@ import org.apache.camel.main.download.DownloadException;
* <p/>
* A {@link CliException} prints its message as one {@code ERROR:} line, and
exits with its code. A file that does not
* exist exits with {@link CliExitCode#FILE_NOT_FOUND}. A dependency that
cannot be downloaded (a
- * {@link DownloadException}) prints its message as one {@code ERROR:} line,
and exits with {@link CliExitCode#FAILURE}.
- * A network timeout ({@link TimeoutException} or {@link HttpTimeoutException}
anywhere in the cause chain) exits with
- * {@link CliExitCode#TIMEOUT}. Any other exception is unexpected: it prints
the error with the stack trace, and exits
- * with {@link CliExitCode#INTERNAL}. With {@code --verbose} or the
environment variable {@code CAMEL_CLI_DEBUG=true},
- * every error prints its stack trace.
+ * {@link DownloadException}) prints its message as one {@code ERROR:} line,
and exits with {@link CliExitCode#FAILURE},
+ * and so does an integration that fails to start ({@link
IntegrationStartException}, such as a route that cannot be
+ * created), whose log has the stack trace already. A network timeout ({@link
TimeoutException} or
+ * {@link HttpTimeoutException} anywhere in the cause chain) exits with {@link
CliExitCode#TIMEOUT}. Any other exception
+ * is unexpected: it prints the error with the stack trace, and exits with
{@link CliExitCode#INTERNAL}. With
+ * {@code --verbose} or the environment variable {@code CAMEL_CLI_DEBUG=true},
every error prints its stack trace.
* <p/>
* A command in JSON mode ({@code --json}) also prints the error as a JSON
document on stdout, see {@link JsonOutput}.
*/
@@ -98,6 +100,10 @@ public class CliExecutionExceptionHandler {
// such as a dependency that does not exist, or a Maven repository
that cannot be reached
return CliExitCode.FAILURE;
}
+ if (find(ex, IntegrationStartException.class) != null) {
+ // such as a route that cannot be created: fix the route (the log
of the integration has the stack trace)
+ return CliExitCode.FAILURE;
+ }
if (find(ex, TimeoutException.class) != null || find(ex,
HttpTimeoutException.class) != null) {
return CliExitCode.TIMEOUT;
}
@@ -122,6 +128,10 @@ public class CliExecutionExceptionHandler {
if (download != null) {
return messageOf(download);
}
+ IntegrationStartException start = find(ex,
IntegrationStartException.class);
+ if (start != null) {
+ return messageOf(start);
+ }
return messageOf(ex);
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
index 016dad353125..784366cb57c4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
@@ -24,6 +24,7 @@ import org.apache.camel.FailedToStartRouteException;
import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.CommandLineTestHelper;
+import org.apache.camel.dsl.jbang.run.IntegrationStartException;
import org.apache.camel.dsl.jbang.run.RunPipeline;
import org.apache.camel.dsl.jbang.run.RunRequest;
import org.apache.camel.dsl.jbang.run.common.RuntimeType;
@@ -350,8 +351,9 @@ class RunTest extends CamelCommandBaseTestSupport {
CommandLineTestHelper.parse(command, "route.yaml");
assertThat(command.toRunRequest(Path.of(".")).keepPreviousLog).isTrue();
// with the exit code camel run exits with for the exception
- assertThat(command.runContext().failureExitCode(new
FailedToStartRouteException("route1", "boom")))
- .isEqualTo(CliExitCode.INTERNAL);
+ assertThat(command.runContext().failureExitCode(
+ new IntegrationStartException(new
FailedToStartRouteException("route1", "boom"))))
+ .isEqualTo(CliExitCode.FAILURE);
// not a Camel that another command runs in its JVM (such as camel cmd
send --local)
assertThat(new Run(new
CamelJBangMain()).toRunRequest(Path.of(".")).keepPreviousLog).isFalse();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandlerTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandlerTest.java
index cb0e71eec6e0..3562cd2e9c78 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandlerTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/CliExecutionExceptionHandlerTest.java
@@ -20,8 +20,11 @@ import java.io.IOException;
import java.net.http.HttpTimeoutException;
import java.util.concurrent.TimeoutException;
+import org.apache.camel.FailedToCreateRouteException;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.dsl.jbang.run.IntegrationStartException;
+import org.apache.camel.dsl.jbang.run.common.StringPrinter;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -56,6 +59,23 @@ class CliExecutionExceptionHandlerTest {
.isEqualTo(CliExitCode.TIMEOUT);
}
+ @Test
+ void anIntegrationThatFailsToStartIsExit1WithOneLine() {
+ // a mistake in the route: exit 70 told an AI agent "a bug in Camel,
do not retry" (CAMEL-26930)
+ StringPrinter printer = new StringPrinter();
+ CliExecutionExceptionHandler handler = new
CliExecutionExceptionHandler(new CamelJBangMain().withPrinter(printer));
+ IntegrationStartException failure = new IntegrationStartException(
+ new FailedToCreateRouteException(
+ "nosteps", "file:broken.yaml", "From[timer:tick]",
+ new IllegalArgumentException("Route nosteps has no
output processors")));
+
+ assertThat(handler.handle(failure,
null)).isEqualTo(CliExitCode.FAILURE);
+ // the log of the integration has the stack trace already
+ assertThat(printer.getStderr().strip()).startsWith("ERROR: Failed to
create route").contains("nosteps")
+ .contains("Route nosteps has no output processors")
+ .doesNotContain("\tat ");
+ }
+
@Test
void genericIoExceptionIsInternal() {
assertThat(handler.getExitCode(new IOException("unexpected error")))
diff --git
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/IntegrationStartException.java
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/IntegrationStartException.java
new file mode 100644
index 000000000000..43b22e01157a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/IntegrationStartException.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.camel.dsl.jbang.run;
+
+/**
+ * The integration failed to start, such as a route that cannot be created or
started: a mistake in the routes or their
+ * configuration, which the user fixes, not a bug of the Camel CLI. Its
message is the message of the cause, and the log
+ * of the integration has the stack trace.
+ *
+ * @since 4.24
+ */
+public class IntegrationStartException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public IntegrationStartException(Throwable cause) {
+ super(cause.getMessage() != null && !cause.getMessage().isBlank() ?
cause.getMessage() : cause.toString(), cause);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/KameletMainIntegration.java
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/KameletMainIntegration.java
index a2e912f75e67..51df50d5b0ee 100644
---
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/KameletMainIntegration.java
+++
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/KameletMainIntegration.java
@@ -77,13 +77,14 @@ final class KameletMainIntegration implements
RunningIntegration {
try {
main.start();
} catch (Exception e) {
- // such as a route that failed to start: noted as why the run ended
- ended(context.failureExitCode(e), failureOf(e));
+ // such as a route that failed to start: a mistake to fix in the
routes, noted as why the run ended
+ IntegrationStartException failure = new
IntegrationStartException(e);
+ ended(context.failureExitCode(failure), failureOf(e));
if (request.ignoreLoadingError) {
printer.printErr(e);
return new EndedIntegration(0);
}
- throw e;
+ throw failure;
} catch (Error e) {
// such as a NoClassDefFoundError of a missing dependency
ended(context.failureExitCode(e), failureOf(e));
diff --git
a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/PreviousRunLogTest.java
b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/PreviousRunLogTest.java
index 504a621279b3..1c9219f3cccb 100644
---
a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/PreviousRunLogTest.java
+++
b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/PreviousRunLogTest.java
@@ -73,7 +73,10 @@ class PreviousRunLogTest {
KameletMain main = mock(KameletMain.class);
doThrow(new FailedToStartRouteException("route1",
"boom")).when(main).start();
- assertThatThrownBy(() -> run(main,
keptRequest())).isInstanceOf(FailedToStartRouteException.class);
+ // a mistake in the routes, which the CLI tells apart from its own
errors
+ assertThatThrownBy(() -> run(main,
keptRequest())).isInstanceOf(IntegrationStartException.class)
+ .hasMessage("Failed to start route: route1 because: boom")
+ .hasCauseInstanceOf(FailedToStartRouteException.class);
// Camel did not get as far as a CamelContext: the name of the run
PreviousRun.Info info = only(PreviousRun.find("orders"));