diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,9 @@
-Bloodhound [![TravisCI](https://travis-ci.org/bitemyapp/bloodhound.svg)](https://travis-ci.org/bitemyapp/bloodhound) [![Hackage](https://img.shields.io/hackage/v/bloodhound.svg?style=flat)](https://hackage.haskell.org/package/bloodhound)
+Bloodhound 
+![compatbuild](https://github.com/bitemyapp/bloodhound/actions/workflows/compat.yml/badge.svg)
+![haskell-ci](https://github.com/bitemyapp/bloodhound/actions/workflows/haskell-ci.yml/badge.svg)
+![nix](https://github.com/bitemyapp/bloodhound/actions/workflows/nix.yml/badge.svg)
+![ormolu](https://github.com/bitemyapp/bloodhound/actions/workflows/ormolu.yml/badge.svg)
+[![Hackage](https://img.shields.io/hackage/v/bloodhound.svg?style=flat)](https://hackage.haskell.org/package/bloodhound)
 ==========
 
 ![Bloodhound (dog)](./bloodhound.jpg)
@@ -21,10 +26,13 @@
 Version compatibility
 ---------------------
 
-See our [TravisCI](https://travis-ci.org/bitemyapp/bloodhound) for a
-listing of Elasticsearch version we test against.
+See the [Github compatibility workflow](./.github/workflows/compat.yml) for a
+listing of Elasticsearch and OpenSearch versions we test against.
 
+The workflow executions can be seen in the [Github actions
+view](https://github.com/bitemyapp/bloodhound/actions/workflows/compat.yml).
 
+
 Stability
 ---------
 
@@ -33,15 +41,16 @@
 Testing
 -------
 
-The TravisCI tests are run using [Stack](http://docs.haskellstack.org/en/stable/README.html). You should use Stack instead of `cabal` to build and test Bloodhound to avoid compatibility problems. You will also need to have an Elasticsearch instance running at `localhost:9200` in order to execute some of the tests. See the "Version compatibility" section above for a list of Elasticsearch versions that are officially validated against in TravisCI.
+The Bloodhound project uses Github workflows using Cabal to test for regressions
+and compatibility. A convenient development environment is provided by Nix and a
+Makefile, though the project can be built with only Cabal.
 
-Steps to run the tests locally:
-  1. Dig through the [past releases] (https://www.elastic.co/downloads/past-releases) section of the Elasticsearch download page and install the desired Elasticsearch versions.
-  2. Install [Stack] (http://docs.haskellstack.org/en/stable/README.html#how-to-install)
-  3. In your local Bloodhound directory, run `stack setup && stack build`
-  4. Start the desired version of Elasticsearch at `localhost:9200`, which should be the default.
-  5. Run `stack test` in your local Bloodhound directory.
-  6. The unit tests will pass if you re-execute `stack test`. If you want to start with a clean slate, stop your Elasticsearch instance, delete the `data/` folder in the Elasticsearch installation, restart Elasticsearch, and re-run `stack test`.
+To run the tests:
+1. Get into the Nix environment by running `nix develop` (or `nix-shell` for a non-flake setup)
+1. Start Elasticsearch defined by `docker-compose.yml`: `make compose`
+1. Run the tests with Cabal: `cabal test`
+
+The second step can be left out if ElasticSearch (or OpenSearch) is started manually.
 
 Contributions
 -------------
diff --git a/bloodhound.cabal b/bloodhound.cabal
--- a/bloodhound.cabal
+++ b/bloodhound.cabal
@@ -1,7 +1,7 @@
 cabal-version:  3.4
 
 name:           bloodhound
-version:        0.23.0.0
+version:        0.23.0.1
 synopsis:       Elasticsearch client library for Haskell
 description:    Elasticsearch made awesome for Haskell hackers
 category:       Database, Search
@@ -16,7 +16,7 @@
 extra-doc-files:
     README.md
     changelog.md
-tested-with: GHC==9.6.2, GHC==9.4.5, GHC==9.2.7, GHC==9.0.2, GHC==8.10.7
+tested-with: GHC==8.10.7, GHC==9.0.2, GHC==9.2.8, GHC==9.4.8, GHC==9.6.6, GHC==9.8.4, GHC==9.10.1, GHC==9.12.1
 
 
 source-repository head
@@ -146,6 +146,8 @@
       TestsUtils.Generators
       TestsUtils.Import
       Paths_bloodhound
+  autogen-modules:
+      Paths_bloodhound
   hs-source-dirs:
       tests
   ghc-options: -Wall -fno-warn-orphans
@@ -199,6 +201,11 @@
   type:             exitcode-stdio-1.0
   hs-source-dirs:   tests
   main-is:          Doctests.hs
+  autogen-modules:
+      Paths_bloodhound
+  other-modules:
+      Paths_bloodhound
+  default-language: Haskell2010
   ghc-options:      -threaded
   build-depends: 
       aeson
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,14 @@
+0.23.0.1
+========
+- @arybczak
+  - add GHC 9.10/9.12 support
+- @supersvan
+  - chore: Improve Haddock of `isVersionConflict`
+  - chore: Add test to check response predicates
+- @blackheaven
+  - chore: migrate CI from `Haskell-CI` to `get-checked`
+  - fix: various bugfixes
+
 0.23.0.0
 ========
 - @blackheaven
diff --git a/src/Database/Bloodhound/Internal/Client/BHRequest.hs b/src/Database/Bloodhound/Internal/Client/BHRequest.hs
--- a/src/Database/Bloodhound/Internal/Client/BHRequest.hs
+++ b/src/Database/Bloodhound/Internal/Client/BHRequest.hs
@@ -304,7 +304,7 @@
 eitherDecodeResponse = eitherDecode . responseBody . getResponse
 
 -- | Was there an optimistic concurrency control conflict when
--- indexing a document?
+-- indexing a document? (Check '409' status code.)
 isVersionConflict :: BHResponse parsingContext a -> Bool
 isVersionConflict = statusCheck (== 409)
 
@@ -390,11 +390,26 @@
   mempty = EsError Nothing "Monoid value, shouldn't happen"
 
 instance FromJSON EsError where
-  parseJSON (Object v) =
-    EsError
-      <$> v
-        .:? "status"
-      <*> (v .: "error" <|> (v .: "error" >>= (.: "reason")))
+  parseJSON (Object v) = p1 <|> p2 <|> p3
+    where
+      p1 =
+        EsError
+          <$> v .:? "status"
+          <*> v .: "error"
+      p2 =
+        EsError
+          <$> v .:? "status"
+          <*> (v .: "error" >>= (.: "reason"))
+      p3 = do
+        failures <- v .: "failures"
+        -- This is a bit imprecise: We're only using the first error, ignoring
+        -- all others.
+        case failures of
+          (failure : _) ->
+            EsError
+              <$> failure .:? "status"
+              <*> (failure .: "cause" >>= (.: "reason"))
+          [] -> fail "could not find field `failure`"
   parseJSON _ = empty
 
 -- | 'EsProtocolException' will be thrown if Bloodhound cannot parse a response
diff --git a/src/Database/Bloodhound/Internal/Versions/Common/Types/Newtypes.hs b/src/Database/Bloodhound/Internal/Versions/Common/Types/Newtypes.hs
--- a/src/Database/Bloodhound/Internal/Versions/Common/Types/Newtypes.hs
+++ b/src/Database/Bloodhound/Internal/Versions/Common/Types/Newtypes.hs
@@ -324,9 +324,9 @@
   check "Is empty" $ not $ T.null name
   check "Is longer than 255 bytes" $ BS.length (T.encodeUtf8 name) < 256
   check "Contains uppercase letter(s)" $ T.all (\x -> not (isLetter x) || isLower x) name
-  check "Includes [\\/*?\"<>| ,#:]" $ T.all (flip @_ @String notElem "\\/*?\"<>| ,#:") name
+  check "Includes [\\/*?\"<>| ,#:]" $ T.all (flip notElem ("\\/*?\"<>| ,#:" :: String)) name
   check "Is (.|..)" $ notElem name [".", ".."]
-  check "Starts with [-_+.]" $ maybe False (flip @_ @String notElem "-_+." . fst) $ T.uncons name
+  check "Starts with [-_+.]" $ maybe False (flip notElem ("-_+." :: String) . fst) $ T.uncons name
   return $ IndexName name
 
 mkIndexNameSystem :: Text -> Either Text IndexName
@@ -335,8 +335,8 @@
   check "Is empty" $ not $ T.null name
   check "Is longer than 255 bytes" $ BS.length (T.encodeUtf8 name) < 256
   check "Contains uppercase letter(s)" $ T.all (\x -> not (isLetter x) || isLower x) name
-  check "Includes [\\/*?\"<>| ,#:]" $ T.all (flip @_ @String notElem "\\/*?\"<>| ,#:") name
-  check "Starts with [-_+]" $ maybe False (flip @_ @String notElem "-_+" . fst) $ T.uncons name
+  check "Includes [\\/*?\"<>| ,#:]" $ T.all (flip notElem ("\\/*?\"<>| ,#:" :: String)) name
+  check "Starts with [-_+]" $ maybe False (flip notElem ("-_+" :: String) . fst) $ T.uncons name
   return $ IndexName name
 
 qqIndexName :: QuasiQuoter
diff --git a/tests/Test/DocumentsSpec.hs b/tests/Test/DocumentsSpec.hs
--- a/tests/Test/DocumentsSpec.hs
+++ b/tests/Test/DocumentsSpec.hs
@@ -38,6 +38,21 @@
           Right (res', _) -> liftIO $ isVersionConflict res' `shouldBe` True
           Left e -> liftIO $ errorStatus e `shouldBe` Just 409
 
+    it "can use predicates on the response" $
+      withTestEnv $ do
+        let ev = ExternalDocVersion minBound
+        let cfg = defaultIndexDocumentSettings {idsVersionControl = ExternalGT ev}
+        resetIndex
+        (res, _) <- insertData' cfg
+        liftIO $ isCreated res `shouldBe` True
+        liftIO $ isSuccess res `shouldBe` True
+        liftIO $ isVersionConflict res `shouldBe` False
+
+        res' <- insertData'' cfg
+        liftIO $ isCreated res' `shouldBe` False
+        liftIO $ isSuccess res' `shouldBe` False
+        liftIO $ isVersionConflict res' `shouldBe` True
+
     it "indexes two documents in a parent/child relationship and checks that the child exists" $
       withTestEnv $ do
         resetIndex
diff --git a/tests/Test/JSONSpec.hs b/tests/Test/JSONSpec.hs
--- a/tests/Test/JSONSpec.hs
+++ b/tests/Test/JSONSpec.hs
@@ -81,6 +81,20 @@
           flagStrs = T.splitOn "|" str
        in noDuplicates flagStrs
 
+  describe "FromJSON EsError" $ do
+    it "should parse ElasticSearch 7 responses (status & error)" $ do
+      let mbJson = decode "{\"status\": 209, \"error\": \"Error message\"}"
+      mbJson `shouldBe` Just (EsError (Just 209) "Error message")
+
+    it "should parse ElasticSearch 7 responses (error.reason)" $ do
+      let mbJson = decode "{\"error\": {\"reason\": \"Error message\"}}"
+      mbJson `shouldBe` Just (EsError Nothing "Error message")
+
+    it "should parse OpenSearch 1.3 responses" $ do
+      -- This is a real error response for a version conflict
+      let mbJson = decode "{\"took\":9,\"timed_out\":false,\"total\":3,\"updated\":1,\"deleted\":0,\"batches\":1,\"version_conflicts\":2,\"noops\":0,\"retries\":{\"bulk\":0,\"search\":0},\"throttled_millis\":0,\"requests_per_second\":-1.0,\"throttled_until_millis\":0,\"failures\":[{\"index\":\"directory_test\",\"type\":\"_doc\",\"id\":\"9fda4188-2afd-490d-8796-e023df61a4e9\",\"cause\":{\"type\":\"version_conflict_engine_exception\",\"reason\":\"[9fda4188-2afd-490d-8796-e023df61a4e9]: version conflict, required seqNo [11], primary term [1]. current document has seqNo [16] and primary term [1]\",\"index\":\"directory_test\",\"shard\":\"0\",\"index_uuid\":\"Y3RpVY_DQEW9ULn8oGulrg\"},\"status\":409},{\"index\":\"directory_test\",\"type\":\"_doc\",\"id\":\"d70b631d-966a-4951-a94c-35ddc210f28a\",\"cause\":{\"type\":\"version_conflict_engine_exception\",\"reason\":\"[d70b631d-966a-4951-a94c-35ddc210f28a]: version conflict, required seqNo [13], primary term [1]. current document has seqNo [15] and primary term [1]\",\"index\":\"directory_test\",\"shard\":\"0\",\"index_uuid\":\"Y3RpVY_DQEW9ULn8oGulrg\"},\"status\":409}]}"
+      mbJson `shouldBe` Just (EsError (Just 409) "[9fda4188-2afd-490d-8796-e023df61a4e9]: version conflict, required seqNo [11], primary term [1]. current document has seqNo [16] and primary term [1]")
+
   describe "Exact isomorphism JSON instances" $ do
     propJSON (Proxy :: Proxy IndexName)
     propJSON (Proxy :: Proxy DocId)
diff --git a/tests/TestsUtils/Common.hs b/tests/TestsUtils/Common.hs
--- a/tests/TestsUtils/Common.hs
+++ b/tests/TestsUtils/Common.hs
@@ -205,6 +205,13 @@
   _ <- performBHRequest $ refreshIndex testIndex
   return r
 
+-- | Returns the `BHResponse` of `indexDocument` without any parsing
+insertData'' :: IndexDocumentSettings -> BH IO (BHResponse StatusDependant IndexedDocument)
+insertData'' ids = do
+  r <- dispatch $ indexDocument testIndex ids exampleTweet (DocId "1")
+  _ <- performBHRequest $ refreshIndex testIndex
+  return r
+
 insertTweetWithDocId :: Tweet -> Text -> BH IO IndexedDocument
 insertTweetWithDocId tweet docId = do
   let ids = defaultIndexDocumentSettings
