This is an automated email from the ASF dual-hosted git repository.

christophd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit afdf38ea81040e1a0eaacfa5bee600bb3f617368
Author: Christoph Deppisch <[email protected]>
AuthorDate: Thu Nov 12 08:45:51 2026 +0100

    CAMEL-26764: Limit legacy fallback to CliExportContext, validate stale cache
    
    - Added legacyContributors parameter to getActivePlugins so only
      CliExportContext (true) loads legacy plugins without appliesTo for
      run/export. addPlugins (false/default) skips them, avoiding
      double-resolution at startup and broken camel cmd.
    - effectiveAppliesTo now validates resolved.camelVersion before
      trusting resolved.appliesTo — stale cache from a different Camel
      version is ignored and the plugin is treated as legacy.
    - Upgrade guide wording: "run and export" instead of "all built-in
      commands"; opts in by listing a command; annotation default {} is
      not cached; PluginAdd reads from known plugins list.
    - Tests: legacyContributors true/false, stale resolved.appliesTo
      ignored, broken legacy plugin skipped by addPlugins.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 11 ++--
 .../dsl/jbang/core/commands/CliExportContext.java  |  2 +-
 .../camel/dsl/jbang/core/common/PluginHelper.java  | 28 ++++++---
 .../dsl/jbang/core/common/PluginFilteringTest.java | 73 ++++++++++++++++++----
 4 files changed, 89 insertions(+), 25 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 4c554e0fe7fb..0f6161c68faf 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
@@ -1185,12 +1185,13 @@ A CLI plugin can now declare which built-in commands it 
participates in beyond i
 `@CamelJBangPlugin(name = "...", firstVersion = "...", appliesTo = 
{"export"})` so it is loaded during `camel export`
 but not during `camel run`.
 
-Existing plugins that do not declare `appliesTo` are loaded for all built-in 
commands as before (backward compatible).
-Once a plugin declares `appliesTo` (even an empty array), only the declared 
commands load it.
+Existing plugins that do not declare `appliesTo` are loaded for `camel run` 
and `camel export` as before (backward
+compatible). A plugin opts in to filtering by listing at least one command in 
`appliesTo`; in the plugin configuration
+file, an empty `appliesTo: []` array means the plugin is only loaded for its 
own command.
 
-When adding a plugin via `camel plugin add`, the `appliesTo` value from the 
`@CamelJBangPlugin` annotation (for known
-Camel plugins) or from the plugin's metadata is stored in the configuration. 
After a plugin is downloaded for the first
-time, its `appliesTo` is learned from its annotation and cached for subsequent 
invocations.
+When adding a plugin via `camel plugin add`, the `appliesTo` value from the 
list of known Camel plugins is stored in
+the configuration. After a plugin is downloaded for the first time, its 
`appliesTo` is learned from its
+`@CamelJBangPlugin` annotation and cached for subsequent invocations.
 
 === camel-resilience4j, camel-microprofile-fault-tolerance - a circuit breaker 
in a doTry
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
index 4d4767155fb5..e435bc9ee639 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
@@ -59,7 +59,7 @@ public final class CliExportContext {
             T context, CamelJBangMain main, Printer printer, boolean yes, 
String target) {
         context.withPrinter(printer)
                 .withPlugins(repos -> {
-                    Collection<Plugin> plugins = 
PluginHelper.getActivePlugins(main, repos, target).values();
+                    Collection<Plugin> plugins = 
PluginHelper.getActivePlugins(main, repos, target, true).values();
                     return new ExportContext.ActivePlugins(
                             
plugins.stream().map(Plugin::getRunCustomizer).flatMap(Optional::stream).toList(),
                             
plugins.stream().map(Plugin::getExporter).flatMap(Optional::stream).toList());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
index 9fb61f22bc64..06d11503d4c0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
@@ -345,6 +345,19 @@ public final class PluginHelper {
      * @throws CliException (exit code 1) when a plugin is not found, or needs 
a newer Camel CLI
      */
     public static Map<String, Plugin> getActivePlugins(CamelJBangMain main, 
String repos, String target) {
+        return getActivePlugins(main, repos, target, false);
+    }
+
+    /**
+     * Gets the active plugins, with an option to load legacy plugins (those 
without {@code appliesTo}) for run and
+     * export targets. {@code CliExportContext} passes {@code true} so that 
third-party plugins that predate
+     * {@code appliesTo} keep their run customizers and exporters. {@code 
addPlugins} passes {@code false} to avoid
+     * resolving and customizing unrelated plugins at CLI startup.
+     *
+     * @param legacyContributors when true and target is "run" or "export", 
plugins without appliesTo are loaded
+     */
+    public static Map<String, Plugin> getActivePlugins(
+            CamelJBangMain main, String repos, String target, boolean 
legacyContributors) {
         Map<String, Plugin> activePlugins = new HashMap<>();
         JsonObject config = getPluginConfig();
         if (config != null) {
@@ -363,12 +376,13 @@ public final class PluginHelper {
 
                 // skip plugins that don't match the target command to avoid 
unnecessary downloads
                 if (target != null && !"shell".equals(target) && 
!target.equals(command)) {
-                    Collection<?> appliesTo = effectiveAppliesTo(properties, 
name);
+                    Collection<?> appliesTo = effectiveAppliesTo(properties, 
name, version);
                     if (appliesTo != null) {
                         if (!appliesTo.contains(target)) {
                             continue;
                         }
-                    } else if (!PLUGIN_CONSUMING_BUILTINS.contains(target)) {
+                    } else if (!legacyContributors
+                            || !("run".equals(target) || 
"export".equals(target))) {
                         continue;
                     }
                 }
@@ -627,17 +641,17 @@ public final class PluginHelper {
 
     /**
      * Returns the effective {@code appliesTo} for a plugin entry, checking 
(in order) the top-level config, the
-     * {@code resolved} block from a previous download, and the {@link 
PluginType} enum for known Camel plugins. Returns
-     * {@code null} when no {@code appliesTo} information is available — the 
caller treats this as a legacy plugin that
-     * should be loaded for all targets.
+     * {@code resolved} block from a previous download (only when its Camel 
version matches), and the {@link PluginType}
+     * enum for known Camel plugins. Returns {@code null} when no {@code 
appliesTo} information is available — the
+     * caller treats this as a legacy plugin.
      */
-    private static Collection<?> effectiveAppliesTo(JsonObject properties, 
String name) {
+    private static Collection<?> effectiveAppliesTo(JsonObject properties, 
String name, String camelVersion) {
         Object obj = properties.get("appliesTo");
         if (obj instanceof Collection<?> list) {
             return list;
         }
         JsonObject resolved = properties.getMap("resolved");
-        if (resolved != null) {
+        if (resolved != null && 
sameCamelVersion(asString(resolved.get("camelVersion")), camelVersion)) {
             Object resolvedObj = resolved.get("appliesTo");
             if (resolvedObj instanceof Collection<?> list) {
                 return list;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
index 23eb812bd09f..940179339aa2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
@@ -106,30 +106,41 @@ class PluginFilteringTest {
                 .containsKey("beta");
     }
 
-    // ---- Legacy plugins without appliesTo are loaded for built-in commands 
(backward compat) ----
+    // ---- Legacy plugins without appliesTo: backward compat only for 
CliExportContext ----
 
     @Test
-    void legacyPluginWithoutAppliesToIsLoadedForRunTarget() throws Exception {
+    void legacyPluginIsLoadedForRunTargetWithLegacyContributors() throws 
Exception {
         writeTwoPluginConfig("alpha", "beta");
 
         CamelJBangMain main = new CamelJBangMain();
-        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "run");
+        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "run", true);
 
         assertThat(plugins).containsKey("alpha")
                 .containsKey("beta");
     }
 
     @Test
-    void legacyPluginWithoutAppliesToIsLoadedForExportTarget() throws 
Exception {
+    void legacyPluginIsLoadedForExportTargetWithLegacyContributors() throws 
Exception {
         writeTwoPluginConfig("alpha", "beta");
 
         CamelJBangMain main = new CamelJBangMain();
-        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "export");
+        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "export", true);
 
         assertThat(plugins).containsKey("alpha")
                 .containsKey("beta");
     }
 
+    @Test
+    void legacyPluginIsSkippedForRunTargetWithoutLegacyContributors() throws 
Exception {
+        writeTwoPluginConfig("alpha", "beta");
+
+        CamelJBangMain main = new CamelJBangMain();
+        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "run");
+
+        assertThat(plugins).doesNotContainKey("alpha")
+                .doesNotContainKey("beta");
+    }
+
     // ---- A broken unrelated plugin should not break the "run" target ----
 
     @Test
@@ -256,6 +267,47 @@ class PluginFilteringTest {
         assertThat(runPlugins).doesNotContainKey("test");
     }
 
+    // ---- resolved.appliesTo is only trusted when the Camel version matches 
----
+
+    @Test
+    void resolvedAppliesToIsUsedWhenCamelVersionMatches() throws Exception {
+        Path jar = tempDir.resolve("fake-exporter.jar");
+        FakePluginJar.write(jar, "exporter");
+
+        // entry with resolved block matching the current Camel version, 
appliesTo: ["export"]
+        JsonObject entry = buildEntry("exporter", camelVersion, jar, 
Files.getLastModifiedTime(jar).toMillis());
+        JsonObject resolved = (JsonObject) entry.getMap("resolved");
+        resolved.put("appliesTo", jsonArray("export"));
+        writeConfig(entry);
+
+        CamelJBangMain main = new CamelJBangMain();
+
+        // resolved.appliesTo says "export" and the version matches — plugin 
is skipped for "run"
+        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "run", true);
+        assertThat(plugins).doesNotContainKey("exporter");
+    }
+
+    @Test
+    void staleResolvedAppliesToIsIgnored() throws Exception {
+        // entry with resolved block from a DIFFERENT Camel version, 
appliesTo: ["export"]
+        // No valid jar — resolution will fail, proving the plugin was NOT 
filtered out.
+        JsonObject entry = buildBrokenEntry("exporter");
+        JsonObject resolved = new JsonObject();
+        resolved.put("camelVersion", "0.0.0-stale");
+        resolved.put("appliesTo", jsonArray("export"));
+        entry.put("resolved", resolved);
+        writeConfig(entry);
+
+        CamelJBangMain main = new CamelJBangMain();
+
+        // The resolved.appliesTo says "export", but the camelVersion is stale 
so it is ignored.
+        // With legacyContributors=true, the plugin passes the filter for 
"run" (treated as legacy).
+        // Resolution fails — the exception proves the stale appliesTo did not 
filter it out.
+        assertThatThrownBy(() -> PluginHelper.getActivePlugins(main, null, 
"run", true))
+                .isInstanceOf(CliException.class)
+                .hasMessageContaining("camel-jbang-plugin-exporter not found");
+    }
+
     // ---- CliExportContext should not resolve unrelated plugins (with 
explicit appliesTo) ----
 
     @Test
@@ -336,23 +388,20 @@ class PluginFilteringTest {
     // ---- A broken unrelated plugin should not break addPlugins for "run" 
----
 
     @Test
-    void brokenUnrelatedPluginDoesNotBreakAddPluginsForRun() throws Exception {
+    void brokenLegacyPluginDoesNotBreakAddPluginsForRun() throws Exception {
         Path goodJar = tempDir.resolve("fake-good.jar");
         FakePluginJar.write(goodJar, "good");
 
-        // broken plugin with empty appliesTo — explicitly not related to "run"
-        JsonObject brokenEntry = buildBrokenEntry("broken");
-        brokenEntry.put("appliesTo", jsonArray());
-
+        // broken plugin without appliesTo — legacy entry; addPlugins uses the 
default
+        // non-legacy path, so this plugin is skipped for "run"
         writeConfig(
                 buildEntry("good", camelVersion, goodJar, 
Files.getLastModifiedTime(goodJar).toMillis()),
-                brokenEntry);
+                buildBrokenEntry("broken"));
 
         CamelJBangMain main = new CamelJBangMain().withPrinter(new 
StringPrinter());
         CommandTree commandLine = new CommandTree(main);
         commandLine.addSubcommand("run", new CommandTree(new NoOpCommand()));
 
-        // addPlugins for "run" should skip the broken plugin since its 
appliesTo excludes "run"
         assertThatCode(() -> PluginHelper.addPlugins(commandLine, main, "run", 
"foo.yaml"))
                 .doesNotThrowAnyException();
     }

Reply via email to