diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index dccad1adaf2d9b05733c35d0165a8052cf35c1ef..69b00809c99a9893555fc7e61d45a4c4b427364f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,8 +52,10 @@ default:
       - .ccache
 
 .build-check: &template_build_check
+  variables:
+    LOG: build/build.log
   script:
-    - ci-utils/build-check build/build.log
+    - ci-utils/build-check ${LOG}
   allow_failure: true
 
 .test: &template_test
@@ -77,6 +79,25 @@ default:
     when: always
     expire_in: 1 week
 
+.test_headers: &template_test_headers
+  tags:
+    - cvmfs
+  script:
+    - export CCACHE_DIR=$PWD/.ccache_headers
+    - find build -type f -exec touch -d $(date +@%s) \{} \; # not to re-run cmake
+    - ccache -z
+    - cmake --build build --target test_public_headers_build 2>&1 | tee build/test_public_headers_build.log
+    - ccache -s
+  artifacts:
+    paths:
+      - build/test_public_headers_build.log
+    when: always
+    expire_in: 1 week
+  cache:
+    key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
+    paths:
+      - .ccache_headers
+
 
 ### Build
 x86_64-centos7-gcc11-opt:
@@ -135,12 +156,24 @@ x86_64-centos7-gcc11-opt:test:
     - job: "x86_64-centos7-gcc11-opt"
       artifacts: true
 
+x86_64-centos7-gcc11-opt:test_headers:
+  <<: *template_test_headers
+  needs:
+    - job: "x86_64-centos7-gcc11-opt"
+      artifacts: true
+
 x86_64-centos7-gcc11-dbg:test:
   <<: *template_test
   needs:
     - job: "x86_64-centos7-gcc11-dbg"
       artifacts: true
 
+x86_64-centos7-gcc11-dbg:test_headers:
+  <<: *template_test_headers
+  needs:
+    - job: "x86_64-centos7-gcc11-dbg"
+      artifacts: true
+
 view-gcc8:test:
   <<: *template_test
   needs:
@@ -189,6 +222,22 @@ x86_64-centos7-gcc11-dbg:build-check:
     - job: "x86_64-centos7-gcc11-dbg"
       artifacts: true
 
+x86_64-centos7-gcc11-opt:build-headers-check:
+  <<: *template_build_check
+  variables:
+    LOG: build/test_public_headers_build.log
+  needs:
+    - job: "x86_64-centos7-gcc11-opt:test_headers"
+      artifacts: true
+
+x86_64-centos7-gcc11-dbg:build-headers-check:
+  <<: *template_build_check
+  variables:
+    LOG: build/test_public_headers_build.log
+  needs:
+    - job: "x86_64-centos7-gcc11-dbg:test_headers"
+      artifacts: true
+
 view-gcc8:build-check:
   <<: *template_build_check
   needs:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8923c373c27bbeca3cfbd9c0108a5158efe1628f..959b0e6ee4a559a61f9d49e7edb885209fce5c18 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -194,6 +194,7 @@ install(EXPORT ${PROJECT_NAME} NAMESPACE ${PROJECT_NAME}::
             DESTINATION "${GAUDI_INSTALL_CONFIGDIR}")
 # Install cmake files for downstream project to be able to use Gaudi
 gaudi_install(CMAKE cmake/GaudiToolbox.cmake
+                    cmake/header_build_test.tpl
                     cmake/GaudiDependencies.cmake
                     cmake/DeveloperBuildType.cmake
                     "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
diff --git a/cmake/GaudiToolbox.cmake b/cmake/GaudiToolbox.cmake
index 47b7170fa66b64ea2994e39ba21595f15c8f1efb..7e4e5aae74a62203576a130789e7c00e2b601ef2 100644
--- a/cmake/GaudiToolbox.cmake
+++ b/cmake/GaudiToolbox.cmake
@@ -91,6 +91,8 @@ Functions
 include_guard(GLOBAL) # Protect from multiple include (global scope, because
                       # everything defined in this file is globally visible)
 
+set(GAUDI_TOOLBOX_DIR "${CMAKE_CURRENT_LIST_DIR}" CACHE PATH "Directory containing this file")
+
 ################################ Global options ################################
 
 # Option used to know if the install() function must be called with OPTIONAL
@@ -110,6 +112,10 @@ endif()
 # Option to prefer local targets to imported ones
 option(GAUDI_PREFER_LOCAL_TARGETS "Prefer local targets over imported ones" FALSE)
 
+option(GAUDI_TEST_PUBLIC_HEADERS_BUILD
+    "Execute a test build of all public headers in the global target 'all'"
+    FALSE)
+
 # Default layout fo the installation (may be overridden in the cache)
 set(CMAKE_INSTALL_BINDIR "bin" CACHE STRING "Install executable in <prefix>/\${CMAKE_INSTALL_BINDIR}")
 set(CMAKE_INSTALL_LIBDIR "lib" CACHE STRING "Install libraries in <prefix>/\${CMAKE_INSTALL_LIBDIR}")
@@ -118,7 +124,7 @@ set(GAUDI_INSTALL_PLUGINDIR "${CMAKE_INSTALL_LIBDIR}" CACHE STRING "Install plug
 set(GAUDI_INSTALL_PYTHONDIR "python" CACHE STRING "Install python packages in <prefix>/\${GAUDI_INSTALL_PYTHONDIR}")
 set(GAUDI_INSTALL_CONFIGDIR "lib/cmake/${PROJECT_NAME}" CACHE STRING "Install cmake files in <prefix>/\${GAUDI_INSTALL_CONFIGDIR}")
 
-set(scan_dict_deps_command ${CMAKE_CURRENT_LIST_DIR}/scan_dict_deps.py
+set(scan_dict_deps_command ${GAUDI_TOOLBOX_DIR}/scan_dict_deps.py
     CACHE INTERNAL "command to use to scan dependencies of dictionary headers")
 
 ################################## Functions  ##################################
@@ -152,6 +158,60 @@ macro(_gaudi_runtime_append runtime value)
     set_property(TARGET target_runtime_paths APPEND PROPERTY runtime_${runtime} ${value})
 endmacro()
 
+# Helper function to add build test for every public header
+function(_test_build_public_headers lib_name)
+    if(NOT BUILD_TESTING)
+        # we test the build of the public headers only when tests are enabled
+        return()
+    endif()
+    # collect the list of public headers
+    file(GLOB_RECURSE headers
+        RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/include
+        CONFIGURE_DEPENDS
+        ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h
+        ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hxx
+        ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp
+    )
+    # list the test source files generated (in a previous config)
+    file(GLOB_RECURSE old_srcs
+        ${CMAKE_CURRENT_BINARY_DIR}/headers_build_test/*.h.cpp
+        ${CMAKE_CURRENT_BINARY_DIR}/headers_build_test/*.hxx.cpp
+        ${CMAKE_CURRENT_BINARY_DIR}/headers_build_test/*.hpp.cpp
+    )
+    set(srcs)
+    if(headers)
+        # create a cpp file for each header file to test
+        foreach(header IN LISTS headers)
+            set(s "${CMAKE_CURRENT_BINARY_DIR}/headers_build_test/${header}.cpp")
+            configure_file("${GAUDI_TOOLBOX_DIR}/header_build_test.tpl" "${s}")
+            list(APPEND srcs "${s}")
+        endforeach()
+        # create an object library (we never need to use the product)
+        add_library(test_public_headers_build_${lib_name} OBJECT EXCLUDE_FROM_ALL ${srcs})        
+        target_link_libraries(test_public_headers_build_${lib_name}
+            PRIVATE ${lib_name})
+        target_compile_definitions(test_public_headers_build_${lib_name}
+            PRIVATE GAUDI_TEST_PUBLIC_HEADERS_BUILD)
+        # add it to the global target "test_public_headers_build" (making sure it exists)
+        if(NOT TARGET test_public_headers_build)
+            if(GAUDI_TEST_PUBLIC_HEADERS_BUILD)
+                add_custom_target(test_public_headers_build ALL
+                    COMMENT "test build of public headers")
+            else()
+                add_custom_target(test_public_headers_build
+                    COMMENT "test build of public headers")
+            endif()
+        endif()
+        add_dependencies(test_public_headers_build test_public_headers_build_${lib_name})
+        # keep in old_srcs only the source files not needed anymore 
+        list(REMOVE_ITEM old_srcs ${srcs})
+    endif()
+    # remove stale test source files
+    if(old_srcs)
+        file(REMOVE ${old_srcs})
+    endif()
+endfunction()
+
 #[========================================================================[.rst:
 .. command:: gaudi_add_library
 
@@ -216,6 +276,7 @@ function(gaudi_add_library lib_name)
         install(DIRECTORY include/
                 TYPE INCLUDE)
         set_property(DIRECTORY PROPERTY include_installed TRUE)
+        _test_build_public_headers(${lib_name})
     endif()
     # Runtime ROOT_INCLUDE_PATH
     _gaudi_runtime_append(root_include_path $<TARGET_PROPERTY:${lib_name},INTERFACE_INCLUDE_DIRECTORIES>)
@@ -287,6 +348,7 @@ function(gaudi_add_header_only_library lib_name)
         install(DIRECTORY include/
                 TYPE INCLUDE)
         set_property(DIRECTORY PROPERTY include_installed TRUE)
+        _test_build_public_headers(${lib_name})
     endif()
     # Runtime ROOT_INCLUDE_PATH
     _gaudi_runtime_append(root_include_path $<TARGET_PROPERTY:${lib_name},INTERFACE_INCLUDE_DIRECTORIES>)
diff --git a/cmake/header_build_test.tpl b/cmake/header_build_test.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..6c200e893f0130704e134a415fd59d4fd7f3a675
--- /dev/null
+++ b/cmake/header_build_test.tpl
@@ -0,0 +1 @@
+#include <${header}>