diff --git a/.ci/config.cmake b/.ci/config.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..bf59fdee6c3bd82f0cd257235cafc4c32456cb8f
--- /dev/null
+++ b/.ci/config.cmake
@@ -0,0 +1,9 @@
+set(BUILD_BENCHMARKS    1 CACHE BOOL "")
+set(BUILD_TESTING       1 CACHE BOOL "")
+set(BUILD_GOOGLETEST    1 CACHE BOOL "")
+set(BUILD_GOOGLEBENCH   1 CACHE BOOL "")
+set(BUILD_UMESIMD ${UNIX} CACHE BOOL "")
+set(BUILD_VC      ${UNIX} CACHE BOOL "")
+set(UMESIMD       ${UNIX} CACHE BOOL "")
+set(VC            ${UNIX} CACHE BOOL "")
+set(CMAKE_DISABLE_FIND_PACKAGE_PkgConfig ${WIN32} CACHE BOOL "")
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8307b936c227998260205008d0a48ef1155b9800..069a719c47cfd2b3fdf0c8e7a8f3578896e4937c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -84,6 +84,7 @@ if (BUILD_DOCS)
   add_subdirectory(doc)
 endif()
 
+include(CTest)
 enable_testing()
 
 if (BUILD_TESTING)
diff --git a/cmake/TestVecCore.cmake b/cmake/TestVecCore.cmake
index 9ea6c80de1a6e40b690ef6735fdf1e3b78c14069..117f05c72033d5e8cf9fa0d9eba333b05619ca6b 100644
--- a/cmake/TestVecCore.cmake
+++ b/cmake/TestVecCore.cmake
@@ -1,9 +1,10 @@
-cmake_minimum_required(VERSION 3.16...3.25)
+cmake_minimum_required(VERSION 3.16...3.29)
 
 set(CTEST_PROJECT_NAME "VecCore")
 
 set(ENV{LANG} "C")
 set(ENV{LC_ALL} "C")
+set(CTEST_USE_LAUNCHERS TRUE)
 
 if(CDASH)
   set(CTEST_DROP_METHOD "http")
@@ -12,17 +13,23 @@ if(CDASH)
   set(CTEST_DROP_SITE_CDASH TRUE)
 endif()
 
-if(NOT DEFINED CTEST_SITE)
-  site_name(CTEST_SITE)
-  if(EXISTS "/etc/os-release")
-    file(STRINGS "/etc/os-release" OSPN REGEX "^PRETTY_NAME=.*$")
-    string(REGEX REPLACE "PRETTY_NAME=\"(.*)\"$" "\\1" DISTRO "${OSPN}")
-    string(APPEND CTEST_SITE " (${DISTRO} ${CMAKE_SYSTEM_PROCESSOR})")
-  else()
-    cmake_host_system_information(RESULT OS_NAME QUERY OS_NAME)
-    cmake_host_system_information(RESULT OS_VERSION QUERY OS_VERSION)
-    string(APPEND CTEST_SITE " (${OS_NAME} ${OS_VERSION} ${CMAKE_SYSTEM_PROCESSOR})")
-  endif()
+if(EXISTS "/etc/os-release")
+  file(STRINGS "/etc/os-release" OS_NAME REGEX "^ID=.*$")
+  string(REGEX REPLACE "ID=[\"']?([^\"']*)[\"']?$" "\\1" OS_NAME "${OS_NAME}")
+  file(STRINGS "/etc/os-release" OS_VERSION REGEX "^VERSION_ID=.*$")
+  string(REGEX REPLACE "VERSION_ID=[\"']?([^\"'.]*).*$" "\\1" OS_VERSION "${OS_VERSION}")
+  file(STRINGS "/etc/os-release" OS_FULL_NAME REGEX "^PRETTY_NAME=.*$")
+  string(REGEX REPLACE "PRETTY_NAME=[\"']?([^\"']*)[\"']?$" "\\1" OS_FULL_NAME "${OS_FULL_NAME}")
+  string(REGEX REPLACE "[ ]*\\(.*\\)" "" OS_FULL_NAME "${OS_FULL_NAME}")
+elseif(APPLE)
+  set(OS_NAME "macOS")
+  execute_process(COMMAND sw_vers -productVersion
+    OUTPUT_VARIABLE OS_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
+  set(OS_FULL_NAME "${OS_NAME} ${OS_VERSION}")
+else()
+  cmake_host_system_information(RESULT OS_NAME QUERY OS_NAME)
+  cmake_host_system_information(RESULT OS_VERSION QUERY OS_VERSION)
+  set(OS_FULL_NAME "${OS_NAME} ${OS_VERSION}")
 endif()
 
 cmake_host_system_information(RESULT
@@ -38,18 +45,6 @@ if(NOT DEFINED ENV{CTEST_PARALLEL_LEVEL})
   set(ENV{CTEST_PARALLEL_LEVEL} ${NCORES})
 endif()
 
-if(DEFINED ENV{CMAKE_GENERATOR})
-  set(CTEST_CMAKE_GENERATOR $ENV{CMAKE_GENERATOR})
-else()
-  execute_process(COMMAND ${CMAKE_COMMAND} --system-information
-    OUTPUT_VARIABLE CMAKE_SYSTEM_INFORMATION ERROR_VARIABLE ERROR)
-  if(ERROR)
-    message(FATAL_ERROR "Could not detect default CMake generator")
-  endif()
-  string(REGEX REPLACE ".+CMAKE_GENERATOR \"([-0-9A-Za-z ]+)\".*$" "\\1"
-    CTEST_CMAKE_GENERATOR "${CMAKE_SYSTEM_INFORMATION}")
-endif()
-
 if(NOT DEFINED CTEST_CONFIGURATION_TYPE)
   if(DEFINED ENV{CMAKE_BUILD_TYPE})
     set(CTEST_CONFIGURATION_TYPE $ENV{CMAKE_BUILD_TYPE})
@@ -58,14 +53,6 @@ if(NOT DEFINED CTEST_CONFIGURATION_TYPE)
   endif()
 endif()
 
-if(DEFINED CTEST_SCRIPT_ARG)
-  set(TARGET_ISA ${CTEST_SCRIPT_ARG})
-else()
-  set(TARGET_ISA Native)
-endif()
-
-set(CTEST_BUILD_NAME "${CMAKE_SYSTEM_NAME}")
-
 execute_process(COMMAND ${CMAKE_COMMAND} --system-information
   OUTPUT_VARIABLE CMAKE_SYSTEM_INFORMATION ERROR_VARIABLE ERROR)
 
@@ -80,55 +67,211 @@ string(REPLACE "GNU" "GCC" COMPILER_ID "${COMPILER_ID}")
 string(REGEX REPLACE ".+CMAKE_CXX_COMPILER_VERSION \"([^\"]+)\".*$" "\\1"
   COMPILER_VERSION "${CMAKE_SYSTEM_INFORMATION}")
 
+set(CTEST_BUILD_NAME "${OS_FULL_NAME}")
 string(APPEND CTEST_BUILD_NAME " ${COMPILER_ID} ${COMPILER_VERSION}")
 string(APPEND CTEST_BUILD_NAME " ${CTEST_CONFIGURATION_TYPE}")
-string(APPEND CTEST_BUILD_NAME " ${TARGET_ISA}")
+
+if(DEFINED ENV{CMAKE_GENERATOR})
+  set(CTEST_CMAKE_GENERATOR $ENV{CMAKE_GENERATOR})
+else()
+  string(REGEX REPLACE ".+CMAKE_GENERATOR \"([-0-9A-Za-z ]+)\".*$" "\\1"
+    CTEST_CMAKE_GENERATOR "${CMAKE_SYSTEM_INFORMATION}")
+endif()
+
+if(NOT CTEST_CMAKE_GENERATOR MATCHES "Makefile")
+  string(APPEND CTEST_BUILD_NAME " ${CTEST_CMAKE_GENERATOR}")
+endif()
+
+if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
+  string(APPEND CTEST_BUILD_NAME " ${CMAKE_SYSTEM_PROCESSOR}")
+endif()
+
+if(DEFINED ENV{GITHUB_ACTIONS})
+  set(CTEST_SITE "GitHub Actions ($ENV{GITHUB_REPOSITORY_OWNER})")
+
+  if("$ENV{GITHUB_REPOSITORY_OWNER}" STREQUAL "root-project")
+    set(CDASH TRUE)
+    set(MODEL "Continuous")
+  endif()
+
+  if("$ENV{GITHUB_EVENT_NAME}" MATCHES "pull_request")
+    set(GROUP "Pull Requests")
+    set(ENV{BASE_REF} $ENV{GITHUB_SHA}^1)
+    set(ENV{HEAD_REF} $ENV{GITHUB_SHA}^2)
+    string(REGEX REPLACE "/merge" "" PR_NUMBER "$ENV{GITHUB_REF_NAME}")
+    string(PREPEND CTEST_BUILD_NAME "#${PR_NUMBER} ($ENV{GITHUB_ACTOR}) ")
+  else()
+    set(ENV{HEAD_REF} $ENV{GITHUB_SHA})
+    string(APPEND CTEST_BUILD_NAME " ($ENV{GITHUB_REF_NAME})")
+  endif()
+
+  if("$ENV{GITHUB_RUN_ATTEMPT}" GREATER 1)
+    string(APPEND CTEST_BUILD_NAME " #$ENV{GITHUB_RUN_ATTEMPT}")
+  endif()
+
+  macro(section title)
+      message("::group::${title}")
+  endmacro()
+
+  macro(endsection)
+      message("::endgroup::")
+  endmacro()
+else()
+  macro(section title)
+  endmacro()
+  macro(endsection)
+  endmacro()
+endif()
+
+if(NOT DEFINED CTEST_SITE)
+  site_name(CTEST_SITE)
+endif()
 
 if(NOT DEFINED CTEST_SOURCE_DIRECTORY)
-  set(CTEST_SOURCE_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/..")
+  get_filename_component(CTEST_SOURCE_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/.." REALPATH)
 endif()
 
 if(NOT DEFINED CTEST_BINARY_DIRECTORY)
-  get_filename_component(CTEST_BINARY_DIRECTORY "$ENV{PWD}/build" REALPATH)
+  get_filename_component(CTEST_BINARY_DIRECTORY "build" REALPATH)
 endif()
 
-if(EXISTS "${CTEST_BINARY_DIRECTORY}")
-  ctest_empty_binary_directory("${CTEST_BINARY_DIRECTORY}")
+if(NOT DEFINED MODEL)
+  set(MODEL Experimental)
+endif()
+
+if(NOT DEFINED GROUP)
+  set(GROUP ${MODEL})
+endif()
+
+if(DEFINED CTEST_SCRIPT_ARG)
+  set(TARGET_ISA ${CTEST_SCRIPT_ARG})
+else()
+  set(TARGET_ISA Native)
+endif()
+
+string(APPEND CTEST_BUILD_NAME " ${TARGET_ISA}")
+
+set(CMAKE_ARGS $ENV{CMAKE_ARGS} ${CMAKE_ARGS})
+
+if(COVERAGE)
+  find_program(CTEST_COVERAGE_COMMAND NAMES gcov)
+  list(PREPEND CMAKE_ARGS "-DCMAKE_C_FLAGS=--coverage -fprofile-update=atomic")
+  list(PREPEND CMAKE_ARGS "-DCMAKE_CXX_FLAGS=--coverage -fprofile-update=atomic")
 endif()
 
 if(MEMCHECK)
   find_program(CTEST_MEMORYCHECK_COMMAND NAMES valgrind)
 endif()
 
-set(CMAKE_ARGS
-  -DBUILD_BENCHMARKS=ON
-  -DBUILD_TESTING=ON
-  -DBUILD_GOOGLETEST=ON
-  -DBUILD_GOOGLEBENCH=ON
-  -DBUILD_UMESIMD=${UNIX}
-  -DBUILD_VC=${UNIX}
-  -DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=${WIN32}
-  -DTARGET_ISA=${TARGET_ISA}
-  $ENV{CMAKE_ARGS}
-  ${CMAKE_ARGS}
-)
+if(STATIC_ANALYSIS)
+  find_program(CMAKE_CXX_CLANG_TIDY NAMES clang-tidy)
+  list(PREPEND CMAKE_ARGS "-DCMAKE_CXX_CLANG_TIDY=${CMAKE_CXX_CLANG_TIDY}")
+endif()
 
-if(NOT DEFINED MODEL)
-  set(MODEL Experimental)
+foreach(FILENAME ${OS_NAME}${OS_VERSION}.cmake ${OS_NAME}.cmake config.cmake)
+  if(EXISTS "${CTEST_SOURCE_DIRECTORY}/.ci/${FILENAME}")
+    message(STATUS "Using CMake cache file ${FILENAME}")
+    list(PREPEND CMAKE_ARGS -C ${CTEST_SOURCE_DIRECTORY}/.ci/${FILENAME})
+    list(APPEND CTEST_NOTES_FILES ${CTEST_SOURCE_DIRECTORY}/.ci/${FILENAME})
+    break()
+  endif()
+endforeach()
+
+if(IS_DIRECTORY "${CTEST_BINARY_DIRECTORY}")
+  ctest_empty_binary_directory("${CTEST_BINARY_DIRECTORY}")
 endif()
 
 ctest_read_custom_files("${CTEST_SOURCE_DIRECTORY}")
 
-ctest_start(${MODEL})
-ctest_configure(OPTIONS "${CMAKE_ARGS}")
+if(IS_DIRECTORY ${CTEST_SOURCE_DIRECTORY}/.git)
+  find_program(CTEST_GIT_COMMAND NAMES git)
+endif()
+
+if(EXISTS "${CTEST_GIT_COMMAND}" AND DEFINED ENV{HEAD_REF} AND NOT DEFINED ENV{BASE_REF})
+  set(CTEST_CHECKOUT_COMMAND
+    "\"${CTEST_GIT_COMMAND}\" --git-dir \"${CTEST_SOURCE_DIRECTORY}/.git\" checkout -f $ENV{HEAD_REF}")
+endif()
+
+ctest_start(${MODEL} GROUP "${GROUP}")
+
+if(EXISTS "${CTEST_GIT_COMMAND}")
+  if(DEFINED ENV{BASE_REF})
+    execute_process(COMMAND ${CTEST_GIT_COMMAND} checkout -f $ENV{BASE_REF}
+      WORKING_DIRECTORY ${CTEST_SOURCE_DIRECTORY} ERROR_QUIET RESULT_VARIABLE GIT_STATUS)
+    if(NOT ${GIT_STATUS} EQUAL 0)
+      message(FATAL_ERROR "Could not checkout base ref: $ENV{BASE_REF}")
+    endif()
+  endif()
+  if(DEFINED ENV{HEAD_REF})
+    set(CTEST_GIT_UPDATE_CUSTOM
+      ${CTEST_GIT_COMMAND} --git-dir ${CTEST_SOURCE_DIRECTORY}/.git checkout -f $ENV{HEAD_REF})
+  else()
+    set(CTEST_GIT_UPDATE_CUSTOM ${CTEST_GIT_COMMAND} --git-dir ${CTEST_SOURCE_DIRECTORY}/.git diff)
+  endif()
+
+  ctest_update()
+endif()
+
+section("Configure")
+ctest_configure(OPTIONS "${CMAKE_ARGS}" RETURN_VALUE CONFIG_RESULT)
 
 ctest_read_custom_files("${CTEST_BINARY_DIRECTORY}")
 list(APPEND CTEST_NOTES_FILES ${CTEST_BINARY_DIRECTORY}/CMakeCache.txt)
 
-ctest_build()
-ctest_test()
-ctest_memcheck()
+if(NOT ${CONFIG_RESULT} EQUAL 0)
+  if(CDASH OR (DEFINED ENV{CDASH} AND "$ENV{CDASH}"))
+    ctest_submit()
+  endif()
+  message(FATAL_ERROR "Configuration failed")
+endif()
+endsection()
+
+section("Build")
+ctest_build(RETURN_VALUE BUILD_RESULT)
 
-if(CDASH)
+if(NOT ${BUILD_RESULT} EQUAL 0)
+  if(CDASH OR (DEFINED ENV{CDASH} AND "$ENV{CDASH}"))
+    ctest_submit()
+  endif()
+  message(FATAL_ERROR "Build failed")
+endif()
+
+if(INSTALL)
+  set(ENV{DESTDIR} "${CTEST_BINARY_DIRECTORY}/install")
+  ctest_build(TARGET install)
+endif()
+endsection()
+
+section("Test")
+ctest_test(PARALLEL_LEVEL $ENV{CTEST_PARALLEL_LEVEL} RETURN_VALUE TEST_RESULT)
+
+if(NOT ${TEST_RESULT} EQUAL 0)
+  message(SEND_ERROR "Tests failed")
+endif()
+endsection()
+
+if(DEFINED CTEST_COVERAGE_COMMAND)
+  section("Coverage")
+  find_program(GCOVR NAMES gcovr)
+  if(EXISTS ${GCOVR})
+    execute_process(COMMAND
+      ${GCOVR} --gcov-executable ${CTEST_COVERAGE_COMMAND}
+        -r ${CTEST_SOURCE_DIRECTORY} ${CTEST_BINARY_DIRECTORY}
+        --html-details ${CTEST_BINARY_DIRECTORY}/coverage/ ERROR_VARIABLE ERROR)
+    if(ERROR)
+      message(SEND_ERROR "Failed to generate coverage report")
+    endif()
+  endif()
+  ctest_coverage()
+  endsection()
+endif()
+
+if(DEFINED CTEST_MEMORYCHECK_COMMAND)
+  section("Memcheck")
+  ctest_memcheck()
+  endsection()
+endif()
+
+if(CDASH OR (DEFINED ENV{CDASH} AND "$ENV{CDASH}"))
   ctest_submit()
 endif()