diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index b5f0965..9c0bc3b 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -45,5 +45,21 @@ jobs:
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
+ #- name: Cache SDKMan
+ # id: cache-sdkman
+ # uses: actions/cache@v4
+ # with:
+ # path: ~/.sdkman
+ # key: ${{ runner.os }}-sdkman
+
- name: 'Build project'
run: mvn -B clean verify -Pci
+
+ - name: 'Setup SDKMAN'
+ uses: sdkman/sdkman-action@b1f9b696c79148b66d3d3a06f7ea801820318d0f
+ id: sdkman
+ if: github.event_name == 'pull_request'
+
+ - name: 'Test submission'
+ run: ./test_ci.sh ${{ github.event.pull_request.user.login }}
+ if: github.event_name == 'pull_request'
diff --git a/.sdkmanrc b/.sdkmanrc
new file mode 100644
index 0000000..dfb233b
--- /dev/null
+++ b/.sdkmanrc
@@ -0,0 +1,3 @@
+# Enable auto-env through the sdkman_auto_env config
+# Add key=value pairs of SDKs to use below
+java=21.0.1-open
diff --git a/pom.xml b/pom.xml
index 51226a3..a2c91e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -211,6 +211,7 @@
github_users.txt
src/main/java/dev/morling/onebrc/CalculateAverage_cliffclick.java
+ .sdkmanrc
diff --git a/test_ci.sh b/test_ci.sh
new file mode 100755
index 0000000..1f399e6
--- /dev/null
+++ b/test_ci.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+#
+# Copyright 2023 The original authors
+#
+# Licensed 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 -eo pipefail
+
+if [ -z "$1" ]
+ then
+ echo "Usage: test_ci.sh ( ...)"
+ echo " for each fork, there must be a 'calculate_average_.sh' script and an optional 'prepare_.sh'."
+ exit 1
+fi
+
+BOLD_WHITE='\033[1;37m'
+CYAN='\033[0;36m'
+GREEN='\033[0;32m'
+PURPLE='\033[0;35m'
+BOLD_RED='\033[1;31m'
+RED='\033[0;31m'
+BOLD_YELLOW='\033[1;33m'
+RESET='\033[0m' # No Color
+
+MEASUREMENTS_FILE="measurements_10M.txt"
+RUNS=5
+DEFAULT_JAVA_VERSION="21.0.1-open"
+RUN_TIME_LIMIT=300 # seconds
+
+TIMEOUT=""
+if [ "$(uname -s)" == "Linux" ]; then
+ TIMEOUT="timeout -v $RUN_TIME_LIMIT"
+else # MacOs
+ if [ -x "$(command -v gtimeout)" ]; then
+ TIMEOUT="gtimeout -v $RUN_TIME_LIMIT" # from `brew install coreutils`
+ else
+ echo -e "${BOLD_YELLOW}WARNING${RESET} gtimeout not available, benchmark runs may take indefinitely long."
+ fi
+fi
+
+function check_command_installed {
+ if ! [ -x "$(command -v $1)" ]; then
+ echo "Error: $1 is not installed." >&2
+ exit 1
+ fi
+}
+
+function print_and_execute() {
+ echo "+ $@" >&2
+ "$@"
+}
+
+check_command_installed java
+
+# Validate that ./calculate_average_.sh exists for each fork
+for fork in "$@"; do
+ if [ ! -f "./calculate_average_$fork.sh" ]; then
+ echo -e "${BOLD_RED}ERROR${RESET}: ./calculate_average_$fork.sh does not exist." >&2
+ exit 1
+ fi
+done
+
+## SDKMAN Setup
+# 1. Custom check for sdkman installed; not sure why check_command_installed doesn't detect it properly
+if [ ! -f "$HOME/.sdkman/bin/sdkman-init.sh" ]; then
+ echo -e "${BOLD_RED}ERROR${RESET}: sdkman is not installed." >&2
+ exit 1
+fi
+
+# 2. Init sdkman in this script
+source "$HOME/.sdkman/bin/sdkman-init.sh"
+
+# 3. make sure the default java version is installed
+if [ ! -d "$HOME/.sdkman/candidates/java/$DEFAULT_JAVA_VERSION" ]; then
+ print_and_execute sdk install java $DEFAULT_JAVA_VERSION
+fi
+
+# 4. Install missing SDK java versions in any of the prepare_*.sh scripts for the provided forks
+for fork in "$@"; do
+ if [ -f "./prepare_$fork.sh" ]; then
+ grep -h "^sdk use" "./prepare_$fork.sh" | cut -d' ' -f4 | while read -r version; do
+ if [ ! -d "$HOME/.sdkman/candidates/java/$version" ]; then
+ print_and_execute sdk install java $version
+ fi
+ done || true # grep returns exit code 1 when no match, `|| true` prevents the script from exiting early
+ fi
+done
+## END - SDKMAN Setup
+
+# Run tests and benchmark for each fork
+filetimestamp=$(date +"%Y%m%d%H%M%S") # same for all fork.out files from this run
+failed=()
+for fork in "$@"; do
+ set +e # we don't want prepare.sh, test.sh or hyperfine failing on 1 fork to exit the script early
+
+ # Run prepare script
+ if [ -f "./prepare_$fork.sh" ]; then
+ print_and_execute source "./prepare_$fork.sh"
+ else
+ print_and_execute sdk use java $DEFAULT_JAVA_VERSION
+ fi
+
+ # Run the test suite
+ print_and_execute $TIMEOUT ./test.sh $fork
+ if [ $? -ne 0 ]; then
+ failed+=("$fork")
+ echo ""
+ echo -e "${BOLD_RED}FAILURE${RESET}: ./test.sh $fork failed"
+
+ exit 1
+ fi
+ echo ""
+done