diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.12.1 - 2021-03-20
+
+- Use HLint-3.3
+- GHA: Support --distribution and --submodules (thanks to Ryan Scott)
+
 ## 0.12 - 2021-03-13
 
 - Use `cabal-install-3.4`
diff --git a/fixtures/all-versions.github b/fixtures/all-versions.github
--- a/fixtures/all-versions.github
+++ b/fixtures/all-versions.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
@@ -128,7 +128,7 @@
           apt-add-repository -y 'ppa:hvr/ghc'
           if [ $((GHCJSARITH)) -ne 0 ] ; then apt-add-repository -y 'ppa:hvr/ghcjs' ; fi
           if [ $((GHCJSARITH)) -ne 0 ] ; then curl -sSL "https://deb.nodesource.com/gpgkey/nodesource.gpg.key" | apt-key add - ; fi
-          if [ $((GHCJSARITH)) -ne 0 ] ; then apt-add-repository -y 'deb https://deb.nodesource.com/node_10.x bionic main' ; fi
+          if [ $((GHCJSARITH)) -ne 0 ] ; then apt-add-repository -y 'deb https://deb.nodesource.com/node_10.x xenial main' ; fi
           apt-get update
           if [ $((GHCJSARITH)) -ne 0 ] ; then apt-get install -y $CC cabal-install-3.4 ghc-8.4.4 nodejs ; else apt-get install -y $CC cabal-install-3.4 ; fi
         env:
diff --git a/fixtures/copy-fields-all.github b/fixtures/copy-fields-all.github
--- a/fixtures/copy-fields-all.github
+++ b/fixtures/copy-fields-all.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/fixtures/copy-fields-none.github b/fixtures/copy-fields-none.github
--- a/fixtures/copy-fields-none.github
+++ b/fixtures/copy-fields-none.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/fixtures/copy-fields-some.github b/fixtures/copy-fields-some.github
--- a/fixtures/copy-fields-some.github
+++ b/fixtures/copy-fields-some.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/fixtures/empty-line.github b/fixtures/empty-line.github
--- a/fixtures/empty-line.github
+++ b/fixtures/empty-line.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/fixtures/irc-channels.github b/fixtures/irc-channels.github
--- a/fixtures/irc-channels.github
+++ b/fixtures/irc-channels.github
@@ -46,7 +46,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/fixtures/messy.github b/fixtures/messy.github
--- a/fixtures/messy.github
+++ b/fixtures/messy.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/fixtures/psql.github b/fixtures/psql.github
--- a/fixtures/psql.github
+++ b/fixtures/psql.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     services:
       postgres:
         image: postgres:10
diff --git a/fixtures/travis-patch.github b/fixtures/travis-patch.github
--- a/fixtures/travis-patch.github
+++ b/fixtures/travis-patch.github
@@ -21,7 +21,7 @@
     name: Haskell-CI - Linux - ${{ matrix.compiler }}
     runs-on: ubuntu-18.04
     container:
-      image: buildpack-deps:bionic
+      image: buildpack-deps:xenial
     continue-on-error: ${{ matrix.allow-failure }}
     strategy:
       matrix:
diff --git a/haskell-ci.cabal b/haskell-ci.cabal
--- a/haskell-ci.cabal
+++ b/haskell-ci.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               haskell-ci
-version:            0.12
+version:            0.12.1
 synopsis:           Cabal package script generator for Travis-CI
 description:
   Script generator (@haskell-ci@) for
@@ -88,7 +88,9 @@
     HaskellCI.Config.Jobs
     HaskellCI.Config.PackageScope
     HaskellCI.Config.Ubuntu
+    HaskellCI.Config.Validity
     HaskellCI.Diagnostics
+    HaskellCI.Error
     HaskellCI.GitConfig
     HaskellCI.GitHub
     HaskellCI.GitHub.Yaml
@@ -150,7 +152,7 @@
 
   -- other dependencies
   build-depends:
-    , aeson                          ^>=1.4.2.0 || ^>=1.5.0.0
+    , aeson                          ^>=1.5.6.0
     , attoparsec                     ^>=0.13.2.4
     , base-compat                    ^>=0.11
     , base16-bytestring              ^>=1.0.1.0
@@ -170,8 +172,8 @@
     , zinza                          ^>=0.2
 
   -- ShellCheck. Would need newer transformers for older GHC
-  if (flag(shellcheck) && impl(ghc >=7.10 && <8.11))
-    build-depends: ShellCheck ==0.7.0 || ==0.7.1
+  if flag(shellcheck)
+    build-depends: ShellCheck ==0.7.1
 
 executable haskell-ci
   main-is:          Main.hs
@@ -203,5 +205,5 @@
   build-depends:
     , ansi-terminal  >=0.10    && <0.12
     , Diff           ^>=0.4.0
-    , tasty          >=1.0     && <1.5
+    , tasty          ^>=1.4.1
     , tasty-golden   ^>=2.3.1.1
diff --git a/src/HaskellCI/Bash.hs b/src/HaskellCI/Bash.hs
--- a/src/HaskellCI/Bash.hs
+++ b/src/HaskellCI/Bash.hs
@@ -22,6 +22,7 @@
 import HaskellCI.Config.Doctest
 import HaskellCI.Config.Installed
 import HaskellCI.Config.PackageScope
+import HaskellCI.Config.Validity
 import HaskellCI.Jobs
 import HaskellCI.List
 import HaskellCI.Package
@@ -41,8 +42,11 @@
     -> Config
     -> Project URI Void Package
     -> JobVersions
-    -> Either ShError Z -- TODO: writer
+    -> Either HsCiError Z -- TODO: writer
 makeBash _argv config@Config {..} prj jobs@JobVersions {..} = do
+    -- Validity checks
+    checkConfigValidity config jobs
+
     blocks <- traverse (fmap shsToList) $ buildList $ do
         -- install doctest
         when doctestEnabled $ step "install doctest" $ do
@@ -238,7 +242,7 @@
     Auxiliary {..} = auxiliary config prj jobs
 
     -- TODO: this can be smart and do nothing is there are only comments in [Sh]
-    step :: String -> ShM () -> ListBuilder (Either ShError [Sh]) ()
+    step :: String -> ShM () -> ListBuilder (Either HsCiError [Sh]) ()
     step name action = item $ runSh $ do
         comment name
         put_info name
diff --git a/src/HaskellCI/Config/HLint.hs b/src/HaskellCI/Config/HLint.hs
--- a/src/HaskellCI/Config/HLint.hs
+++ b/src/HaskellCI/Config/HLint.hs
@@ -31,7 +31,7 @@
 -------------------------------------------------------------------------------
 
 defaultHLintVersion :: VersionRange
-defaultHLintVersion = withinVersion (mkVersion [3,2])
+defaultHLintVersion = withinVersion (mkVersion [3,3])
 
 defaultHLintConfig :: HLintConfig
 defaultHLintConfig = HLintConfig
diff --git a/src/HaskellCI/Config/Validity.hs b/src/HaskellCI/Config/Validity.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskellCI/Config/Validity.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE RecordWildCards  #-}
+module HaskellCI.Config.Validity where
+
+import HaskellCI.Prelude
+
+import HaskellCI.Compiler
+import HaskellCI.Config
+import HaskellCI.Config.Ubuntu
+import HaskellCI.Error
+import HaskellCI.Jobs
+import HaskellCI.MonadErr
+
+-- Validity checks shared in common among all backends.
+checkConfigValidity :: MonadErr HsCiError m => Config -> JobVersions -> m ()
+checkConfigValidity Config {..} JobVersions {..} = do
+    when (anyGHCJS && cfgUbuntu > Bionic) $
+        throwErr $ ValidationError $ "Using GHCJS requires Ubuntu 16.04 (Xenial) or 18.04 (Bionic)."
+  where
+    anyGHCJS = any isGHCJS versions
diff --git a/src/HaskellCI/Error.hs b/src/HaskellCI/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskellCI/Error.hs
@@ -0,0 +1,23 @@
+module HaskellCI.Error (
+    HsCiError (..),
+    FromHsCiError (..),
+) where
+
+import HaskellCI.Prelude
+
+data HsCiError
+    = ShellCheckError String  -- ^ @ShellCheck@ disagrees.
+    | ValidationError String  -- ^ used by validations
+    | FailError String        -- ^ made by 'fail'.
+  deriving (Show)
+
+instance Exception HsCiError where
+    displayException (ShellCheckError s) = s
+    displayException (ValidationError s) = s
+    displayException (FailError s)         = "PANIC " ++ s
+
+class FromHsCiError e where
+    fromHsCiError :: HsCiError -> e
+
+instance FromHsCiError HsCiError where
+    fromHsCiError = id
diff --git a/src/HaskellCI/GitHub.hs b/src/HaskellCI/GitHub.hs
--- a/src/HaskellCI/GitHub.hs
+++ b/src/HaskellCI/GitHub.hs
@@ -35,11 +35,14 @@
 import HaskellCI.Config.HLint
 import HaskellCI.Config.Installed
 import HaskellCI.Config.PackageScope
+import HaskellCI.Config.Ubuntu
+import HaskellCI.Config.Validity
 import HaskellCI.GitConfig
 import HaskellCI.GitHub.Yaml
 import HaskellCI.HeadHackage
 import HaskellCI.Jobs
 import HaskellCI.List
+import HaskellCI.MonadErr
 import HaskellCI.Package
 import HaskellCI.Sh
 import HaskellCI.ShVersionRange
@@ -92,12 +95,20 @@
     -> GitConfig
     -> Project URI Void Package
     -> JobVersions
-    -> Either ShError GitHub
+    -> Either HsCiError GitHub
 makeGitHub _argv config@Config {..} gitconfig prj jobs@JobVersions {..} = do
     let envEnv = Map.fromList
             [ ("CC", "${{ matrix.compiler }}")
             ]
 
+    -- Validity checks
+    checkConfigValidity config jobs
+    when (cfgSubmodules && cfgUbuntu < Focal) $
+        throwErr $ ValidationError $ unwords
+            [ "Using submodules on the GitHub Actions backend requires"
+            , "Ubuntu 20.04 (Focal Fossa) or later."
+            ]
+
     steps <- sequence $ buildList $ do
         -- This have to be first, since the packages we install depend on
         -- whether we need GHCJS or not.
@@ -118,7 +129,7 @@
             when anyGHCJS $ do
                 sh_if RangeGHCJS "apt-add-repository -y 'ppa:hvr/ghcjs'"
                 sh_if RangeGHCJS "curl -sSL \"https://deb.nodesource.com/gpgkey/nodesource.gpg.key\" | apt-key add -"
-                sh_if RangeGHCJS "apt-add-repository -y 'deb https://deb.nodesource.com/node_10.x bionic main'" -- TODO: Use cfgUbuntu
+                sh_if RangeGHCJS $ "apt-add-repository -y 'deb https://deb.nodesource.com/node_10.x " ++ ubuntuVer ++ " main'"
             sh "apt-get update"
             let basePackages  = ["$CC", "cabal-install-" ++ cabalVer] ++ S.toList cfgApt
                 ghcjsPackages = ["ghc-8.4.4", "nodejs"]
@@ -281,11 +292,12 @@
                 forHLint $ "$CABAL --store-dir=$HOME/.haskell-ci-tools/store v2-install $ARG_COMPILER --ignore-project -j2 hlint" ++ hlintVersionConstraint
                 forHLint "hlint --version"
 
-        githubUses "checkout" "actions/checkout@v2"
-            [ ("path", "source")
-            ]
+        githubUses "checkout" "actions/checkout@v2" $ buildList $ do
+            item ("path", "source")
+            when cfgSubmodules $
+                item ("submodules", "true")
 
-        githubRun "initial cabal.project for sdist" $do
+        githubRun "initial cabal.project for sdist" $ do
             sh "touch cabal.project"
             for_ pkgs $ \pkg ->
                 echo_if_to (RangePoints $ pkgJobs pkg) "cabal.project" $ "packages: $GITHUB_WORKSPACE/source/" ++ pkgDir pkg
@@ -515,11 +527,13 @@
         , ghJobs = Map.fromList $ buildList $ do
             item (mainJobName, GitHubJob
                 { ghjName            = actionName ++ " - Linux - ${{ matrix.compiler }}"
-                , ghjRunsOn          = "ubuntu-18.04" -- TODO: use cfgUbuntu
+                  -- NB: The Ubuntu version used in `runs-on` isn't
+                  -- particularly important since we use a Docker container.
+                , ghjRunsOn          = "ubuntu-18.04"
                 , ghjNeeds           = []
                 , ghjSteps           = steps
                 , ghjIf              = Nothing
-                , ghjContainer       = Just "buildpack-deps:bionic" -- use cfgUbuntu?
+                , ghjContainer       = Just $ "buildpack-deps:" ++ ubuntuVer
                 , ghjContinueOnError = Just "${{ matrix.allow-failure }}"
                 , ghjServices        = mconcat
                     [ Map.singleton "postgres" postgresService | cfgPostgres ]
@@ -543,6 +557,7 @@
     mainJobName = "linux"
 
     cabalVer = dispCabalVersion cfgCabalInstallVersion
+    ubuntuVer = showUbuntu cfgUbuntu
 
     Auxiliary {..} = auxiliary config prj jobs
 
@@ -553,16 +568,15 @@
     headGhcVers = S.filter (previewGHC cfgHeadHackage) versions
 
     -- step primitives
-    githubRun' :: String -> Map.Map String String ->  ShM () -> ListBuilder (Either ShError GitHubStep) ()
+    githubRun' :: String -> Map.Map String String ->  ShM () -> ListBuilder (Either HsCiError GitHubStep) ()
     githubRun' name env shm = item $ do
         shs <- runSh shm
         return $ GitHubStep name $ Left $ GitHubRun shs env
 
-    githubRun :: String -> ShM () -> ListBuilder (Either ShError GitHubStep) ()
+    githubRun :: String -> ShM () -> ListBuilder (Either HsCiError GitHubStep) ()
     githubRun name = githubRun' name mempty
 
-
-    githubUses :: String -> String -> [(String, String)] -> ListBuilder (Either ShError GitHubStep) ()
+    githubUses :: String -> String -> [(String, String)] -> ListBuilder (Either HsCiError GitHubStep) ()
     githubUses name action with = item $ return $
         GitHubStep name $ Right $ GitHubUses action Nothing (Map.fromList with)
 
diff --git a/src/HaskellCI/Sh.hs b/src/HaskellCI/Sh.hs
--- a/src/HaskellCI/Sh.hs
+++ b/src/HaskellCI/Sh.hs
@@ -8,17 +8,18 @@
     sh,
     ShM (..),
     runSh,
-    ShError (..),
-    FromShError (..),
+    HsCiError (..),
+    FromHsCiError (..),
     ) where
 
 import HaskellCI.Prelude
 
 #ifdef MIN_VERSION_ShellCheck
-import           ShellCheck.Checker    (checkScript)
-import qualified ShellCheck.Interface  as SC
+import           ShellCheck.Checker   (checkScript)
+import qualified ShellCheck.Interface as SC
 #endif
 
+import HaskellCI.Error
 import HaskellCI.MonadErr
 
 -------------------------------------------------------------------------------
@@ -61,34 +62,15 @@
     ]
 
 -------------------------------------------------------------------------------
--- Errors
--------------------------------------------------------------------------------
-
-data ShError
-    = ShellCheckError String  -- ^ @ShellCheck@ disagrees.
-    | ShError String          -- ^ made by 'fail'.
-  deriving (Show)
-
-instance Exception ShError where
-    displayException (ShellCheckError s) = s
-    displayException (ShError s)         = "PANIC " ++ s
-
-class FromShError e where
-    fromShError :: ShError -> e
-
-instance FromShError ShError where
-    fromShError = id
-
--------------------------------------------------------------------------------
 -- implementation
 -------------------------------------------------------------------------------
 
-newtype ShM a = ShM { unShM :: ([Sh] -> [Sh]) -> Either ShError ([Sh] -> [Sh], a) }
+newtype ShM a = ShM { unShM :: ([Sh] -> [Sh]) -> Either HsCiError ([Sh] -> [Sh], a) }
   deriving (Functor)
 
-runSh :: (MonadErr e m, FromShError e) => ShM () -> m [Sh]
+runSh :: (MonadErr e m, FromHsCiError e) => ShM () -> m [Sh]
 runSh (ShM f) = case f id of
-    Left err      -> throwErr (fromShError err)
+    Left err      -> throwErr (fromHsCiError err)
     Right (g, ()) -> return (g [])
 
 instance Applicative ShM where
@@ -103,7 +85,7 @@
         (shs2, y) <- unShM (k x) shs1
         return (shs2, y)
 
-instance MonadErr ShError ShM where
+instance MonadErr HsCiError ShM where
     throwErr err = ShM $ \_ -> Left err
 
 unsafeSh :: String -> ShM ()
diff --git a/src/HaskellCI/Travis.hs b/src/HaskellCI/Travis.hs
--- a/src/HaskellCI/Travis.hs
+++ b/src/HaskellCI/Travis.hs
@@ -29,6 +29,7 @@
 import HaskellCI.Config.Installed
 import HaskellCI.Config.Jobs
 import HaskellCI.Config.PackageScope
+import HaskellCI.Config.Validity
 import HaskellCI.HeadHackage
 import HaskellCI.Jobs
 import HaskellCI.List
@@ -78,7 +79,7 @@
     -> Config
     -> Project URI Void Package
     -> JobVersions
-    -> Either ShError Travis -- TODO: writer
+    -> Either HsCiError Travis -- TODO: writer
 makeTravis argv config@Config {..} prj jobs@JobVersions {..} = do
     -- before caching: clear some redundant stuff
     beforeCache <- runSh $ when cfgCache $ do
@@ -93,6 +94,9 @@
 
     -- before install: we set up the environment, install GHC/cabal on OSX
     beforeInstall <- runSh $ do
+        -- Validity checks
+        checkConfigValidity config jobs
+
         -- This have to be first
         when anyGHCJS $ sh $ unlines
             [ "if echo $CC | grep -q ghcjs; then"
