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 b7659d17f465 CAMEL-24602: Report per-module CI build elapsed time in 
PR test comments (#26147)
b7659d17f465 is described below

commit b7659d17f46584a872ae70fc041b581bca7c1d21
Author: Omar Atie <[email protected]>
AuthorDate: Mon Sep 7 11:55:01 2026 -0700

    CAMEL-24602: Report per-module CI build elapsed time in PR test comments 
(#26147)
    
    * CAMEL-24602: Report per-module CI build elapsed time in PR comments
    
    Parse Maven reactor SUCCESS/FAILURE/SKIPPED lines from incremental-test.log
    and include total duration, a per-module timing table, and top slowest
    modules in the unified PR test comment and GitHub step summary.
    
    Co-authored-by: Cursor Agent <[email protected]>
    
    * CAMEL-24602: Harden reactor timing parser for empty logs and pipefail
    
    Avoid pipeline subshells so parsing helpers remain available, skip empty
    awk keys, and add tests for empty log files.
    
    Co-authored-by: Cursor Agent <[email protected]>
    
    * CAMEL-24602: Address PR review for reactor timing report
    
    Raise TOP_SLOWEST_LIMIT to 20, extract _write_timing_section helper to
    remove duplicated comment/summary rendering, and add regression test for
    module names without dot padding before the status token.
    
    Co-authored-by: Cursor <[email protected]>
    
    * CAMEL-24602: Update CI-ARCHITECTURE.md for top-20 slowest modules
    
    Co-authored-by: Omar Atie <[email protected]>
    
    ---------
    
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Omar Atie <[email protected]>
---
 .github/CI-ARCHITECTURE.md                         |   3 +
 .../actions/incremental-build/incremental-build.sh |  45 +---
 .../actions/incremental-build/reactor_timing.sh    | 232 +++++++++++++++++++++
 .../incremental-build/reactor_timing_test.sh       | 122 +++++++++++
 4 files changed, 368 insertions(+), 34 deletions(-)

diff --git a/.github/CI-ARCHITECTURE.md b/.github/CI-ARCHITECTURE.md
index 90139a08698b..22acc2a1a3a5 100644
--- a/.github/CI-ARCHITECTURE.md
+++ b/.github/CI-ARCHITECTURE.md
@@ -103,6 +103,9 @@ The script also:
 - Applies an exclusion list for generated/meta modules
 - Checks for excluded modules with associated integration tests (via 
`manual-it-mapping.txt`) and advises contributors to run them manually
 - Generates a unified PR comment with all test information
+- Parses Maven reactor output from `incremental-test.log` and reports 
**per-module elapsed time**, total reactor duration, and the top 20 slowest 
modules (see `reactor_timing.sh`)
+
+Unit tests for reactor timing parsing live in `reactor_timing_test.sh`.
 
 ### `install-mvnd`
 
diff --git a/.github/actions/incremental-build/incremental-build.sh 
b/.github/actions/incremental-build/incremental-build.sh
index 6aa87cf1ae85..947c6af672ad 100755
--- a/.github/actions/incremental-build/incremental-build.sh
+++ b/.github/actions/incremental-build/incremental-build.sh
@@ -30,6 +30,10 @@
 
 set -euo pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck source=reactor_timing.sh
+source "${SCRIPT_DIR}/reactor_timing.sh"
+
 echo "Using MVND_OPTS=$MVND_OPTS"
 echo "Using MAVEN_EXTRA_ARGS=${MAVEN_EXTRA_ARGS:-}"
 
@@ -987,42 +991,15 @@ main() {
   # Check for excluded IT suites that should be run manually
   checkManualItTests "$final_pl" "$comment_file"
 
-  # Append reactor module list from build log
+  # Append reactor module list from build log (with per-module elapsed time)
   if [[ -f "$log" ]]; then
-    local reactor_modules
-    reactor_modules=$(grep '^\[INFO\] Camel ::' "$log" | sed 's/\[INFO\] //' | 
sed 's/ \..*$//' | sed 's/  *\[.*\]$//' | sed 's/ SUCCESS$//' | sed 's/ 
FAILURE$//' | sed 's/ SKIPPED$//' | sed 's/  *$//' | sort -u || true)
-    if [[ -n "$reactor_modules" ]]; then
-      local count
-      count=$(echo "$reactor_modules" | wc -l | tr -d ' ')
-      local reactor_label
-      if [[ "${testedDependents}" = "false" ]]; then
-        reactor_label="Build reactor — dependencies compiled but only changed 
modules were tested"
-      else
-        reactor_label="All tested modules"
-      fi
-
-      echo "" >> "$comment_file"
-      echo "<details><summary>${reactor_label} ($count modules)</summary>" >> 
"$comment_file"
-      echo "" >> "$comment_file"
-
-      if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
-        echo "" >> "$GITHUB_STEP_SUMMARY"
-        echo "<details><summary><b>${reactor_label} ($count)</b></summary>" >> 
"$GITHUB_STEP_SUMMARY"
-        echo "" >> "$GITHUB_STEP_SUMMARY"
-      fi
-
-      echo "$reactor_modules" | while read -r m; do
-        [ -n "${GITHUB_STEP_SUMMARY:-}" ] && echo "- $m" >> 
"$GITHUB_STEP_SUMMARY"
-        echo "- $m" >> "$comment_file"
-      done
-
-      if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
-        echo "" >> "$GITHUB_STEP_SUMMARY"
-        echo "</details>" >> "$GITHUB_STEP_SUMMARY"
-      fi
-      echo "" >> "$comment_file"
-      echo "</details>" >> "$comment_file"
+    local reactor_label
+    if [[ "${testedDependents}" = "false" ]]; then
+      reactor_label="Build reactor — dependencies compiled but only changed 
modules were tested"
+    else
+      reactor_label="All tested modules"
     fi
+    append_reactor_timing_report "$log" "$comment_file" "$reactor_label" 
"${GITHUB_STEP_SUMMARY:-}"
   fi
 
   # Write step summary header
diff --git a/.github/actions/incremental-build/reactor_timing.sh 
b/.github/actions/incremental-build/reactor_timing.sh
new file mode 100644
index 000000000000..291132cdb2c0
--- /dev/null
+++ b/.github/actions/incremental-build/reactor_timing.sh
@@ -0,0 +1,232 @@
+#
+# 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.
+#
+
+# Helpers for parsing Maven reactor timing lines from incremental-test.log.
+
+TOP_SLOWEST_LIMIT=20
+
+parse_reactor_duration_seconds() {
+  local line="$1"
+  if [[ "$line" =~ 
\[[[:space:]]*([0-9]+(\.[0-9]+)?)[[:space:]]*s\][[:space:]]*$ ]]; then
+    echo "${BASH_REMATCH[1]}"
+    return 0
+  fi
+  echo ""
+}
+
+parse_reactor_status() {
+  local line="$1"
+  if [[ "$line" =~ [[:space:]](SUCCESS|FAILURE|SKIPPED)[[:space:]]*\[ ]]; then
+    echo "${BASH_REMATCH[1]}"
+    return 0
+  fi
+  if [[ "$line" =~ [[:space:]](SUCCESS|FAILURE|SKIPPED)[[:space:]]*$ ]]; then
+    echo "${BASH_REMATCH[1]}"
+    return 0
+  fi
+  echo ""
+}
+
+parse_reactor_module_name() {
+  local line="$1"
+  local name="${line#\[INFO\] }"
+  name=$(echo "$name" | sed 's/ \..*$//')
+  name=$(echo "$name" | sed 's/  *\[.*\]$//')
+  name=$(echo "$name" | sed 's/ SUCCESS$//; s/ FAILURE$//; s/ SKIPPED$//')
+  name=$(echo "$name" | sed 's/  *$//')
+  echo "$name"
+}
+
+format_elapsed_seconds() {
+  local raw="$1"
+  if [[ -z "$raw" ]]; then
+    echo "n/a"
+    return 0
+  fi
+
+  awk -v seconds="$raw" '
+    BEGIN {
+      if (seconds < 60) {
+        printf "%.1fs", seconds
+      } else {
+        mins = int(seconds / 60)
+        secs = seconds - (mins * 60)
+        if (mins < 60) {
+          printf "%dm %.0fs", mins, secs
+        } else {
+          hours = int(mins / 60)
+          mins = mins - (hours * 60)
+          printf "%dh %dm", hours, mins
+        }
+      }
+    }'
+}
+
+parse_reactor_log_to_tsv() {
+  local log_file="$1"
+  local parsed=""
+  local line module duration status
+
+  while IFS= read -r line; do
+    module=$(parse_reactor_module_name "$line")
+    duration=$(parse_reactor_duration_seconds "$line")
+    status=$(parse_reactor_status "$line")
+    if [[ -n "$module" ]]; then
+      parsed+="${module}"$'\t'"${duration}"$'\t'"${status}"$'\n'
+    fi
+  done < <(grep '^\[INFO\] Camel ::' "$log_file" || true)
+
+  if [[ -z "$parsed" ]]; then
+    return 0
+  fi
+
+  echo "$parsed" | awk -F '\t' '
+    {
+      key = $1
+      if (key == "") {
+        next
+      }
+      duration = ($2 == "" ? -1 : $2)
+      status = $3
+      if (!(key in seen) || duration > stored[key]) {
+        seen[key] = 1
+        stored[key] = duration
+        statuses[key] = status
+      }
+    }
+    END {
+      for (key in seen) {
+        duration = stored[key]
+        if (duration < 0) {
+          duration = ""
+        }
+        printf "%s\t%s\t%s\n", key, duration, statuses[key]
+      }
+    }' | sort
+}
+
+sum_elapsed_seconds_from_tsv() {
+  local tsv="$1"
+  if [[ -z "$tsv" ]]; then
+    echo "0"
+    return 0
+  fi
+  echo "$tsv" | awk -F '\t' '
+    $2 != "" && $2 ~ /^[0-9]+(\.[0-9]+)?$/ { total += $2 }
+    END { printf "%.3f", total + 0 }'
+}
+
+render_top_slowest_modules() {
+  local tsv="$1"
+  local limit="${2:-${TOP_SLOWEST_LIMIT}}"
+  echo "$tsv" | awk -F '\t' '
+    $2 != "" && $2 ~ /^[0-9]+(\.[0-9]+)?$/ {
+      printf "%s\t%s\n", $2, $1
+    }' | sort -t $'\t' -k1,1nr | head -n "$limit" | while IFS=$'\t' read -r 
seconds module; do
+    local formatted
+    formatted=$(format_elapsed_seconds "$seconds")
+    echo "- \`${module}\` (${formatted})"
+  done
+}
+
+_write_timing_section() {
+  local tsv="$1"
+  local slowest="$2"
+  local total_formatted="$3"
+  local reactor_label="$4"
+  local count="$5"
+  local outfile="$6"
+  local bold_summary="${7:-false}"
+
+  if [[ "$bold_summary" == "true" ]]; then
+    {
+      echo ""
+      echo "<details><summary><b>${reactor_label} (${count} modules, 
${total_formatted} total)</b></summary>"
+      echo ""
+      echo "**Total reactor time:** ${total_formatted}"
+      echo ""
+      echo "| Module | Duration | Status |"
+      echo "| --- | --- | --- |"
+    } >> "$outfile"
+  else
+    {
+      echo ""
+      echo "<details><summary>${reactor_label} (${count} modules, 
${total_formatted} total)</summary>"
+      echo ""
+      echo "**Total reactor time:** ${total_formatted}"
+      echo ""
+      echo "| Module | Duration | Status |"
+      echo "| --- | --- | --- |"
+    } >> "$outfile"
+  fi
+
+  echo "$tsv" | awk -F '\t' '
+    $2 != "" && $2 ~ /^[0-9]+(\.[0-9]+)?$/ {
+      printf "%s\t%s\t%s\n", $2, $1, $3
+    }
+    $2 == "" {
+      printf "-1\t%s\t%s\n", $1, $3
+    }' | sort -t $'\t' -k1,1nr | while IFS=$'\t' read -r sort_key module 
status; do
+    local duration_display="n/a"
+    if [[ "$sort_key" != "-1" ]]; then
+      duration_display=$(format_elapsed_seconds "$sort_key")
+    fi
+    echo "| ${module} | ${duration_display} | ${status:-} |" >> "$outfile"
+  done
+
+  if [[ -n "$slowest" ]]; then
+    {
+      echo ""
+      echo "**Top ${TOP_SLOWEST_LIMIT} slowest modules:**"
+      echo "$slowest"
+    } >> "$outfile"
+  fi
+
+  {
+    echo ""
+    echo "</details>"
+  } >> "$outfile"
+}
+
+append_reactor_timing_report() {
+  local log_file="$1"
+  local comment_file="$2"
+  local reactor_label="$3"
+  local step_summary_file="${4:-}"
+
+  if [[ ! -f "$log_file" ]]; then
+    return 0
+  fi
+
+  local tsv
+  tsv=$(parse_reactor_log_to_tsv "$log_file")
+  if [[ -z "$tsv" ]]; then
+    return 0
+  fi
+
+  local count total_seconds total_formatted slowest
+  count=$(echo "$tsv" | grep -c . || true)
+  total_seconds=$(sum_elapsed_seconds_from_tsv "$tsv")
+  total_formatted=$(format_elapsed_seconds "$total_seconds")
+  slowest=$(render_top_slowest_modules "$tsv" "$TOP_SLOWEST_LIMIT")
+
+  _write_timing_section "$tsv" "$slowest" "$total_formatted" "$reactor_label" 
"$count" "$comment_file" "false"
+
+  if [[ -n "$step_summary_file" ]]; then
+    _write_timing_section "$tsv" "$slowest" "$total_formatted" 
"$reactor_label" "$count" "$step_summary_file" "true"
+  fi
+}
diff --git a/.github/actions/incremental-build/reactor_timing_test.sh 
b/.github/actions/incremental-build/reactor_timing_test.sh
new file mode 100755
index 000000000000..c1e39b5c5c80
--- /dev/null
+++ b/.github/actions/incremental-build/reactor_timing_test.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck source=reactor_timing.sh
+source "${SCRIPT_DIR}/reactor_timing.sh"
+
+pass=0
+fail=0
+
+assert_eq() {
+  local expected="$1"
+  local actual="$2"
+  local message="$3"
+  if [[ "$expected" == "$actual" ]]; then
+    pass=$((pass + 1))
+  else
+    echo "FAIL: $message"
+    echo "  expected: [$expected]"
+    echo "  actual:   [$actual]"
+    fail=$((fail + 1))
+  fi
+}
+
+assert_contains() {
+  local haystack="$1"
+  local needle="$2"
+  local message="$3"
+  if [[ "$haystack" == *"$needle"* ]]; then
+    pass=$((pass + 1))
+  else
+    echo "FAIL: $message"
+    echo "  expected to contain: [$needle]"
+    echo "  actual:              [$haystack]"
+    fail=$((fail + 1))
+  fi
+}
+
+SAMPLE_LINE='[INFO] Camel :: Kafka :: camel-kafka ........................ 
SUCCESS [ 252.500 s]'
+assert_eq "252.500" "$(parse_reactor_duration_seconds "$SAMPLE_LINE")" "parse 
duration from success line"
+assert_eq "SUCCESS" "$(parse_reactor_status "$SAMPLE_LINE")" "parse status 
from success line"
+assert_eq "Camel :: Kafka :: camel-kafka" "$(parse_reactor_module_name 
"$SAMPLE_LINE")" "parse module name"
+
+NO_DOTS_LINE='[INFO] Camel :: CSimple Maven Plugin (deprecated) SUCCESS [  
2.123 s]'
+assert_eq "2.123" "$(parse_reactor_duration_seconds "$NO_DOTS_LINE")" "parse 
duration from no-dots line"
+assert_eq "SUCCESS" "$(parse_reactor_status "$NO_DOTS_LINE")" "parse status 
from no-dots line"
+assert_eq "Camel :: CSimple Maven Plugin (deprecated)" 
"$(parse_reactor_module_name "$NO_DOTS_LINE")" "parse module name without dot 
padding"
+
+FAILURE_LINE='[INFO] Camel :: Exec .................................... 
FAILURE [  1.234 s]'
+assert_eq "1.234" "$(parse_reactor_duration_seconds "$FAILURE_LINE")" "parse 
duration from failure line"
+assert_eq "FAILURE" "$(parse_reactor_status "$FAILURE_LINE")" "parse status 
from failure line"
+
+SKIPPED_LINE='[INFO] Camel :: Catalog ................................. 
SKIPPED'
+assert_eq "" "$(parse_reactor_duration_seconds "$SKIPPED_LINE")" "skipped line 
has no duration"
+assert_eq "SKIPPED" "$(parse_reactor_status "$SKIPPED_LINE")" "parse skipped 
status"
+
+assert_eq "12.3s" "$(format_elapsed_seconds "12.345")" "format sub-minute 
duration"
+assert_eq "1m 5s" "$(format_elapsed_seconds "65")" "format minute duration"
+assert_eq "1h 2m" "$(format_elapsed_seconds "3720")" "format hour duration"
+assert_eq "n/a" "$(format_elapsed_seconds "")" "format empty duration"
+
+fixture="$(mktemp)"
+cat > "$fixture" <<'EOF'
+[INFO] Camel :: Core :: camel-core ........................ SUCCESS [  10.000 
s]
+[INFO] Camel :: Kafka :: camel-kafka .................... SUCCESS [ 120.000 s]
+[INFO] Camel :: HTTP :: camel-http ...................... SUCCESS [  30.000 s]
+[INFO] Camel :: Exec .................................... FAILURE [   5.500 s]
+[INFO] Camel :: Catalog ................................. SKIPPED
+EOF
+
+tsv="$(parse_reactor_log_to_tsv "$fixture")"
+assert_eq "5" "$(echo "$tsv" | grep -c . || true)" "module count includes 
skipped modules"
+assert_eq "165.500" "$(sum_elapsed_seconds_from_tsv "$tsv")" "sum elapsed 
seconds"
+
+report_file="$(mktemp)"
+append_reactor_timing_report "$fixture" "$report_file" "All tested modules" ""
+report_content="$(cat "$report_file")"
+if [[ "$report_content" == *"165.500"* ]]; then
+  echo "FAIL: report should not expose raw seconds"
+  fail=$((fail + 1))
+else
+  pass=$((pass + 1))
+fi
+assert_contains "$report_content" "2m 46s total" "report includes formatted 
total time"
+assert_contains "$report_content" "| Camel :: Kafka :: camel-kafka | 2m 0s | 
SUCCESS |" "slowest module listed first"
+assert_contains "$report_content" "**Top ${TOP_SLOWEST_LIMIT} slowest 
modules:**" "report includes slowest section"
+assert_contains "$report_content" "\`Camel :: Kafka :: camel-kafka\` (2m 0s)" 
"slowest bullet uses formatted duration"
+
+rm -f "$fixture" "$report_file"
+
+empty_fixture="$(mktemp)"
+touch "$empty_fixture"
+empty_tsv="$(parse_reactor_log_to_tsv "$empty_fixture")"
+assert_eq "" "$empty_tsv" "empty log yields empty tsv without error"
+
+empty_report="$(mktemp)"
+append_reactor_timing_report "$empty_fixture" "$empty_report" "All tested 
modules" ""
+assert_eq "" "$(cat "$empty_report")" "empty log yields empty report"
+rm -f "$empty_fixture" "$empty_report"
+
+echo ""
+echo "reactor_timing_test.sh: ${pass} passed, ${fail} failed"
+if [[ "$fail" -ne 0 ]]; then
+  exit 1
+fi

Reply via email to