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

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

commit 6fd79ed88bcb4eba408162807ff6874659349275
Author: Jinal Sarvaiya <[email protected]>
AuthorDate: Wed Nov 11 16:08:41 2026 +0000

    YAML profile consumers, inventory newlines, per-module version evidence
    
    Signed-off-by: Jinal Sarvaiya <[email protected]>
---
 .../org/apache/camel/kit/service/InitService.java  | 14 +++--
 .../kit/generator/ResourceConsistencyTest.java     | 10 ++++
 .../apache/camel/kit/service/InitServiceTest.java  | 35 +++++++++++++
 .../apache/camel/kit/graph/ProjectDiscovery.java   |  7 +++
 .../camel/kit/graph/parser/ConfigParser.java       | 25 ++-------
 .../apache/camel/kit/graph/parser/PomParser.java   | 50 ++++++++++++++++++
 .../kit/graph/parser/PropertyBindingParser.java    | 14 ++++-
 .../camel/kit/graph/ProjectDiscoveryTest.java      | 60 ++++++++++++++++++++++
 .../camel/kit/graph/parser/ConfigParserTest.java   | 54 +++++++++++++++++++
 9 files changed, 242 insertions(+), 27 deletions(-)

diff --git 
a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java
 
b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java
index d4211d29..0c6e72c6 100644
--- 
a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java
+++ 
b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java
@@ -1011,7 +1011,7 @@ public class InitService {
 
             if (!testFiles.isEmpty()) {
                 Path baselineFile = camelKitDir.resolve("baseline-tests.txt");
-                writeBaselineFile(projectRoot, baselineFile, String.join("\n", 
testFiles.stream().sorted().toList()));
+                writeBaselineFile(projectRoot, baselineFile, 
inventoryText(testFiles.stream().sorted().toList()));
             }
         } catch (Exception e) {
             // Non-fatal - baseline recording is best-effort
@@ -1043,7 +1043,7 @@ public class InitService {
                     })
                     .sorted()
                     .toList();
-            writeBaselineFile(projectRoot, baselineFile, String.join("\n", 
relativePaths));
+            writeBaselineFile(projectRoot, baselineFile, 
inventoryText(relativePaths));
         } catch (Exception e) {
             // Non-fatal - baseline recording is best-effort
         }
@@ -1089,9 +1089,17 @@ public class InitService {
         AtomicFileWriter.write(file, content);
     }
 
+    /**
+     * One record per line, every line (including the last) terminated by a 
line feed, so shell consumers such as
+     * {@code while read} see the final entry. An empty inventory is an empty 
file.
+     */
+    static String inventoryText(List<String> lines) {
+        return lines.isEmpty() ? "" : String.join("\n", lines) + "\n";
+    }
+
     private void writeBaselineLines(Path file, List<String> lines, Path 
projectRoot) {
         try {
-            writeBaselineFile(projectRoot, file, String.join("\n", lines));
+            writeBaselineFile(projectRoot, file, inventoryText(lines));
         } catch (Exception e) {
             // Non-fatal - baseline recording is best-effort
         }
diff --git 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java
 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java
index ed3ec554..0d80b03f 100644
--- 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java
+++ 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java
@@ -1552,6 +1552,16 @@ class ResourceConsistencyTest {
         assertFalse(validate.contains("done < .camel-kit/baseline-tests.txt"),
                 "camel-validate must not derive test classes from file names 
and count lines");
         assertFalse(validate.contains("| Tests Run |"), "camel-validate must 
not compare totals");
+        // every outcome transition has a verdict: a swapped failing test 
(same totals), errors, removed and skipped tests
+        for (String row : List.of("| passed | failed or error | **REGRESSION** 
|",
+                "| passed | missing (test removed, renamed, or not run) | 
**REGRESSION**",
+                "| passed | skipped | **REGRESSION**",
+                "| failed or error | failed or error | Pre-existing failure",
+                "| failed or error | passed | Fixed")) {
+            assertTrue(validate.contains(row), "camel-validate verdict table 
lost the row: " + row);
+        }
+        assertTrue(validate.contains("one test fixed and another broken has 
the same totals"),
+                "a swap in failing test identity with unchanged totals must be 
called out as a regression");
         assertTrue(validate.contains("DO NOT run the tests and call that the 
baseline"));
 
         // execute captures the baseline before the first edit; plan makes 
sure that happens
diff --git 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java
 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java
index c0cd416d..06eacf76 100644
--- 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java
+++ 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java
@@ -1584,6 +1584,41 @@ class InitServiceTest {
                 "Custom skill files should be preserved exactly, not 
overwritten on reinit");
     }
 
+    /**
+     * Producer-to-consumer: the shipped camel-validate instructions read the 
inventories line by line, so a final entry
+     * without a line feed is silently skipped. Runs the real producer, then a 
literal {@code while read} loop.
+     */
+    @Test
+    void 
baselineInventoriesAreNewlineTerminatedSoShellConsumersSeeEveryEntry() throws 
Exception {
+        Path targetDir = tempDir.resolve("inventory-newline");
+        Files.createDirectories(targetDir.resolve("src/main/resources/camel"));
+        
Files.createDirectories(targetDir.resolve("src/test/java/com/example"));
+        Files.writeString(targetDir.resolve("pom.xml"), 
minimalCamelPom("4.8.0"));
+        
Files.writeString(targetDir.resolve("src/main/resources/camel/only-route.yaml"),
+                "- route:\n    id: r\n    from:\n      uri: timer:t\n      
steps:\n        - log: hi\n");
+        
Files.writeString(targetDir.resolve("src/test/java/com/example/OnlyTest.java"),
+                "package com.example;\nclass OnlyTest {\n}\n");
+
+        new InitService().initialize(
+                requestForExistingProject(targetDir, "claude", 
InitProgress.noop(), InitReporter.noop()));
+
+        for (String inventory : List.of("baseline-routes.txt", 
"baseline-tests.txt")) {
+            Path file = targetDir.resolve(".camel-kit/" + inventory);
+            String content = Files.readString(file);
+            assertTrue(content.endsWith("\n"), inventory + " must end with a 
line feed: [" + content + "]");
+
+            Process loop = new ProcessBuilder(
+                    "sh", "-c",
+                    "n=0; while IFS= read -r entry; do n=$((n+1)); done < " + 
file + "; echo $n")
+                    .redirectErrorStream(true).start();
+            String counted
+                    = new String(loop.getInputStream().readAllBytes(), 
java.nio.charset.StandardCharsets.UTF_8).trim();
+            loop.waitFor();
+            assertEquals("1", counted, inventory + ": the shell loop must see 
the single entry");
+        }
+        assertEquals("", InitService.inventoryText(List.of()), "an empty 
inventory is an empty file");
+    }
+
     @Test
     void reinitRefreshesSharedSkillResourcesAndKeepsUserOwnedFiles() throws 
Exception {
         Path targetDir = tempDir.resolve("shared-refresh");
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java
index 56c1229a..f1cf4ce9 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java
@@ -120,6 +120,13 @@ public final class ProjectDiscovery {
                         "imported BOM " + 
managed.properties().get("artifactId"));
             }
         }
+        // Per-POM declarations: in a reactor the nodes above collapse to one 
module's value, these do not.
+        for (GraphNode evidence : graph.findByType(NodeType.CONFIG_PROPERTY)) {
+            if 
("camel-version-evidence".equals(evidence.properties().get("kind"))) {
+                addVersion(versions, evidence.properties().get("version"),
+                        evidence.properties().get("source") + " in " + 
evidence.properties().get("file"));
+            }
+        }
         return versions;
     }
 
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
index 5690bdab..5bdbc2cd 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
@@ -68,11 +68,11 @@ public class ConfigParser implements GraphParser {
                 JsonNode document = (JsonNode) documents.nextValue();
                 Map<String, String> flattened = new LinkedHashMap<>();
 
-                // Check for document-level profile 
(spring.config.activate.on-profile)
-                String documentProfile = extractDocumentProfile(document);
-
                 flatten("", document, flattened);
 
+                // Document-level profile (spring.config.activate.on-profile), 
nested or written as a flat dotted key
+                String documentProfile = 
flattened.get("spring.config.activate.on-profile");
+
                 // Determine effective profile (document > filename)
                 String effectiveProfile = documentProfile != null ? 
documentProfile : filenameProfile;
 
@@ -100,25 +100,6 @@ public class ConfigParser implements GraphParser {
         }
     }
 
-    /**
-     * Extract profile from document content 
(spring.config.activate.on-profile).
-     */
-    private String extractDocumentProfile(JsonNode document) {
-        if (document.has("spring")) {
-            JsonNode spring = document.get("spring");
-            if (spring.has("config")) {
-                JsonNode config = spring.get("config");
-                if (config.has("activate")) {
-                    JsonNode activate = config.get("activate");
-                    if (activate.has("on-profile")) {
-                        return activate.get("on-profile").asText();
-                    }
-                }
-            }
-        }
-        return null;
-    }
-
     private static void flatten(String prefix, JsonNode node, Map<String, 
String> out) {
         if (node == null || node.isNull() || node.isMissingNode()) {
             return;
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
index 3068013a..d8687768 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
@@ -103,6 +103,8 @@ public class PomParser implements GraphParser {
                 }
             }
 
+            recordCamelVersionEvidence(model, pomRelative, graph);
+
             for (Dependency dep : model.getDependencies()) {
                 String depId = "maven:" + dep.getGroupId() + ":" + 
dep.getArtifactId();
                 String depVersion = dep.getVersion() != null ? 
dep.getVersion() : "managed";
@@ -123,4 +125,52 @@ public class PomParser implements GraphParser {
             throw new RuntimeException("Failed to parse POM: " + pomFile, e);
         }
     }
+
+    /**
+     * The graph keys properties and artifacts by name, so in a reactor the 
declarations of several modules collapse
+     * into one node and only the last survives. Camel version declarations 
are therefore also recorded once per POM
+     * under {@code maven-camel-version:<pom>:<source>}, with the property 
references resolved against that POM's own
+     * properties. Discovery uses them to notice modules that disagree.
+     */
+    private void recordCamelVersionEvidence(Model model, String pomRelative, 
ProjectGraph graph) {
+        String camelVersionProperty
+                = model.getProperties() == null ? null : 
model.getProperties().getProperty("camel.version");
+        addCamelVersionEvidence(graph, pomRelative, "property camel.version", 
camelVersionProperty, model);
+        for (Dependency dep : model.getDependencies()) {
+            if ("org.apache.camel".equals(dep.getGroupId())) {
+                addCamelVersionEvidence(graph, pomRelative, "dependency " + 
dep.getArtifactId(), dep.getVersion(),
+                        model);
+            }
+        }
+        DependencyManagement management = model.getDependencyManagement();
+        if (management != null) {
+            for (Dependency managed : management.getDependencies()) {
+                if ("pom".equals(managed.getType()) && managed.getGroupId() != 
null
+                        && managed.getGroupId().startsWith("org.apache.camel")
+                        && managed.getArtifactId().startsWith("camel-") && 
managed.getArtifactId().endsWith("-bom")) {
+                    addCamelVersionEvidence(graph, pomRelative, "imported BOM 
" + managed.getArtifactId(),
+                            managed.getVersion(), model);
+                }
+            }
+        }
+    }
+
+    private void addCamelVersionEvidence(
+            ProjectGraph graph, String pomRelative, String source, String 
rawVersion, Model model) {
+        String version = resolveProperty(rawVersion, model);
+        if (version == null || version.isBlank() || version.contains("${")) {
+            return;
+        }
+        graph.addNode(new GraphNode(
+                "maven-camel-version:" + pomRelative + ":" + source, 
NodeType.CONFIG_PROPERTY,
+                Map.of("kind", "camel-version-evidence", "version", version, 
"source", source, "file", pomRelative)));
+    }
+
+    private static String resolveProperty(String value, Model model) {
+        if (value == null || !value.startsWith("${") || !value.endsWith("}") 
|| model.getProperties() == null) {
+            return value;
+        }
+        String resolved = model.getProperties().getProperty(value.substring(2, 
value.length() - 1));
+        return resolved == null ? value : resolved;
+    }
 }
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java
index 3fb7f179..2036d098 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java
@@ -1,6 +1,7 @@
 package org.apache.camel.kit.graph.parser;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -182,9 +183,18 @@ public class PropertyBindingParser {
             Matcher matcher = PLACEHOLDER_PATTERN.matcher(uri);
             while (matcher.find()) {
                 String key = matcher.group(1);
+                // The default value ("config:<key>") and every 
profile-specific variant ("config:<key>:profile=<name>")
+                // configure the endpoint; a key defined only in a profile 
document is still linked.
                 String configNodeId = "config:" + key;
-                if (graph.hasNode(configNodeId)) {
-                    graph.addEdge(new GraphEdge(configNodeId, node.id(), 
EdgeType.CONFIGURES, Map.of()));
+                for (String candidate : List.of(configNodeId)) {
+                    if (graph.hasNode(candidate)) {
+                        graph.addEdge(new GraphEdge(candidate, node.id(), 
EdgeType.CONFIGURES, Map.of()));
+                    }
+                }
+                for (GraphNode config : 
graph.findByType(NodeType.CONFIG_PROPERTY)) {
+                    if (config.id().startsWith(configNodeId + ":profile=")) {
+                        graph.addEdge(new GraphEdge(config.id(), node.id(), 
EdgeType.CONFIGURES, Map.of()));
+                    }
                 }
             }
         }
diff --git 
a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java
 
b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java
index bdc8dd2e..7384e0f9 100644
--- 
a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java
+++ 
b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java
@@ -605,4 +605,64 @@ class ProjectDiscoveryTest {
         assertTrue(result.coverageGaps().stream().anyMatch(g -> 
g.contains("Conflicting Camel versions")
                 && g.contains("4.8.0") && g.contains("4.10.0")));
     }
+
+    private static void module(java.nio.file.Path root, String name, String 
camelVersion) throws java.io.IOException {
+        java.nio.file.Path dir = 
java.nio.file.Files.createDirectories(root.resolve(name));
+        java.nio.file.Files.writeString(dir.resolve("pom.xml"), 
String.format(java.util.Locale.ROOT, """
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                  <modelVersion>4.0.0</modelVersion>
+                  <groupId>com.example</groupId>
+                  <artifactId>%s</artifactId>
+                  <version>1.0.0</version>
+                  <properties><camel.version>%s</camel.version></properties>
+                  <dependencies>
+                    <dependency>
+                      <groupId>org.apache.camel</groupId>
+                      <artifactId>camel-main</artifactId>
+                      <version>${camel.version}</version>
+                    </dependency>
+                  </dependencies>
+                </project>
+                """, name, camelVersion));
+    }
+
+    @Test
+    void 
modulesDeclaringDifferentCamelVersionsAreAConflictNotTheLastModulesVersion(
+            @org.junit.jupiter.api.io.TempDir java.nio.file.Path root)
+            throws Exception {
+        java.nio.file.Files.writeString(root.resolve("pom.xml"), """
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                  <modelVersion>4.0.0</modelVersion>
+                  <groupId>com.example</groupId>
+                  <artifactId>parent</artifactId>
+                  <version>1.0.0</version>
+                  <packaging>pom</packaging>
+                  <modules><module>a</module><module>b</module></modules>
+                </project>
+                """);
+        module(root, "a", "4.8.0");
+        module(root, "b", "4.10.0");
+        ProjectGraph reactor = new ProjectGraph();
+
+        new org.apache.camel.kit.graph.parser.PomParser().parse(root, reactor);
+        ProjectDiscovery.DiscoveryResult result = 
ProjectDiscovery.discover(reactor);
+
+        assertNull(result.camelVersion(), "one module's version must not be 
bound to the whole reactor");
+        assertTrue(result.hasConflict());
+        
assertTrue(result.allCamelVersions().containsAll(java.util.Set.of("4.8.0", 
"4.10.0")));
+        assertTrue(result.coverageGaps().stream().anyMatch(g -> 
g.contains("Conflicting Camel versions")
+                && g.contains("a/pom.xml") && g.contains("b/pom.xml")), 
result.coverageGaps().toString());
+    }
+
+    @Test
+    void 
modulesThatAgreeOnTheCamelVersionStayLocked(@org.junit.jupiter.api.io.TempDir 
java.nio.file.Path root)
+            throws Exception {
+        module(root, "a", "4.8.0");
+        module(root, "b", "4.8.0");
+        ProjectGraph reactor = new ProjectGraph();
+
+        new org.apache.camel.kit.graph.parser.PomParser().parse(root, reactor);
+
+        assertEquals("4.8.0", 
ProjectDiscovery.discover(reactor).camelVersion());
+    }
 }
diff --git 
a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java
 
b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java
index a6625d2b..0b563c8b 100644
--- 
a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java
+++ 
b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java
@@ -3,7 +3,9 @@ package org.apache.camel.kit.graph.parser;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
+import org.apache.camel.kit.graph.GraphBuilder;
 import org.apache.camel.kit.graph.ProjectGraph;
+import org.apache.camel.kit.graph.model.EdgeType;
 import org.apache.camel.kit.graph.model.NodeType;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -127,4 +129,56 @@ class ConfigParserTest {
         assertEquals("prod:9092", prod.properties().get("value"));
         assertEquals("prod", prod.properties().get("profile"));
     }
+
+    @Test
+    void flatDottedActivationKeyIsRecognisedAsTheDocumentProfile(@TempDir Path 
root) throws Exception {
+        Path res = Files.createDirectories(root.resolve("src/main/resources"));
+        Files.writeString(res.resolve("application.yml"), """
+                port: 8080
+                ---
+                spring.config.activate.on-profile: prod
+                port: 9090
+                """);
+        ProjectGraph yamlGraph = new ProjectGraph();
+
+        new ConfigParser().parse(root, yamlGraph);
+
+        assertEquals("8080", 
yamlGraph.getNode("config:port").properties().get("value"));
+        var prod = yamlGraph.getNode("config:port:profile=prod");
+        assertNotNull(prod, "a flat dotted activation key must scope the 
document too");
+        assertEquals("9090", prod.properties().get("value"));
+        assertEquals("prod", prod.properties().get("profile"));
+    }
+
+    /** Connected consumer: a YAML property referenced by a route placeholder 
must still configure the endpoint. */
+    @Test
+    void 
yamlPropertiesStillConfigureTheRouteEndpointsThatReferenceThem(@TempDir Path 
root) throws Exception {
+        Path res = 
Files.createDirectories(root.resolve("src/main/resources/camel"));
+        Files.writeString(res.getParent().resolve("application.yml"), """
+                timer:
+                  name: tick
+                ---
+                spring.config.activate.on-profile: prod
+                kafka:
+                  topic: prod-topic
+                """);
+        Files.writeString(res.resolve("routes.yaml"), """
+                - route:
+                    id: ticker
+                    from:
+                      uri: "timer:{{timer.name}}"
+                      steps:
+                        - to:
+                            uri: "kafka:{{kafka.topic}}"
+                """);
+
+        ProjectGraph built = new GraphBuilder().build(root);
+
+        assertTrue(built.getEdges().stream().anyMatch(e -> e.type() == 
EdgeType.CONFIGURES
+                && e.from().equals("config:timer.name") && 
e.to().startsWith("endpoint:timer")),
+                "a default YAML property must keep its CONFIGURES edge");
+        assertTrue(built.getEdges().stream().anyMatch(e -> e.type() == 
EdgeType.CONFIGURES
+                && e.from().equals("config:kafka.topic:profile=prod") && 
e.to().startsWith("endpoint:kafka")),
+                "a property defined only in a profile document must still be 
linked");
+    }
 }

Reply via email to