summary refs log tree commit diff
path: root/maintainers/scripts/haskell
diff options
context:
space:
mode:
Diffstat (limited to 'maintainers/scripts/haskell')
-rw-r--r--maintainers/scripts/haskell/dependencies.nix10
-rwxr-xr-xmaintainers/scripts/haskell/hydra-report.hs490
-rw-r--r--maintainers/scripts/haskell/maintainer-handles.nix7
-rwxr-xr-xmaintainers/scripts/haskell/mark-broken.sh47
-rwxr-xr-xmaintainers/scripts/haskell/merge-and-open-pr.sh122
-rwxr-xr-xmaintainers/scripts/haskell/regenerate-hackage-packages.sh46
-rwxr-xr-xmaintainers/scripts/haskell/regenerate-transitive-broken-packages.sh15
-rw-r--r--maintainers/scripts/haskell/test-configurations.nix136
-rw-r--r--maintainers/scripts/haskell/transitive-broken-packages.nix16
-rwxr-xr-xmaintainers/scripts/haskell/update-cabal2nix-unstable.sh17
-rwxr-xr-xmaintainers/scripts/haskell/update-hackage.sh35
-rwxr-xr-xmaintainers/scripts/haskell/update-stackage.sh57
-rwxr-xr-xmaintainers/scripts/haskell/upload-nixos-package-list-to-hackage.sh22
13 files changed, 1020 insertions, 0 deletions
diff --git a/maintainers/scripts/haskell/dependencies.nix b/maintainers/scripts/haskell/dependencies.nix
new file mode 100644
index 00000000000..f0620902c0e
--- /dev/null
+++ b/maintainers/scripts/haskell/dependencies.nix
@@ -0,0 +1,10 @@
+# Nix script to calculate the Haskell dependencies of every haskellPackage. Used by ./hydra-report.hs.
+let
+  pkgs = import ../../.. {};
+  inherit (pkgs) lib;
+  getDeps = _: pkg: {
+    deps = builtins.filter (x: !isNull x) (map (x: x.pname or null) (pkg.propagatedBuildInputs or []));
+    broken = (pkg.meta.hydraPlatforms or [null]) == [];
+  };
+in
+  lib.mapAttrs getDeps pkgs.haskellPackages
diff --git a/maintainers/scripts/haskell/hydra-report.hs b/maintainers/scripts/haskell/hydra-report.hs
new file mode 100755
index 00000000000..360b9f2058d
--- /dev/null
+++ b/maintainers/scripts/haskell/hydra-report.hs
@@ -0,0 +1,490 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p "haskellPackages.ghcWithPackages (p: [p.aeson p.req])"
+#! nix-shell -p hydra-unstable
+#! nix-shell -i runhaskell
+
+{-
+
+The purpose of this script is
+
+1) download the state of the nixpkgs/haskell-updates job from hydra (with get-report)
+2) print a summary of the state suitable for pasting into a github comment (with ping-maintainers)
+3) print a list of broken packages suitable for pasting into configuration-hackage2nix.yaml
+
+Because step 1) is quite expensive and takes roughly ~5 minutes the result is cached in a json file in XDG_CACHE.
+
+-}
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+{-# OPTIONS_GHC -Wall #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE TupleSections #-}
+
+import Control.Monad (forM_, (<=<))
+import Control.Monad.Trans (MonadIO (liftIO))
+import Data.Aeson (
+   FromJSON,
+   ToJSON,
+   decodeFileStrict',
+   eitherDecodeStrict',
+   encodeFile,
+ )
+import Data.Foldable (Foldable (toList), foldl')
+import Data.List.NonEmpty (NonEmpty, nonEmpty)
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (fromMaybe, mapMaybe, isNothing)
+import Data.Monoid (Sum (Sum, getSum))
+import Data.Sequence (Seq)
+import qualified Data.Sequence as Seq
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Data.Text.Encoding (encodeUtf8)
+import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)
+import Data.Time.Clock (UTCTime)
+import GHC.Generics (Generic)
+import Network.HTTP.Req (
+   GET (GET),
+   NoReqBody (NoReqBody),
+   defaultHttpConfig,
+   header,
+   https,
+   jsonResponse,
+   req,
+   responseBody,
+   responseTimeout,
+   runReq,
+   (/:),
+ )
+import System.Directory (XdgDirectory (XdgCache), getXdgDirectory)
+import System.Environment (getArgs)
+import System.Process (readProcess)
+import Prelude hiding (id)
+import Data.List (sortOn)
+import Control.Concurrent.Async (concurrently)
+import Control.Exception (evaluate)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import Data.Bifunctor (second)
+
+newtype JobsetEvals = JobsetEvals
+   { evals :: Seq Eval
+   }
+   deriving (Generic, ToJSON, FromJSON, Show)
+
+newtype Nixpkgs = Nixpkgs {revision :: Text}
+   deriving (Generic, ToJSON, FromJSON, Show)
+
+newtype JobsetEvalInputs = JobsetEvalInputs {nixpkgs :: Nixpkgs}
+   deriving (Generic, ToJSON, FromJSON, Show)
+
+data Eval = Eval
+   { id :: Int
+   , jobsetevalinputs :: JobsetEvalInputs
+   }
+   deriving (Generic, ToJSON, FromJSON, Show)
+
+data Build = Build
+   { job :: Text
+   , buildstatus :: Maybe Int
+   , finished :: Int
+   , id :: Int
+   , nixname :: Text
+   , system :: Text
+   , jobsetevals :: Seq Int
+   }
+   deriving (Generic, ToJSON, FromJSON, Show)
+
+main :: IO ()
+main = do
+   args <- getArgs
+   case args of
+      ["get-report"] -> getBuildReports
+      ["ping-maintainers"] -> printMaintainerPing
+      ["mark-broken-list"] -> printMarkBrokenList
+      _ -> putStrLn "Usage: get-report | ping-maintainers | mark-broken-list"
+
+reportFileName :: IO FilePath
+reportFileName = getXdgDirectory XdgCache "haskell-updates-build-report.json"
+
+showT :: Show a => a -> Text
+showT = Text.pack . show
+
+getBuildReports :: IO ()
+getBuildReports = runReq defaultHttpConfig do
+   evalMay <- Seq.lookup 0 . evals <$> myReq (https "hydra.nixos.org" /: "jobset" /: "nixpkgs" /: "haskell-updates" /: "evals") mempty
+   eval@Eval{id} <- maybe (liftIO $ fail "No Evalution found") pure evalMay
+   liftIO . putStrLn $ "Fetching evaluation " <> show id <> " from Hydra. This might take a few minutes..."
+   buildReports :: Seq Build <- myReq (https "hydra.nixos.org" /: "eval" /: showT id /: "builds") (responseTimeout 600000000)
+   liftIO do
+      fileName <- reportFileName
+      putStrLn $ "Finished fetching all builds from Hydra, saving report as " <> fileName
+      now <- getCurrentTime
+      encodeFile fileName (eval, now, buildReports)
+  where
+   myReq query option = responseBody <$> req GET query NoReqBody jsonResponse (header "User-Agent" "hydra-report.hs/v1 (nixpkgs;maintainers/scripts/haskell)" <> option)
+
+hydraEvalCommand :: FilePath
+hydraEvalCommand = "hydra-eval-jobs"
+
+hydraEvalParams :: [String]
+hydraEvalParams = ["-I", ".", "pkgs/top-level/release-haskell.nix"]
+
+nixExprCommand :: FilePath
+nixExprCommand = "nix-instantiate"
+
+nixExprParams :: [String]
+nixExprParams = ["--eval", "--strict", "--json"]
+
+-- | This newtype is used to parse a Hydra job output from @hydra-eval-jobs@.
+-- The only field we are interested in is @maintainers@, which is why this
+-- is just a newtype.
+--
+-- Note that there are occasionally jobs that don't have a maintainers
+-- field, which is why this has to be @Maybe Text@.
+newtype Maintainers = Maintainers { maintainers :: Maybe Text }
+  deriving stock (Generic, Show)
+  deriving anyclass (FromJSON, ToJSON)
+
+-- | This is a 'Map' from Hydra job name to maintainer email addresses.
+--
+-- It has values similar to the following:
+--
+-- @@
+--  fromList
+--    [ ("arion.aarch64-linux", Maintainers (Just "robert@example.com"))
+--    , ("bench.x86_64-linux", Maintainers (Just ""))
+--    , ("conduit.x86_64-linux", Maintainers (Just "snoy@man.com, web@ber.com"))
+--    , ("lens.x86_64-darwin", Maintainers (Just "ek@category.com"))
+--    ]
+-- @@
+--
+-- Note that Hydra jobs without maintainers will have an empty string for the
+-- maintainer list.
+type HydraJobs = Map Text Maintainers
+
+-- | Map of email addresses to GitHub handles.
+-- This is built from the file @../../maintainer-list.nix@.
+--
+-- It has values similar to the following:
+--
+-- @@
+--  fromList
+--    [ ("robert@example.com", "rob22")
+--    , ("ek@category.com", "edkm")
+--    ]
+-- @@
+type EmailToGitHubHandles = Map Text Text
+
+-- | Map of Hydra jobs to maintainer GitHub handles.
+--
+-- It has values similar to the following:
+--
+-- @@
+--  fromList
+--    [ ("arion.aarch64-linux", ["rob22"])
+--    , ("conduit.x86_64-darwin", ["snoyb", "webber"])
+--    ]
+-- @@
+type MaintainerMap = Map Text (NonEmpty Text)
+
+-- | Information about a package which lists its dependencies and whether the
+-- package is marked broken.
+data DepInfo = DepInfo {
+   deps :: Set Text,
+   broken :: Bool
+}
+   deriving stock (Generic, Show)
+   deriving anyclass (FromJSON, ToJSON)
+
+-- | Map from package names to their DepInfo. This is the data we get out of a
+-- nix call.
+type DependencyMap = Map Text DepInfo
+
+-- | Map from package names to its broken state, number of reverse dependencies (fst) and
+-- unbroken reverse dependencies (snd).
+type ReverseDependencyMap = Map Text (Int, Int)
+
+-- | Calculate the (unbroken) reverse dependencies of a package by transitively
+-- going through all packages if it’s a dependency of them.
+calculateReverseDependencies :: DependencyMap -> ReverseDependencyMap
+calculateReverseDependencies depMap = Map.fromDistinctAscList $ zip keys (zip (rdepMap False) (rdepMap True))
+ where
+    -- This code tries to efficiently invert the dependency map and calculate
+    -- it’s transitive closure by internally identifying every pkg with it’s index
+    -- in the package list and then using memoization.
+    keys = Map.keys depMap
+    pkgToIndexMap = Map.fromDistinctAscList (zip keys [0..])
+    intDeps = zip [0..] $ (\DepInfo{broken,deps} -> (broken,mapMaybe (`Map.lookup` pkgToIndexMap) $ Set.toList deps)) <$> Map.elems depMap
+    rdepMap onlyUnbroken = IntSet.size <$> resultList
+     where
+       resultList = go <$> [0..]
+       oneStepMap = IntMap.fromListWith IntSet.union $ (\(key,(_,deps)) -> (,IntSet.singleton key) <$> deps) <=< filter (\(_, (broken,_)) -> not (broken && onlyUnbroken)) $ intDeps
+       go pkg = IntSet.unions (oneStep:((resultList !!) <$> IntSet.toList oneStep))
+        where oneStep = IntMap.findWithDefault mempty pkg oneStepMap
+
+-- | Generate a mapping of Hydra job names to maintainer GitHub handles. Calls
+-- hydra-eval-jobs and the nix script ./maintainer-handles.nix.
+getMaintainerMap :: IO MaintainerMap
+getMaintainerMap = do
+   hydraJobs :: HydraJobs <-
+      readJSONProcess hydraEvalCommand hydraEvalParams "Failed to decode hydra-eval-jobs output: "
+   handlesMap :: EmailToGitHubHandles <-
+      readJSONProcess nixExprCommand ("maintainers/scripts/haskell/maintainer-handles.nix":nixExprParams) "Failed to decode nix output for lookup of github handles: "
+   pure $ Map.mapMaybe (splitMaintainersToGitHubHandles handlesMap) hydraJobs
+   where
+   -- Split a comma-spearated string of Maintainers into a NonEmpty list of
+   -- GitHub handles.
+   splitMaintainersToGitHubHandles
+      :: EmailToGitHubHandles -> Maintainers -> Maybe (NonEmpty Text)
+   splitMaintainersToGitHubHandles handlesMap (Maintainers maint) =
+      nonEmpty .  mapMaybe (`Map.lookup` handlesMap) .  Text.splitOn ", " $ fromMaybe "" maint
+
+-- | Get the a map of all dependencies of every package by calling the nix
+-- script ./dependencies.nix.
+getDependencyMap :: IO DependencyMap
+getDependencyMap =
+   readJSONProcess nixExprCommand ("maintainers/scripts/haskell/dependencies.nix":nixExprParams) "Failed to decode nix output for lookup of dependencies: "
+
+-- | Run a process that produces JSON on stdout and and decode the JSON to a
+-- data type.
+--
+-- If the JSON-decoding fails, throw the JSON-decoding error.
+readJSONProcess
+   :: FromJSON a
+   => FilePath -- ^ Filename of executable.
+   -> [String] -- ^ Arguments
+   -> String -- ^ String to prefix to JSON-decode error.
+   -> IO a
+readJSONProcess exe args err = do
+   output <- readProcess exe args ""
+   let eitherDecodedOutput = eitherDecodeStrict' . encodeUtf8 . Text.pack $ output
+   case eitherDecodedOutput of
+     Left decodeErr -> error $ err <> decodeErr <> "\nRaw: '" <> take 1000 output <> "'"
+     Right decodedOutput -> pure decodedOutput
+
+-- BuildStates are sorted by subjective importance/concerningness
+data BuildState
+  = Failed
+  | DependencyFailed
+  | OutputLimitExceeded
+  | Unknown (Maybe Int)
+  | TimedOut
+  | Canceled
+  | HydraFailure
+  | Unfinished
+  | Success
+  deriving stock (Show, Eq, Ord)
+
+icon :: BuildState -> Text
+icon = \case
+   Failed -> ":x:"
+   DependencyFailed -> ":heavy_exclamation_mark:"
+   OutputLimitExceeded -> ":warning:"
+   Unknown x -> "unknown code " <> showT x
+   TimedOut -> ":hourglass::no_entry_sign:"
+   Canceled -> ":no_entry_sign:"
+   Unfinished -> ":hourglass_flowing_sand:"
+   HydraFailure -> ":construction:"
+   Success -> ":heavy_check_mark:"
+
+platformIcon :: Platform -> Text
+platformIcon (Platform x) = case x of
+   "x86_64-linux" -> ":penguin:"
+   "aarch64-linux" -> ":iphone:"
+   "x86_64-darwin" -> ":apple:"
+   _ -> x
+
+data BuildResult = BuildResult {state :: BuildState, id :: Int} deriving (Show, Eq, Ord)
+newtype Platform = Platform {platform :: Text} deriving (Show, Eq, Ord)
+newtype Table row col a = Table (Map (row, col) a)
+data SummaryEntry = SummaryEntry {
+   summaryBuilds :: Table Text Platform BuildResult,
+   summaryMaintainers :: Set Text,
+   summaryReverseDeps :: Int,
+   summaryUnbrokenReverseDeps :: Int
+}
+type StatusSummary = Map Text SummaryEntry
+
+instance (Ord row, Ord col, Semigroup a) => Semigroup (Table row col a) where
+   Table l <> Table r = Table (Map.unionWith (<>) l r)
+instance (Ord row, Ord col, Semigroup a) => Monoid (Table row col a) where
+   mempty = Table Map.empty
+instance Functor (Table row col) where
+   fmap f (Table a) = Table (fmap f a)
+instance Foldable (Table row col) where
+   foldMap f (Table a) = foldMap f a
+
+buildSummary :: MaintainerMap -> ReverseDependencyMap -> Seq Build -> StatusSummary
+buildSummary maintainerMap reverseDependencyMap = foldl (Map.unionWith unionSummary) Map.empty . fmap toSummary
+  where
+   unionSummary (SummaryEntry (Table lb) lm lr lu) (SummaryEntry (Table rb) rm rr ru) = SummaryEntry (Table $ Map.union lb rb) (lm <> rm) (max lr rr) (max lu ru)
+   toSummary Build{finished, buildstatus, job, id, system} = Map.singleton name (SummaryEntry (Table (Map.singleton (set, Platform system) (BuildResult state id))) maintainers reverseDeps unbrokenReverseDeps)
+     where
+      state :: BuildState
+      state = case (finished, buildstatus) of
+         (0, _) -> Unfinished
+         (_, Just 0) -> Success
+         (_, Just 1) -> Failed
+         (_, Just 2) -> DependencyFailed
+         (_, Just 3) -> HydraFailure
+         (_, Just 4) -> Canceled
+         (_, Just 7) -> TimedOut
+         (_, Just 11) -> OutputLimitExceeded
+         (_, i) -> Unknown i
+      packageName = fromMaybe job (Text.stripSuffix ("." <> system) job)
+      splitted = nonEmpty $ Text.splitOn "." packageName
+      name = maybe packageName NonEmpty.last splitted
+      set = maybe "" (Text.intercalate "." . NonEmpty.init) splitted
+      maintainers = maybe mempty (Set.fromList . toList) (Map.lookup job maintainerMap)
+      (reverseDeps, unbrokenReverseDeps) = Map.findWithDefault (0,0) name reverseDependencyMap
+
+readBuildReports :: IO (Eval, UTCTime, Seq Build)
+readBuildReports = do
+   file <- reportFileName
+   fromMaybe (error $ "Could not decode " <> file) <$> decodeFileStrict' file
+
+sep :: Text
+sep = " | "
+joinTable :: [Text] -> Text
+joinTable t = sep <> Text.intercalate sep t <> sep
+
+type NumSummary = Table Platform BuildState Int
+
+printTable :: (Ord rows, Ord cols) => Text -> (rows -> Text) -> (cols -> Text) -> (entries -> Text) -> Table rows cols entries -> [Text]
+printTable name showR showC showE (Table mapping) = joinTable <$> (name : map showC cols) : replicate (length cols + sepsInName + 1) "---" : map printRow rows
+  where
+   sepsInName = Text.count "|" name
+   printRow row = showR row : map (\col -> maybe "" showE (Map.lookup (row, col) mapping)) cols
+   rows = toList $ Set.fromList (fst <$> Map.keys mapping)
+   cols = toList $ Set.fromList (snd <$> Map.keys mapping)
+
+printJob :: Int -> Text -> (Table Text Platform BuildResult, Text) -> [Text]
+printJob evalId name (Table mapping, maintainers) =
+   if length sets <= 1
+      then map printSingleRow sets
+      else ["- [ ] " <> makeJobSearchLink "" name <> " " <> maintainers] <> map printRow sets
+  where
+   printRow set = "  - " <> printState set <> " " <> makeJobSearchLink set (if Text.null set then "toplevel" else set)
+   printSingleRow set = "- [ ] " <> printState set <> " " <> makeJobSearchLink set (makePkgName set) <> " " <> maintainers
+   makePkgName set = (if Text.null set then "" else set <> ".") <> name
+   printState set = Text.intercalate " " $ map (\pf -> maybe "" (label pf) $ Map.lookup (set, pf) mapping) platforms
+   makeJobSearchLink set linkLabel= makeSearchLink evalId linkLabel (makePkgName set)
+   sets = toList $ Set.fromList (fst <$> Map.keys mapping)
+   platforms = toList $ Set.fromList (snd <$> Map.keys mapping)
+   label pf (BuildResult s i) = "[[" <> platformIcon pf <> icon s <> "]](https://hydra.nixos.org/build/" <> showT i <> ")"
+
+makeSearchLink :: Int -> Text -> Text -> Text
+makeSearchLink evalId linkLabel query = "[" <> linkLabel <> "](" <> "https://hydra.nixos.org/eval/" <> showT evalId <> "?filter=" <> query <> ")"
+
+statusToNumSummary :: StatusSummary -> NumSummary
+statusToNumSummary = fmap getSum . foldMap (fmap Sum . jobTotals)
+
+jobTotals :: SummaryEntry -> Table Platform BuildState Int
+jobTotals (summaryBuilds -> Table mapping) = getSum <$> Table (Map.foldMapWithKey (\(_, platform) (BuildResult buildstate _) -> Map.singleton (platform, buildstate) (Sum 1)) mapping)
+
+details :: Text -> [Text] -> [Text]
+details summary content = ["<details><summary>" <> summary <> " </summary>", ""] <> content <> ["</details>", ""]
+
+printBuildSummary :: Eval -> UTCTime -> StatusSummary -> [(Text, Int)] -> Text
+printBuildSummary
+   Eval{id, jobsetevalinputs = JobsetEvalInputs{nixpkgs = Nixpkgs{revision}}}
+   fetchTime
+   summary
+   topBrokenRdeps =
+      Text.unlines $
+         headline <> [""] <> tldr <> (("  * "<>) <$> (errors <> warnings)) <> [""]
+            <> totals
+            <> optionalList "#### Maintained packages with build failure" (maintainedList fails)
+            <> optionalList "#### Maintained packages with failed dependency" (maintainedList failedDeps)
+            <> optionalList "#### Maintained packages with unknown error" (maintainedList unknownErr)
+            <> optionalHideableList "#### Unmaintained packages with build failure" (unmaintainedList fails)
+            <> optionalHideableList "#### Unmaintained packages with failed dependency" (unmaintainedList failedDeps)
+            <> optionalHideableList "#### Unmaintained packages with unknown error" (unmaintainedList unknownErr)
+            <> optionalHideableList "#### Top 50 broken packages, sorted by number of reverse dependencies" (brokenLine <$> topBrokenRdeps)
+            <> ["","*:arrow_heading_up:: The number of packages that depend (directly or indirectly) on this package (if any). If two numbers are shown the first (lower) number considers only packages which currently have enabled hydra jobs, i.e. are not marked broken. The second (higher) number considers all packages.*",""]
+            <> footer
+     where
+      footer = ["*Report generated with [maintainers/scripts/haskell/hydra-report.hs](https://github.com/NixOS/nixpkgs/blob/haskell-updates/maintainers/scripts/haskell/hydra-report.sh)*"]
+      totals =
+         [ "#### Build summary"
+         , ""
+         ]
+            <> printTable "Platform" (\x -> makeSearchLink id (platform x <> " " <> platformIcon x) ("." <> platform x)) (\x -> showT x <> " " <> icon x) showT numSummary
+      headline =
+         [ "### [haskell-updates build report from hydra](https://hydra.nixos.org/jobset/nixpkgs/haskell-updates)"
+         , "*evaluation ["
+            <> showT id
+            <> "](https://hydra.nixos.org/eval/"
+            <> showT id
+            <> ") of nixpkgs commit ["
+            <> Text.take 7 revision
+            <> "](https://github.com/NixOS/nixpkgs/commits/"
+            <> revision
+            <> ") as of "
+            <> Text.pack (formatTime defaultTimeLocale "%Y-%m-%d %H:%M UTC" fetchTime)
+            <> "*"
+         ]
+      brokenLine (name, rdeps) = "[" <> name <> "](https://packdeps.haskellers.com/reverse/" <> name <> ") :arrow_heading_up: " <> Text.pack (show rdeps) <> "  "
+      numSummary = statusToNumSummary summary
+      jobsByState predicate = Map.filter (predicate . worstState) summary
+      worstState = foldl' min Success . fmap state . summaryBuilds
+      fails = jobsByState (== Failed)
+      failedDeps = jobsByState (== DependencyFailed)
+      unknownErr = jobsByState (\x -> x > DependencyFailed && x < TimedOut)
+      withMaintainer = Map.mapMaybe (\e -> (summaryBuilds e,) <$> nonEmpty (Set.toList (summaryMaintainers e)))
+      withoutMaintainer = Map.mapMaybe (\e -> if Set.null (summaryMaintainers e) then Just e else Nothing)
+      optionalList heading list = if null list then mempty else [heading] <> list
+      optionalHideableList heading list = if null list then mempty else [heading] <> details (showT (length list) <> " job(s)") list
+      maintainedList = showMaintainedBuild <=< Map.toList . withMaintainer
+      unmaintainedList = showBuild <=< sortOn (\(snd -> x) -> (negate (summaryUnbrokenReverseDeps x), negate (summaryReverseDeps x))) . Map.toList . withoutMaintainer
+      showBuild (name, entry) = printJob id name (summaryBuilds entry, Text.pack (if summaryReverseDeps entry > 0 then " :arrow_heading_up: " <> show (summaryUnbrokenReverseDeps entry) <>" | "<> show (summaryReverseDeps entry) else ""))
+      showMaintainedBuild (name, (table, maintainers)) = printJob id name (table, Text.intercalate " " (fmap ("@" <>) (toList maintainers)))
+      tldr = case (errors, warnings) of
+               ([],[]) -> [":green_circle: **Ready to merge**"]
+               ([],_) -> [":yellow_circle: **Potential issues**"]
+               _ -> [":red_circle: **Branch not mergeable**"]
+      warnings =
+         if' (Unfinished > maybe Success worstState maintainedJob) "`maintained` jobset failed." <>
+         if' (Unfinished == maybe Success worstState mergeableJob) "`mergeable` jobset is not finished." <>
+         if' (Unfinished == maybe Success worstState maintainedJob) "`maintained` jobset is not finished."
+      errors =
+         if' (isNothing mergeableJob) "No `mergeable` job found." <>
+         if' (isNothing maintainedJob) "No `maintained` job found." <>
+         if' (Unfinished > maybe Success worstState mergeableJob) "`mergeable` jobset failed." <>
+         if' (outstandingJobs (Platform "x86_64-linux") > 100) "Too many outstanding jobs on x86_64-linux." <>
+         if' (outstandingJobs (Platform "aarch64-linux") > 100) "Too many outstanding jobs on aarch64-linux."
+      if' p e = if p then [e] else mempty
+      outstandingJobs platform | Table m <- numSummary = Map.findWithDefault 0 (platform, Unfinished) m
+      maintainedJob = Map.lookup "maintained" summary
+      mergeableJob = Map.lookup "mergeable" summary
+
+printMaintainerPing :: IO ()
+printMaintainerPing = do
+   (maintainerMap, (reverseDependencyMap, topBrokenRdeps)) <- concurrently getMaintainerMap do
+      depMap <- getDependencyMap
+      rdepMap <- evaluate . calculateReverseDependencies $ depMap
+      let tops = take 50 . sortOn (negate . snd) . fmap (second fst) . filter (\x -> maybe False broken $ Map.lookup (fst x) depMap) . Map.toList $ rdepMap
+      pure (rdepMap, tops)
+   (eval, fetchTime, buildReport) <- readBuildReports
+   putStrLn (Text.unpack (printBuildSummary eval fetchTime (buildSummary maintainerMap reverseDependencyMap buildReport) topBrokenRdeps))
+
+printMarkBrokenList :: IO ()
+printMarkBrokenList = do
+   (_, _, buildReport) <- readBuildReports
+   forM_ buildReport \Build{buildstatus, job} ->
+      case (buildstatus, Text.splitOn "." job) of
+         (Just 1, ["haskellPackages", name, "x86_64-linux"]) -> putStrLn $ "  - " <> Text.unpack name
+         _ -> pure ()
diff --git a/maintainers/scripts/haskell/maintainer-handles.nix b/maintainers/scripts/haskell/maintainer-handles.nix
new file mode 100644
index 00000000000..08c6bc4c96a
--- /dev/null
+++ b/maintainers/scripts/haskell/maintainer-handles.nix
@@ -0,0 +1,7 @@
+# Nix script to lookup maintainer github handles from their email address. Used by ./hydra-report.hs.
+let
+  pkgs = import ../../.. {};
+  maintainers = import ../../maintainer-list.nix;
+  inherit (pkgs) lib;
+  mkMailGithubPair = _: maintainer: if maintainer ? github then { "${maintainer.email}" = maintainer.github; } else {};
+in lib.zipAttrsWith (_: builtins.head) (lib.mapAttrsToList mkMailGithubPair maintainers)
diff --git a/maintainers/scripts/haskell/mark-broken.sh b/maintainers/scripts/haskell/mark-broken.sh
new file mode 100755
index 00000000000..97dd5be8aaa
--- /dev/null
+++ b/maintainers/scripts/haskell/mark-broken.sh
@@ -0,0 +1,47 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p coreutils git -I nixpkgs=.
+
+# This script uses the data pulled with
+# maintainers/scripts/haskell/hydra-report.hs get-report to produce a list of
+# failing builds that get written to the hackage2nix config. Then
+# hackage-packages.nix gets regenerated and transitive-broken packages get
+# marked as dont-distribute in the config as well.
+# This should disable builds for most failing jobs in the haskell-updates jobset.
+
+set -euo pipefail
+
+broken_config="pkgs/development/haskell-modules/configuration-hackage2nix/broken.yaml"
+
+tmpfile=$(mktemp)
+trap "rm ${tmpfile}" 0
+
+echo "Remember that you need to manually run 'maintainers/scripts/haskell/hydra-report.hs get-report' sometime before running this script."
+echo "Generating a list of broken builds and displaying for manual confirmation ..."
+maintainers/scripts/haskell/hydra-report.hs mark-broken-list | sort -i > "$tmpfile"
+
+$EDITOR "$tmpfile"
+
+tail -n +3 "$broken_config" >> "$tmpfile"
+
+cat > "$broken_config" << EOF
+broken-packages:
+  # These packages don't compile.
+EOF
+
+# clear environment here to avoid things like allowing broken builds in
+sort -iu "$tmpfile" >> "$broken_config"
+clear="env -u HOME -u NIXPKGS_CONFIG"
+$clear maintainers/scripts/haskell/regenerate-hackage-packages.sh
+$clear maintainers/scripts/haskell/regenerate-transitive-broken-packages.sh
+$clear maintainers/scripts/haskell/regenerate-hackage-packages.sh
+
+if [[ "${1:-}" == "--do-commit" ]]; then
+git add $broken_config
+git add pkgs/development/haskell-modules/configuration-hackage2nix/transitive-broken.yaml
+git add pkgs/development/haskell-modules/hackage-packages.nix
+git commit -F - << EOF
+haskellPackages: mark builds failing on hydra as broken
+
+This commit has been generated by maintainers/scripts/haskell/mark-broken.sh
+EOF
+fi
diff --git a/maintainers/scripts/haskell/merge-and-open-pr.sh b/maintainers/scripts/haskell/merge-and-open-pr.sh
new file mode 100755
index 00000000000..18db1da0f2a
--- /dev/null
+++ b/maintainers/scripts/haskell/merge-and-open-pr.sh
@@ -0,0 +1,122 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p git gh -I nixpkgs=.
+#
+# Script to merge the currently open haskell-updates PR into master, bump the
+# Stackage version and Hackage versions, and open the next haskell-updates PR.
+
+set -eu -o pipefail
+
+# exit after printing first argument to this function
+function die {
+  # echo the first argument
+  echo "ERROR: $1"
+  echo "Aborting!"
+
+  exit 1
+}
+
+function help {
+  echo "Usage: $0 HASKELL_UPDATES_PR_NUM"
+  echo "Merge the currently open haskell-updates PR into master, and open the next one."
+  echo
+  echo "  -h, --help                print this help"
+  echo "  HASKELL_UPDATES_PR_NUM    number of the currently open PR on NixOS/nixpkgs"
+  echo "                            for the haskell-updates branch"
+  echo
+  echo "Example:"
+  echo "  \$ $0 137340"
+
+  exit 1
+}
+
+# Read in the current haskell-updates PR number from the command line.
+while [[ $# -gt 0 ]]; do
+  key="$1"
+
+  case $key in
+    -h|--help)
+      help
+      ;;
+    *)
+      curr_haskell_updates_pr_num="$1"
+      shift
+      ;;
+  esac
+done
+
+if [[ -z "${curr_haskell_updates_pr_num-}" ]] ; then
+  die "You must pass the current haskell-updates PR number as the first argument to this script."
+fi
+
+# Make sure you have gh authentication setup.
+if ! gh auth status 2>/dev/null ; then
+  die "You must setup the \`gh\` command.  Run \`gh auth login\`."
+fi
+
+# Fetch nixpkgs to get an up-to-date origin/haskell-updates branch.
+echo "Fetching origin..."
+git fetch origin >/dev/null
+
+# Make sure we are currently on a local haskell-updates branch.
+curr_branch="$(git rev-parse --abbrev-ref HEAD)"
+if [[ "$curr_branch" != "haskell-updates" ]]; then
+    die "Current branch is not called \"haskell-updates\"."
+fi
+
+# Make sure our local haskell-updates branch is on the same commit as
+# origin/haskell-updates.
+curr_branch_commit="$(git rev-parse haskell-updates)"
+origin_haskell_updates_commit="$(git rev-parse origin/haskell-updates)"
+if [[ "$curr_branch_commit" != "$origin_haskell_updates_commit" ]]; then
+    die "Current branch is not at the same commit as origin/haskell-updates"
+fi
+
+# Merge the current open haskell-updates PR.
+echo "Merging https://github.com/NixOS/nixpkgs/pull/${curr_haskell_updates_pr_num}..."
+gh pr merge --repo NixOS/nixpkgs --merge "$curr_haskell_updates_pr_num"
+
+# Update the list of Haskell package versions in NixOS on Hackage.
+echo "Updating list of Haskell package versions in NixOS on Hackage..."
+./maintainers/scripts/haskell/upload-nixos-package-list-to-hackage.sh
+
+# Update stackage, Hackage hashes, and regenerate Haskell package set
+echo "Updating Stackage..."
+./maintainers/scripts/haskell/update-stackage.sh --do-commit
+echo "Updating Hackage hashes..."
+./maintainers/scripts/haskell/update-hackage.sh --do-commit
+echo "Regenerating Hackage packages..."
+./maintainers/scripts/haskell/regenerate-hackage-packages.sh --do-commit
+
+# Push these new commits to the haskell-updates branch
+echo "Pushing commits just created to the remote haskell-updates branch..."
+git push
+
+# Open new PR
+new_pr_body=$(cat <<EOF
+### This Merge
+
+This PR is the regular merge of the \`haskell-updates\` branch into \`master\`.
+
+This branch is being continually built and tested by hydra at https://hydra.nixos.org/jobset/nixpkgs/haskell-updates. You may be able to find an up-to-date Hydra build report at [cdepillabout/nix-haskell-updates-status](https://github.com/cdepillabout/nix-haskell-updates-status).
+
+We roughly aim to merge these \`haskell-updates\` PRs at least once every two weeks. See the @NixOS/haskell [team calendar](https://cloud.maralorn.de/apps/calendar/p/Mw5WLnzsP7fC4Zky) for who is currently in charge of this branch.
+
+### haskellPackages Workflow Summary
+
+Our workflow is currently described in [\`pkgs/development/haskell-modules/HACKING.md\`](https://github.com/NixOS/nixpkgs/blob/haskell-updates/pkgs/development/haskell-modules/HACKING.md).
+
+The short version is this:
+* We regularly update the Stackage and Hackage pins on \`haskell-updates\` (normally at the beginning of a merge window).
+* The community fixes builds of Haskell packages on that branch.
+* We aim at at least one merge of \`haskell-updates\` into \`master\` every two weeks.
+* We only do the merge if the [\`mergeable\`](https://hydra.nixos.org/job/nixpkgs/haskell-updates/mergeable) job is succeeding on hydra.
+* If a [\`maintained\`](https://hydra.nixos.org/job/nixpkgs/haskell-updates/maintained) package is still broken at the time of merge, we will only merge if the maintainer has been pinged 7 days in advance. (If you care about a Haskell package, become a maintainer!)
+
+---
+
+This is the follow-up to #${curr_haskell_updates_pr_num}. Come to [#haskell:nixos.org](https://matrix.to/#/#haskell:nixos.org) if you have any questions.
+EOF
+)
+
+echo "Opening a PR for the next haskell-updates merge cycle..."
+gh pr create --repo NixOS/nixpkgs --base master --head haskell-updates --title "haskellPackages: update stackage and hackage" --body "$new_pr_body"
diff --git a/maintainers/scripts/haskell/regenerate-hackage-packages.sh b/maintainers/scripts/haskell/regenerate-hackage-packages.sh
new file mode 100755
index 00000000000..9d51eb4ca4a
--- /dev/null
+++ b/maintainers/scripts/haskell/regenerate-hackage-packages.sh
@@ -0,0 +1,46 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p coreutils haskellPackages.cabal2nix-unstable git nix -I nixpkgs=.
+
+# This script is used to regenerate nixpkgs' Haskell package set, using the
+# tool hackage2nix from the nixos/cabal2nix repo. hackage2nix looks at the
+# config files in pkgs/development/haskell-modules/configuration-hackage2nix
+# and generates a Nix expression for package version specified there, using the
+# Cabal files from the Hackage database (available under all-cabal-hashes) and
+# its companion tool cabal2nix.
+#
+# Related scripts are update-hackage.sh, for updating the snapshot of the
+# Hackage database used by hackage2nix, and update-cabal2nix-unstable.sh,
+# for updating the version of hackage2nix used to perform this task.
+
+set -euo pipefail
+
+HACKAGE2NIX="${HACKAGE2NIX:-hackage2nix}"
+
+# To prevent hackage2nix fails because of encoding.
+# See: https://github.com/NixOS/nixpkgs/pull/122023
+export LC_ALL=C.UTF-8
+
+extraction_derivation='with import ./. {}; runCommandLocal "unpacked-cabal-hashes" { } "tar xf ${all-cabal-hashes} --strip-components=1 --one-top-level=$out"'
+unpacked_hackage="$(nix-build -E "$extraction_derivation" --no-out-link)"
+config_dir=pkgs/development/haskell-modules/configuration-hackage2nix
+
+echo "Starting hackage2nix to regenerate pkgs/development/haskell-modules/hackage-packages.nix ..."
+"$HACKAGE2NIX" \
+   --hackage "$unpacked_hackage" \
+   --preferred-versions <(for n in "$unpacked_hackage"/*/preferred-versions; do cat "$n"; echo; done) \
+   --nixpkgs "$PWD" \
+   --config "$config_dir/main.yaml" \
+   --config "$config_dir/stackage.yaml" \
+   --config "$config_dir/broken.yaml" \
+   --config "$config_dir/transitive-broken.yaml"
+
+if [[ "${1:-}" == "--do-commit" ]]; then
+git add pkgs/development/haskell-modules/hackage-packages.nix
+git commit -F - << EOF
+haskellPackages: regenerate package set based on current config
+
+This commit has been generated by maintainers/scripts/haskell/regenerate-hackage-packages.sh
+EOF
+fi
+
+echo "Regeneration of hackage-packages.nix finished."
diff --git a/maintainers/scripts/haskell/regenerate-transitive-broken-packages.sh b/maintainers/scripts/haskell/regenerate-transitive-broken-packages.sh
new file mode 100755
index 00000000000..94104e00edb
--- /dev/null
+++ b/maintainers/scripts/haskell/regenerate-transitive-broken-packages.sh
@@ -0,0 +1,15 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p coreutils nix gnused -I nixpkgs=.
+
+config_file=pkgs/development/haskell-modules/configuration-hackage2nix/transitive-broken.yaml
+
+cat > $config_file << EOF
+# This file is automatically generated by
+# maintainers/scripts/haskell/regenerate-transitive-broken-packages.sh
+# It is supposed to list all haskellPackages that cannot evaluate because they
+# depend on a dependency marked as broken.
+dont-distribute-packages:
+EOF
+
+echo "Regenerating list of transitive broken packages ..."
+echo -e $(nix-instantiate --eval --strict maintainers/scripts/haskell/transitive-broken-packages.nix) | sed 's/\"//' | LC_ALL=C.UTF-8 sort -i >> $config_file
diff --git a/maintainers/scripts/haskell/test-configurations.nix b/maintainers/scripts/haskell/test-configurations.nix
new file mode 100644
index 00000000000..12287896b50
--- /dev/null
+++ b/maintainers/scripts/haskell/test-configurations.nix
@@ -0,0 +1,136 @@
+/* Nix expression to test for regressions in the Haskell configuration overlays.
+
+   test-configurations.nix determines all attributes touched by given Haskell
+   configuration overlays (i. e. pkgs/development/haskell-modules/configuration-*.nix)
+   and builds all derivations (or at least a reasonable subset) affected by
+   these overrides.
+
+   By default, it checks `configuration-{common,nix,ghc-8.10.x}.nix`. You can
+   invoke it like this:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix --keep-going
+
+   It is possible to specify other configurations:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix \
+       --arg files '[ "configuration-ghc-9.0.x.nix" "configuration-ghc-9.2.x.nix" ]' \
+       --keep-going
+
+   You can also just supply a single string:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix \
+       --argstr files "configuration-arm.nix" --keep-going
+
+   You can even supply full paths which is handy, as it allows for tab-completing
+   the configurations:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix \
+       --argstr files pkgs/development/haskell-modules/configuration-arm.nix \
+       --keep-going
+
+   By default, derivation that fail to evaluate are skipped, unless they are
+   “just” marked as broken. You can check for other eval errors like this:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix \
+       --arg skipEvalErrors false --keep-going
+
+   You can also disable checking broken packages by passing a nixpkgs config:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix \
+       --arg config '{ allowBroken = false; }' --keep-going
+
+   By default the haskell.packages.ghc*Binary sets used for bootstrapping GHC
+   are _not_ tested. You can change this using:
+
+     nix-build maintainers/scripts/haskell/test-configurations.nix \
+       --arg skipBinaryGHCs false --keep-going
+
+*/
+{ files ? [
+    "configuration-common.nix"
+    "configuration-nix.nix"
+    "configuration-ghc-8.10.x.nix"
+  ]
+, nixpkgsPath ? ../../..
+, config ? { allowBroken = true; }
+, skipEvalErrors ? true
+, skipBinaryGHCs ? true
+}:
+
+let
+  pkgs = import nixpkgsPath { inherit config; };
+  inherit (pkgs) lib;
+
+  # see usage explanation for the input format `files` allows
+  files' = builtins.map builtins.baseNameOf (
+    if !builtins.isList files then [ files ] else files
+  );
+
+  setsForFile = fileName:
+    let
+      # extract the unique part of the config's file name
+      configName = builtins.head (
+        builtins.match "configuration-(.+).nix" fileName
+      );
+      # match the major and minor version of the GHC the config is intended for, if any
+      configVersion = lib.concatStrings (
+        builtins.match "ghc-([0-9]+).([0-9]+).x" configName
+      );
+      # return all package sets under haskell.packages matching the version components
+      setsForVersion =  builtins.map (name: pkgs.haskell.packages.${name}) (
+        builtins.filter (setName:
+          lib.hasPrefix "ghc${configVersion}" setName
+          && (skipBinaryGHCs -> !(lib.hasInfix "Binary" setName))
+        ) (
+          builtins.attrNames pkgs.haskell.packages
+        )
+      );
+
+      defaultSets = [ pkgs.haskellPackages ];
+    in {
+      # use plain haskellPackages for the version-agnostic files
+      # TODO(@sternenseemann): also consider currently selected versioned sets
+      "common" = defaultSets;
+      "nix" = defaultSets;
+      "arm" = defaultSets;
+      "darwin" = defaultSets;
+    }.${configName} or setsForVersion;
+
+  # attribute set that has all the attributes of haskellPackages set to null
+  availableHaskellPackages = builtins.listToAttrs (
+    builtins.map (attr: lib.nameValuePair attr null) (
+      builtins.attrNames pkgs.haskellPackages
+    )
+  );
+
+  # evaluate a configuration and only return the attributes changed by it,
+  # pass availableHaskellPackages as super in case intersectAttrs is used
+  overriddenAttrs = fileName: builtins.attrNames (
+    lib.fix (self:
+      import (nixpkgsPath + "/pkgs/development/haskell-modules/${fileName}") {
+        haskellLib = pkgs.haskell.lib.compose;
+        inherit pkgs;
+      } self availableHaskellPackages
+    )
+  );
+
+  # list of derivations that are affected by overrides in the given configuration
+  # overlays. For common, nix, darwin etc. only the derivation from the default
+  # package set will be emitted.
+  packages = builtins.filter (v:
+    lib.warnIf (v.meta.broken or false) "${v.pname} is marked as broken" (
+      v != null
+      && (skipEvalErrors -> (builtins.tryEval (v.outPath or v)).success)
+    )
+  ) (
+    lib.concatMap (fileName:
+      let
+        sets = setsForFile fileName;
+        attrs = overriddenAttrs fileName;
+      in
+        lib.concatMap (set: builtins.map (attr: set.${attr}) attrs) sets
+    ) files'
+  );
+in
+
+packages
diff --git a/maintainers/scripts/haskell/transitive-broken-packages.nix b/maintainers/scripts/haskell/transitive-broken-packages.nix
new file mode 100644
index 00000000000..d4ddaa95765
--- /dev/null
+++ b/maintainers/scripts/haskell/transitive-broken-packages.nix
@@ -0,0 +1,16 @@
+let
+  nixpkgs = import ../../..;
+  inherit (nixpkgs {}) pkgs lib;
+  getEvaluating = x:
+    builtins.attrNames (
+      lib.filterAttrs (
+        _: v: (builtins.tryEval (v.outPath or null)).success && lib.isDerivation v && !v.meta.broken
+      ) x
+    );
+  brokenDeps = lib.subtractLists
+    (getEvaluating pkgs.haskellPackages)
+    (getEvaluating (nixpkgs { config.allowBroken = true; }).haskellPackages);
+in
+''
+  ${lib.concatMapStringsSep "\n" (x: "  - ${x}") brokenDeps}
+''
diff --git a/maintainers/scripts/haskell/update-cabal2nix-unstable.sh b/maintainers/scripts/haskell/update-cabal2nix-unstable.sh
new file mode 100755
index 00000000000..41583704560
--- /dev/null
+++ b/maintainers/scripts/haskell/update-cabal2nix-unstable.sh
@@ -0,0 +1,17 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p coreutils curl jq gnused haskellPackages.cabal2nix-unstable -I nixpkgs=.
+
+# Updates cabal2nix-unstable to the latest master of the nixos/cabal2nix repository.
+# See regenerate-hackage-packages.sh for details on the purpose of this script.
+
+set -euo pipefail
+
+# fetch current master HEAD from Github
+head_info="$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/NixOS/cabal2nix/branches/master)"
+# extract commit hash
+commit="$(jq -r .commit.sha <<< "$head_info")"
+# extract commit timestamp and convert to date
+date="$(date "--date=$(jq -r .commit.commit.committer.date <<< "$head_info")" +%F)"
+# generate nix expression from cabal file, replacing the version with the commit date
+echo '# This file defines cabal2nix-unstable, used by maintainers/scripts/haskell/regenerate-hackage-packages.sh.' > pkgs/development/haskell-modules/cabal2nix-unstable.nix
+cabal2nix "https://github.com/NixOS/cabal2nix/archive/$commit.tar.gz" | sed -e 's/version = ".*"/version = "'"unstable-$date"'"/' >> pkgs/development/haskell-modules/cabal2nix-unstable.nix
diff --git a/maintainers/scripts/haskell/update-hackage.sh b/maintainers/scripts/haskell/update-hackage.sh
new file mode 100755
index 00000000000..a7cfecbbb0f
--- /dev/null
+++ b/maintainers/scripts/haskell/update-hackage.sh
@@ -0,0 +1,35 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p nix curl jq nix-prefetch-github git gnused -I nixpkgs=.
+
+# See regenerate-hackage-packages.sh for details on the purpose of this script.
+
+set -euo pipefail
+
+pin_file=pkgs/data/misc/hackage/pin.json
+current_commit="$(jq -r .commit $pin_file)"
+old_date="$(jq -r .msg $pin_file | sed 's/Update from Hackage at //')"
+git_info="$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/commercialhaskell/all-cabal-hashes/branches/hackage)"
+head_commit="$(echo "$git_info" | jq -r .commit.sha)"
+commit_msg="$(echo "$git_info" | jq -r .commit.commit.message)"
+new_date="$(echo "$commit_msg" | sed 's/Update from Hackage at //')"
+
+if [ "$current_commit" != "$head_commit" ]; then
+   url="https://github.com/commercialhaskell/all-cabal-hashes/archive/$head_commit.tar.gz"
+   hash="$(nix-prefetch-url "$url")"
+   jq -n \
+     --arg commit "$head_commit" \
+     --arg hash "$hash" \
+     --arg url "$url" \
+     --arg commit_msg "$commit_msg" \
+     '{commit: $commit, url: $url, sha256: $hash, msg: $commit_msg}' \
+     > $pin_file
+fi
+
+if [[ "${1:-}" == "--do-commit" ]]; then
+git add pkgs/data/misc/hackage/pin.json
+git commit -F - << EOF
+all-cabal-hashes: $old_date -> $new_date
+
+This commit has been generated by maintainers/scripts/haskell/update-hackage.sh
+EOF
+fi
diff --git a/maintainers/scripts/haskell/update-stackage.sh b/maintainers/scripts/haskell/update-stackage.sh
new file mode 100755
index 00000000000..ecf38dc4b90
--- /dev/null
+++ b/maintainers/scripts/haskell/update-stackage.sh
@@ -0,0 +1,57 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p nix curl jq nix-prefetch-github git gnused gnugrep -I nixpkgs=.
+
+set -eu -o pipefail
+
+tmpfile=$(mktemp "update-stackage.XXXXXXX")
+# shellcheck disable=SC2064
+
+stackage_config="pkgs/development/haskell-modules/configuration-hackage2nix/stackage.yaml"
+
+trap "rm ${tmpfile} ${tmpfile}.new" 0
+touch "$tmpfile" "$tmpfile.new" # Creating files here so that trap creates no errors.
+
+curl -L -s "https://stackage.org/lts/cabal.config" >"$tmpfile"
+old_version=$(grep "# Stackage" $stackage_config | sed -E 's/.*([0-9]{2}\.[0-9]+)/\1/')
+version=$(sed -rn "s/^--.*http:..(www.)?stackage.org.snapshot.lts-//p" "$tmpfile")
+
+if [[ "$old_version" == "$version" ]]; then
+   echo "No new stackage version"
+   exit 0 # Nothing to do
+fi
+
+echo "Updating Stackage LTS from $old_version to $version."
+
+# Create a simple yaml version of the file.
+sed -r \
+    -e '/^--/d' \
+    -e 's|^constraints:||' \
+    -e 's|^ +|  - |' \
+    -e 's|,$||' \
+    -e '/installed$/d' \
+    -e '/^$/d' \
+    < "${tmpfile}" | sort --ignore-case >"${tmpfile}.new"
+
+cat > $stackage_config << EOF
+# Stackage LTS $version
+# This file is auto-generated by
+# maintainers/scripts/haskell/update-stackage.sh
+default-package-overrides:
+EOF
+
+# Drop restrictions on some tools where we always want the latest version.
+sed -r \
+    -e '/ cabal2nix /d' \
+    -e '/ distribution-nixpkgs /d' \
+    -e '/ jailbreak-cabal /d' \
+    -e '/ language-nix /d' \
+    < "${tmpfile}.new" >> $stackage_config
+
+if [[ "${1:-}" == "--do-commit" ]]; then
+git add $stackage_config
+git commit -F - << EOF
+haskellPackages: stackage-lts $old_version -> $version
+
+This commit has been generated by maintainers/scripts/haskell/update-stackage.sh
+EOF
+fi
diff --git a/maintainers/scripts/haskell/upload-nixos-package-list-to-hackage.sh b/maintainers/scripts/haskell/upload-nixos-package-list-to-hackage.sh
new file mode 100755
index 00000000000..8c39d289f7a
--- /dev/null
+++ b/maintainers/scripts/haskell/upload-nixos-package-list-to-hackage.sh
@@ -0,0 +1,22 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p nix curl gnused -I nixpkgs=.
+
+# On Hackage every package description shows a category "Distributions" which
+# lists a "NixOS" version.
+# This script uploads a csv to hackage which will update the displayed versions
+# based on the current versions in nixpkgs. This happens with a simple http
+# request.
+
+# For authorization you just need to have any valid hackage account. This
+# script uses the `username` and `password-command` field from your
+# ~/.cabal/config file.
+
+# e.g. username: maralorn
+#      password-command: pass hackage.haskell.org (this can be any command, but not an arbitrary shell expression. Like cabal we only read the first output line and ignore the rest.)
+# Those fields are specified under `upload` on the `cabal` man page.
+
+package_list="$(nix-build -A haskell.package-list)/nixos-hackage-packages.csv"
+username=$(grep "^username:" ~/.cabal/config | sed "s/^username: //")
+password_command=$(grep "^password-command:" ~/.cabal/config | sed "s/^password-command: //")
+curl -u "$username:$($password_command | head -n1)" --digest -H "Content-type: text/csv" -T "$package_list" http://hackage.haskell.org/distro/NixOS/packages.csv
+echo