diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 000000000..fff68e0d0
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,49 @@
+name: CI
+run-name: >
+  CI (${{ github.event_name }})
+  ${{ github.event_name == 'pull_request' && format('PR#{0}', github.event.number) || '' }}
+
+on:
+  workflow_dispatch:
+  pull_request:
+    branches: [ develop ]
+  push:
+    branches: [ develop ]
+
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  pre-commit:
+    name: pre-commit
+    uses: ./.github/workflows/step_pre-commit.yaml
+
+  tests:
+    name: test
+    needs: [ pre-commit ]
+    uses: ./.github/workflows/step_test.yaml
+
+  tests-makefile:
+    name: test Makefile
+    needs: [ pre-commit ]
+    uses: ./.github/workflows/step_test-makefile.yaml
+
+  docs:
+    name: 📘 docs
+    needs: [ pre-commit ]
+    uses: ./.github/workflows/step_docs.yaml
+
+  pass:
+    name: ✅ Pass
+    needs: [ pre-commit, tests, tests-makefile, docs ]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check all CI jobs
+        uses: re-actors/alls-green@release/v1
+        with:
+          jobs: ${{ toJSON(needs) }}
+    if: always()
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
deleted file mode 100644
index 9ae2103d8..000000000
--- a/.github/workflows/docs.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Build docs
-on:
-  pull_request:
-  push:
-    branches:
-      - develop
-
-jobs:
-  docs:
-      name: Build docs
-      runs-on: ubuntu-latest
-      steps:
-        - uses: actions/checkout@v4
-        - uses: actions/setup-python@v5
-          with:
-            python-version: "3.11"
-        - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
-        - uses: actions/cache@v4
-          with:
-            key: mkdocs-w90-${{ env.cache_id }}
-            path: .cache
-            restore-keys: |
-              mkdocs-w90-
-        - run: pip install -r docs/requirements.txt
-        - run: mkdocs build -s
-          working-directory: ./docs
-          env:
-            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-            ENABLE_MKDOCS_GIT_COMMITTERS: False
-
-        - name: Deploy to GitHub Pages
-          uses: peaceiris/actions-gh-pages@v3
-          if: github.ref == 'refs/heads/develop'
-          with:
-            github_token: ${{ secrets.GITHUB_TOKEN }}
-            publish_dir: ./docs/site
diff --git a/.github/workflows/step_docs.yaml b/.github/workflows/step_docs.yaml
new file mode 100644
index 000000000..7d72a351a
--- /dev/null
+++ b/.github/workflows/step_docs.yaml
@@ -0,0 +1,37 @@
+name: 📘 test-docs
+
+on:
+  workflow_call:
+
+permissions:
+  contents: read
+
+jobs:
+  docs:
+    name: Build docs
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.11"
+      - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
+      - uses: actions/cache@v4
+        with:
+          key: mkdocs-w90-${{ env.cache_id }}
+          path: .cache
+          restore-keys: |
+            mkdocs-w90-
+      - run: pip install -r docs/requirements.txt
+      - run: mkdocs build -s
+        working-directory: ./docs
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          ENABLE_MKDOCS_GIT_COMMITTERS: False
+
+      - name: Deploy to GitHub Pages
+        uses: peaceiris/actions-gh-pages@v3
+        if: github.ref == 'refs/heads/develop'
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          publish_dir: ./docs/site
diff --git a/.github/workflows/step_pre-commit.yaml b/.github/workflows/step_pre-commit.yaml
new file mode 100644
index 000000000..fbd8b279c
--- /dev/null
+++ b/.github/workflows/step_pre-commit.yaml
@@ -0,0 +1,20 @@
+name: pre-commit
+
+on:
+  workflow_call:
+
+permissions:
+  contents: read
+
+jobs:
+  pre-commit:
+    name: Check pre-commit
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+      - uses: pre-commit/action@v3.0.0
diff --git a/.github/workflows/main.yml b/.github/workflows/step_test-makefile.yaml
similarity index 88%
rename from .github/workflows/main.yml
rename to .github/workflows/step_test-makefile.yaml
index 4d850d6f2..0459a9f56 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/step_test-makefile.yaml
@@ -1,19 +1,12 @@
-name: CI
+name: test Makefile
 
 on:
-  pull_request:
-  push:
-    branches:
-    - develop
+  workflow_call:
+
+permissions:
+  contents: read
     
 jobs:
-  pre-commit:
-    runs-on: ubuntu-20.04
-    steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-      - uses: pre-commit/action@v2.0.0
-
   build:
     runs-on: ubuntu-20.04
     strategy:
diff --git a/.github/workflows/step_test.yaml b/.github/workflows/step_test.yaml
new file mode 100644
index 000000000..9c661d2e9
--- /dev/null
+++ b/.github/workflows/step_test.yaml
@@ -0,0 +1,86 @@
+name: test
+
+on:
+  workflow_call:
+
+permissions:
+  contents: read
+
+jobs:
+  tests:
+    name: Check on ${{ matrix.toolchain }} toolchain ${{ matrix.mpi }}
+    runs-on: ${{ matrix.os || 'ubuntu-latest' }}
+    container: ${{ !matrix.os && 'ghcr.io/lecrisut/dev-env:main' || '' }}
+    continue-on-error: ${{ matrix.experimental || false }}
+    strategy:
+      fail-fast: false
+      matrix:
+        toolchain: [ gcc, llvm, intel ]
+        mpi: ['', openmpi, mpich, intel]
+        include:
+          # flang is missing features in 16.0.6
+          - toolchain: llvm
+            experimental: true
+    steps:
+      - name: Install missing packages
+        run: dnf install -y bzip2 python-unversioned-command
+      - name: Load mpi module ${{ matrix.mpi }}
+        run: |
+          # Get interactive profile to be able to load modules
+          source /etc/profile
+          
+          # Save the current environment since we only want the added difference
+          printenv > orig_env
+          
+          # Load the relevant mpi module
+          module load mpi/${{ matrix.mpi }}
+          printenv > module_env
+          
+          diff orig_env module_env | sed -n 's/> //p' >> $GITHUB_ENV
+          
+          # Set MPI flag on
+          echo "WITH_MPI=ON" >> $GITHUB_ENV
+        if: ${{ matrix.mpi }}
+      - name: Enable msvc toolchain on windows
+        uses: ilammy/msvc-dev-cmd@v1
+        if: contains(matrix.os, 'windows')
+      - name: Activate Intel compilers
+        run: |
+          source /etc/profile
+          printenv > orig_env
+          module load compiler
+          printenv > module_env
+          diff orig_env module_env | sed -n 's/> //p' >> $GITHUB_ENV
+          echo $PATH >> $GITHUB_PATH
+        if: matrix.toolchain == 'intel'
+      - uses: actions/checkout@v3
+      - uses: lukka/get-cmake@latest
+      - name: Run CMake configuration for ${{ matrix.toolchain }} toolchain
+        uses: lukka/run-cmake@v10.3
+        with:
+          workflowPreset: "${{ matrix.toolchain }}-ci"
+
+  coverage:
+    name: Check test coverage
+    runs-on: ubuntu-latest
+    needs: [ tests ]
+    steps:
+      - uses: actions/checkout@v3
+      - uses: lukka/get-cmake@latest
+      - name: Get test coverage
+        uses: lukka/run-cmake@v10.3
+        with:
+          workflowPreset: ci-coverage
+      - name: Get lcov data
+        uses: danielealbano/lcov-action@v3
+        with:
+          # Note lcov-action prepends and appends wild-cards *. Account for those
+          # https://github.com/danielealbano/lcov-action/issues/11
+          remove_patterns: /test/,/cmake-build*/
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v3
+        with:
+          name: ${{ matrix.coverage }} coverage
+          files: coverage.info
+          flags: ${{ matrix.coverage }}
+          verbose: true