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"));

Reply via email to