From 4708860871d129b05bfb030ce1ead1405fcf03a5 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Sun, 22 Mar 2026 08:38:58 -0500 Subject: [PATCH 1/2] ENH: Add collapsible CI log sections for dashboard phases Wrap configure/build/test/coverage/memcheck/submit phases in collapsible log sections so CI output is easier to navigate. Detects the CI environment via standard environment variables and emits the appropriate markers: - GitHub Actions: ::group::/::endgroup:: - Azure DevOps: ##[group]/##[endgroup] - GitLab CI: ANSI escape section_start/section_end (collapsed) - Other/local: plain "--- Title ---" / "--- end id ---" delimiters Section IDs include a per-iteration counter so that GitLab CI (which requires unique IDs within a job log) renders correctly when dashboard_loop runs multiple iterations in continuous mode. Co-Authored-By: Claude Opus 4.6 --- itk_common.cmake | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/itk_common.cmake b/itk_common.cmake index c3e25c64434..bd05b662802 100644 --- a/itk_common.cmake +++ b/itk_common.cmake @@ -382,12 +382,46 @@ if(NOT DEFINED dashboard_loop) endif() endif() +# CI log section helpers — emit collapsible group markers for +# GitHub Actions, Azure DevOps, and GitLab CI. +# GitLab CI requires unique section IDs within a job log, so callers +# in the loop below include _dashboard_iteration in the ID. +string(ASCII 27 _CI_ESC) + +function(ci_section_start section_id title) + if(DEFINED ENV{GITLAB_CI}) + string(TIMESTAMP _epoch "%s" UTC) + message("${_CI_ESC}[0Ksection_start:${_epoch}:${section_id}[collapsed=true]\r${_CI_ESC}[0K${title}") + elseif(DEFINED ENV{GITHUB_ACTIONS}) + message("::group::${title}") + elseif(DEFINED ENV{TF_BUILD}) + message("##[group]${title}") + else() + message("--- ${title} ---") + endif() +endfunction() + +function(ci_section_end section_id) + if(DEFINED ENV{GITLAB_CI}) + string(TIMESTAMP _epoch "%s" UTC) + message("${_CI_ESC}[0Ksection_end:${_epoch}:${section_id}\r${_CI_ESC}[0K") + elseif(DEFINED ENV{GITHUB_ACTIONS}) + message("::endgroup::") + elseif(DEFINED ENV{TF_BUILD}) + message("##[endgroup]") + else() + message("--- end ${section_id} ---") + endif() +endfunction() + if(COMMAND dashboard_hook_init) dashboard_hook_init() endif() set(dashboard_done 0) +set(_dashboard_iteration 0) while(NOT dashboard_done) + math(EXPR _dashboard_iteration "${_dashboard_iteration} + 1") if(dashboard_loop) set(START_TIME ${CTEST_ELAPSED_TIME}) endif() @@ -423,39 +457,51 @@ while(NOT dashboard_done) message("Found ${count} changed files") if(dashboard_fresh OR NOT dashboard_continuous OR count GREATER 0) + ci_section_start("configure_${_dashboard_iteration}" "Configure") ctest_configure(RETURN_VALUE configure_return) ctest_read_custom_files(${CTEST_BINARY_DIRECTORY}) + ci_section_end("configure_${_dashboard_iteration}") + ci_section_start("build_${_dashboard_iteration}" "Build") if(COMMAND dashboard_hook_build) dashboard_hook_build() endif() ctest_build(RETURN_VALUE build_return NUMBER_ERRORS build_errors NUMBER_WARNINGS build_warnings) + ci_section_end("build_${_dashboard_iteration}") + ci_section_start("test_${_dashboard_iteration}" "Test") if(COMMAND dashboard_hook_test) dashboard_hook_test() endif() ctest_test(${CTEST_TEST_ARGS} RETURN_VALUE test_return) + ci_section_end("test_${_dashboard_iteration}") if(dashboard_do_coverage) + ci_section_start("coverage_${_dashboard_iteration}" "Coverage") if(COMMAND dashboard_hook_coverage) dashboard_hook_coverage() endif() ctest_coverage(${CTEST_COVERAGE_ARGS}) + ci_section_end("coverage_${_dashboard_iteration}") endif() if(dashboard_do_memcheck) + ci_section_start("memcheck_${_dashboard_iteration}" "MemCheck") if(COMMAND dashboard_hook_memcheck) dashboard_hook_memcheck() endif() ctest_memcheck(${CTEST_MEMCHECK_ARGS}) + ci_section_end("memcheck_${_dashboard_iteration}") endif() + ci_section_start("submit_${_dashboard_iteration}" "Submit to CDash") if(COMMAND dashboard_hook_submit) dashboard_hook_submit() endif() if(NOT dashboard_no_submit) ctest_submit() endif() + ci_section_end("submit_${_dashboard_iteration}") if(COMMAND dashboard_hook_end) dashboard_hook_end() endif() From 773bdf81469e7d8514a4e9bddd17a64766afcafc Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Sun, 22 Mar 2026 08:43:23 -0500 Subject: [PATCH 2/2] ENH: Report build warnings and errors from Build.xml in CI logs ctest_build() only exposes warning/error counts via NUMBER_WARNINGS and NUMBER_ERRORS. The actual compiler messages are written to Build.xml for CDash submission but never printed to the CI log, making it hard to see what went wrong without visiting CDash. Add ci_report_build_diagnostics() which reads Build.xml after the build step, extracts content from and elements, and prints them directly to stdout. The call is intentionally placed outside the collapsible build section so that warnings and errors are always visible without having to expand any section. Co-Authored-By: Claude Opus 4.6 --- itk_common.cmake | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/itk_common.cmake b/itk_common.cmake index bd05b662802..0dffbc5a449 100644 --- a/itk_common.cmake +++ b/itk_common.cmake @@ -414,6 +414,74 @@ function(ci_section_end section_id) endif() endfunction() +# Extract and print build warnings/errors from Build.xml so they +# appear directly in the CI log. ctest_build() only exposes counts +# (NUMBER_WARNINGS / NUMBER_ERRORS) — the actual compiler messages +# live inside the XML that CTest writes for CDash submission. +function(ci_report_build_diagnostics binary_dir num_warnings num_errors) + if(num_warnings EQUAL 0 AND num_errors EQUAL 0) + return() + endif() + + # Locate the Build.xml via the TAG file that CTest maintains. + set(_tag_file "${binary_dir}/Testing/TAG") + if(NOT EXISTS "${_tag_file}") + return() + endif() + file(STRINGS "${_tag_file}" _tag_lines) + list(GET _tag_lines 0 _tag_dir) + set(_build_xml "${binary_dir}/Testing/${_tag_dir}/Build.xml") + if(NOT EXISTS "${_build_xml}") + return() + endif() + + # Read Build.xml — escape semicolons so CMake list operations + # don't mangle lines that happen to contain them. + file(READ "${_build_xml}" _xml) + string(REPLACE ";" "\\;" _xml "${_xml}") + string(REPLACE "\n" ";" _xml_lines "${_xml}") + + # Walk the XML line-by-line, tracking whether we are inside a + # or block, and collect the content. + set(_in_warning FALSE) + set(_in_error FALSE) + set(_warning_texts "") + set(_error_texts "") + foreach(_line IN LISTS _xml_lines) + if("${_line}" MATCHES "") + set(_in_warning TRUE) + elseif("${_line}" MATCHES "") + set(_in_warning FALSE) + elseif("${_line}" MATCHES "") + set(_in_error TRUE) + elseif("${_line}" MATCHES "") + set(_in_error FALSE) + endif() + if("${_line}" MATCHES "(.*)") + if(_in_warning) + list(APPEND _warning_texts "${CMAKE_MATCH_1}") + elseif(_in_error) + list(APPEND _error_texts "${CMAKE_MATCH_1}") + endif() + endif() + endforeach() + + # Print collected diagnostics so they are visible in CI output. + if(num_errors GREATER 0) + message("========== BUILD ERRORS (${num_errors}) ==========") + foreach(_t IN LISTS _error_texts) + message(" ${_t}") + endforeach() + endif() + if(num_warnings GREATER 0) + message("========== BUILD WARNINGS (${num_warnings}) ==========") + foreach(_t IN LISTS _warning_texts) + message(" ${_t}") + endforeach() + endif() + message("====================================================") +endfunction() + if(COMMAND dashboard_hook_init) dashboard_hook_init() endif() @@ -471,6 +539,12 @@ while(NOT dashboard_done) NUMBER_WARNINGS build_warnings) ci_section_end("build_${_dashboard_iteration}") + # Intentionally placed OUTSIDE the collapsible build section so + # that warnings and errors are always visible in the CI log + # without having to expand the build section. + ci_report_build_diagnostics( + "${CTEST_BINARY_DIRECTORY}" "${build_warnings}" "${build_errors}") + ci_section_start("test_${_dashboard_iteration}" "Test") if(COMMAND dashboard_hook_test) dashboard_hook_test()