diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6a72c8cd690b50a99853d21c92da7dcc2d79d2a6..88810dcdb3ddc49fd0602c9e1108f875f039035c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -93,6 +93,9 @@ build-8.8:
       --out-link ghc \
       ghcHEAD
     - GHC=`pwd`/ghc/bin/ghc
+    - |
+      source ci/broken-packages.sh
+      EXTRA_OPTS="$EXTRA_OPTS $BROKEN_ARGS"
     - rm -Rf $HOME/.cabal/pacakages/local tmp; mkdir -p tmp; cd tmp
     - |
       EXTRA_OPTS="--cabal-option=-j$CPUS" # Use cabal's build parallelism
diff --git a/ci/TestPatches.hs b/ci/TestPatches.hs
index 7b4fefc38f66f27029dab6329ae940eadae27d6c..cdf8d0e54e2892d9b8a357a9bb7a4caa160bb025 100644
--- a/ci/TestPatches.hs
+++ b/ci/TestPatches.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE MultiWayIf #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
 module TestPatches
   ( testPatches
@@ -45,6 +46,12 @@ import Types
 import MakeConstraints
 import Utils
 
+newtype BrokenPackages = BrokenPackages { getBrokenPackageNames :: S.Set PkgName }
+  deriving (Semigroup, Monoid)
+
+failureExpected :: BrokenPackages -> PkgName -> Bool
+failureExpected (BrokenPackages pkgs) name = name `S.member` pkgs
+
 data Config = Config { configPatchDir :: FilePath
                      , configCompiler :: FilePath
                      , configGhcOptions :: [String]
@@ -52,6 +59,7 @@ data Config = Config { configPatchDir :: FilePath
                      , configOnlyPackages :: Maybe (S.Set Cabal.PackageName)
                      , configConcurrency :: Int
                      , configExtraCabalFragments :: [FilePath]
+                     , configExpectedBrokenPkgs :: BrokenPackages
                      }
 
 cabalOptions :: Config -> [String]
@@ -70,6 +78,7 @@ config =
     <*> onlyPackages
     <*> concurrency
     <*> extraCabalFragments
+    <*> expectedBrokenPkgs
   where
     patchDir = option str (short 'p' <> long "patches" <> help "patch directory" <> value "./patches")
     compiler = option str (short 'w' <> long "with-compiler" <> help "path of compiler")
@@ -80,6 +89,11 @@ config =
       <|> pure Nothing
     concurrency = option auto (short 'j' <> long "concurrency" <> value 1 <> help "number of concurrent builds")
     extraCabalFragments = many $ option str (long "extra-cabal-fragment" <> help "path of extra configuration to include in cabal project files")
+    expectedBrokenPkgs =
+      fmap (BrokenPackages . S.fromList) $ many
+      $ option
+          (fmap toPkgName pkgName)
+          (short 'b' <> long "expect-broken" <> metavar "PKGNAME" <> help "expect the given package to fail to build")
 
     pkgName :: ReadM Cabal.PackageName
     pkgName = str >>= maybe (fail "invalid package name") pure . simpleParse
@@ -106,14 +120,17 @@ testPatches cfg = do
   testedPatches <- fold <$> mapConcurrentlyN (fromIntegral $ configConcurrency cfg) build (S.toList packages')
   let runResult = RunResult testedPatches
 
-  print $ resultSummary runResult
+  print $ resultSummary (configExpectedBrokenPkgs cfg) runResult
   BSL.writeFile "results.json" $ encode runResult
-  exitWith $ if anyFailures runResult then ExitFailure 1 else ExitSuccess
+  exitWith $ if anyFailures (configExpectedBrokenPkgs cfg) runResult then ExitFailure 1 else ExitSuccess
 
-anyFailures :: RunResult -> Bool
-anyFailures (RunResult testedPatches) =
-    any patchFailed testedPatches
+anyFailures :: BrokenPackages -> RunResult -> Bool
+anyFailures broken (RunResult testedPatches) =
+    any failed testedPatches
   where
+    failed tp =
+      patchFailed tp /= failureExpected broken (patchedPackageName tp)
+
     patchFailed :: TestedPatch -> Bool
     patchFailed tp =
       case patchedPackageResult tp of
@@ -121,17 +138,21 @@ anyFailures (RunResult testedPatches) =
         PackageResult False _ -> True
         _ -> False
 
-resultSummary :: RunResult -> Doc
-resultSummary runResult = vcat
+resultSummary :: BrokenPackages -> RunResult -> Doc
+resultSummary broken runResult = vcat
   [ "Total units built:" <+> pshow (length allUnits)
   , ""
   , pshow (length planningErrors) <+> "had no valid install plan:"
   , PP.indent 4 $ vcat $ map (uncurry prettyPkgVer) planningErrors
   , ""
   , pshow (length failedUnits) <+> "units failed to build:"
-  , PP.indent 4 $ vcat [ prettyPkgVer (pkgName binfo) (version binfo)
-                     | (binfo, _) <- M.elems failedUnits
-                     ]
+  , PP.indent 4 $ vcat
+    [ prettyPkgVer (pkgName binfo) (version binfo) <+> expectedDoc
+    | (binfo, _) <- M.elems failedUnits
+    , let expectedDoc
+            | failureExpected broken (pkgName binfo) = PP.parens $ PP.yellow $ PP.text "expected"
+            | otherwise = mempty
+    ]
   , ""
   , pshow (length failedDependsUnits) <+> "units failed to build due to unbuildable dependencies."
   ]
diff --git a/ci/broken-packages.sh b/ci/broken-packages.sh
new file mode 100644
index 0000000000000000000000000000000000000000..d8b46bd907b8bcfd7f10c8d5c89c533afc8bec1e
--- /dev/null
+++ b/ci/broken-packages.sh
@@ -0,0 +1,46 @@
+# vi: set filetype=sh
+
+# Packages expected not to build due to GHC bugs. This is `source`'d by the CI
+# script and the arguments in BROKEN_ARGS are added to the hackage-ci
+# command-line.
+
+# Mark the named package as broken.
+#
+# Usage:
+#    broken $pkg_name $ghc_ticket_number
+#
+function broken() {
+  pkg_name="$1"
+  ticket="$2"
+  echo "Marking $pkg_name as broken due to #$ticket"
+  BROKEN_ARGS="$BROKEN_ARGS --expect-broken=$pkg_name"
+}
+
+if [ -z "$GHC" ]; then GHC=ghc; fi
+
+function ghc_version() {
+  $GHC --version | sed 's/.*version \([0-9]*\.\([0-9]*\.\)*\)/\1/'
+}
+
+# ======================================================================
+# The lists begin here
+#
+# For instance:
+#
+#    broken "lens" 17988
+
+version="$(ghc_version)"
+case $version in
+  8.8.*)
+    ;;
+
+  8.9.*)
+    #       package             ticket
+    broken "singletons"         17405
+    broken "vinyl"              17405
+    ;;
+
+  *)
+    echo "No broken packages for GHC $version"
+    ;;
+esac