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