at 25.11-pre 33 kB view raw
1#! /usr/bin/env nix-shell 2#! nix-shell -p "haskellPackages.ghcWithPackages (p: [p.aeson p.req])" 3#! nix-shell -p hydra 4#! nix-shell -i runhaskell 5 6{- 7 8The purpose of this script is 9 101) download the state of the nixpkgs/haskell-updates job from hydra (with get-report) 112) print a summary of the state suitable for pasting into a github comment (with ping-maintainers) 123) print a list of broken packages suitable for pasting into configuration-hackage2nix.yaml 13 14Because step 1) is quite expensive and takes roughly ~5 minutes the result is cached in a json file in XDG_CACHE. 15 16-} 17{-# LANGUAGE BlockArguments #-} 18{-# LANGUAGE DeriveAnyClass #-} 19{-# LANGUAGE DeriveGeneric #-} 20{-# LANGUAGE DerivingStrategies #-} 21{-# LANGUAGE DuplicateRecordFields #-} 22{-# LANGUAGE FlexibleContexts #-} 23{-# LANGUAGE GeneralizedNewtypeDeriving #-} 24{-# LANGUAGE LambdaCase #-} 25{-# LANGUAGE NamedFieldPuns #-} 26{-# LANGUAGE OverloadedStrings #-} 27{-# LANGUAGE ScopedTypeVariables #-} 28{-# LANGUAGE TupleSections #-} 29{-# LANGUAGE ViewPatterns #-} 30{-# OPTIONS_GHC -Wall #-} 31{-# LANGUAGE DataKinds #-} 32 33import Control.Monad (forM_, forM, (<=<)) 34import Control.Monad.Trans (MonadIO (liftIO)) 35import Data.Aeson ( 36 FromJSON, 37 FromJSONKey, 38 ToJSON, 39 decodeFileStrict', 40 eitherDecodeStrict', 41 encodeFile, 42 ) 43import Data.Foldable (Foldable (toList), foldl') 44import Data.List.NonEmpty (NonEmpty, nonEmpty) 45import qualified Data.List.NonEmpty as NonEmpty 46import Data.Map.Strict (Map) 47import qualified Data.Map.Strict as Map 48import Data.Maybe (fromMaybe, mapMaybe, isNothing) 49import Data.Monoid (Sum (Sum, getSum)) 50import Data.Sequence (Seq) 51import qualified Data.Sequence as Seq 52import Data.Set (Set) 53import qualified Data.Set as Set 54import Data.Text (Text) 55import qualified Data.Text as Text 56import Data.Text.Encoding (encodeUtf8) 57import qualified Data.Text.IO as Text 58import Data.Time (defaultTimeLocale, formatTime, getCurrentTime) 59import Data.Time.Clock (UTCTime) 60import GHC.Generics (Generic) 61import Network.HTTP.Req ( 62 GET (GET), 63 HttpResponse (HttpResponseBody), 64 NoReqBody (NoReqBody), 65 Option, 66 Req, 67 Scheme (Https), 68 bsResponse, 69 defaultHttpConfig, 70 header, 71 https, 72 jsonResponse, 73 req, 74 responseBody, 75 responseTimeout, 76 runReq, 77 (/:), 78 ) 79import System.Directory (XdgDirectory (XdgCache), getXdgDirectory) 80import System.Environment (getArgs) 81import System.Exit (die) 82import System.Process (readProcess) 83import Prelude hiding (id) 84import Data.List (sortOn) 85import Control.Concurrent.Async (concurrently) 86import Control.Exception (evaluate) 87import qualified Data.IntMap.Lazy as IntMap 88import qualified Data.IntSet as IntSet 89import Data.Bifunctor (second) 90import Data.Data (Proxy) 91import Data.ByteString (ByteString) 92import qualified Data.ByteString.Char8 as ByteString 93import Distribution.Simple.Utils (safeLast, fromUTF8BS) 94 95newtype JobsetEvals = JobsetEvals 96 { evals :: Seq Eval 97 } 98 deriving stock (Generic, Show) 99 deriving anyclass (ToJSON, FromJSON) 100 101newtype Nixpkgs = Nixpkgs {revision :: Text} 102 deriving stock (Generic, Show) 103 deriving anyclass (ToJSON, FromJSON) 104 105newtype JobsetEvalInputs = JobsetEvalInputs {nixpkgs :: Nixpkgs} 106 deriving stock (Generic, Show) 107 deriving anyclass (ToJSON, FromJSON) 108 109data Eval = Eval 110 { id :: Int 111 , jobsetevalinputs :: JobsetEvalInputs 112 , builds :: Seq Int 113 } 114 deriving (Generic, ToJSON, FromJSON, Show) 115 116-- | Hydra job name. 117-- 118-- Examples: 119-- - @"haskellPackages.lens.x86_64-linux"@ 120-- - @"haskell.packages.ghc925.cabal-install.aarch64-darwin"@ 121-- - @"pkgsMusl.haskell.compiler.ghc90.x86_64-linux"@ 122-- - @"arion.aarch64-linux"@ 123newtype JobName = JobName { unJobName :: Text } 124 deriving stock (Generic, Show) 125 deriving newtype (Eq, FromJSONKey, FromJSON, Ord, ToJSON) 126 127-- | Datatype representing the result of querying the build evals of the 128-- haskell-updates Hydra jobset. 129-- 130-- The URL <https://hydra.nixos.org/eval/EVAL_ID/builds> (where @EVAL_ID@ is a 131-- value like 1792418) returns a list of 'Build'. 132data Build = Build 133 { job :: JobName 134 , buildstatus :: Maybe Int 135 -- ^ Status of the build. See 'getBuildState' for the meaning of each state. 136 , finished :: Int 137 -- ^ Whether or not the build is finished. @0@ if finished, non-zero otherwise. 138 , id :: Int 139 , nixname :: Text 140 -- ^ Nix name of the derivation. 141 -- 142 -- Examples: 143 -- - @"lens-5.2.1"@ 144 -- - @"cabal-install-3.8.0.1"@ 145 -- - @"lens-static-x86_64-unknown-linux-musl-5.1.1"@ 146 , system :: Text 147 -- ^ System 148 -- 149 -- Examples: 150 -- - @"x86_64-linux"@ 151 -- - @"aarch64-darwin"@ 152 , jobsetevals :: Seq Int 153 } 154 deriving (Generic, ToJSON, FromJSON, Show) 155 156data HydraSlownessWorkaroundFlag = HydraSlownessWorkaround | NoHydraSlownessWorkaround 157data RequestLogsFlag = RequestLogs | NoRequestLogs 158 159usage :: IO a 160usage = die "Usage: get-report [--slow] [EVAL-ID] | ping-maintainers | mark-broken-list [--no-request-logs] | eval-info" 161 162main :: IO () 163main = do 164 args <- getArgs 165 case args of 166 "get-report":"--slow":id -> getBuildReports HydraSlownessWorkaround id 167 "get-report":id -> getBuildReports NoHydraSlownessWorkaround id 168 ["ping-maintainers"] -> printMaintainerPing 169 ["mark-broken-list", "--no-request-logs"] -> printMarkBrokenList NoRequestLogs 170 ["mark-broken-list"] -> printMarkBrokenList RequestLogs 171 ["eval-info"] -> printEvalInfo 172 _ -> usage 173 174reportFileName :: IO FilePath 175reportFileName = getXdgDirectory XdgCache "haskell-updates-build-report.json" 176 177showT :: Show a => a -> Text 178showT = Text.pack . show 179 180getBuildReports :: HydraSlownessWorkaroundFlag -> [String] -> IO () 181getBuildReports opt args = runReq defaultHttpConfig do 182 eval@Eval{id} <- case args of 183 [id] -> hydraJSONQuery mempty ["eval", Text.pack id] 184 [] -> do 185 evalMay <- Seq.lookup 0 . evals <$> hydraJSONQuery mempty ["jobset", "nixpkgs", "haskell-updates", "evals"] 186 maybe (liftIO $ fail "No Evaluation found") pure evalMay 187 _ -> liftIO usage 188 liftIO . putStrLn $ "Fetching evaluation " <> show id <> " from Hydra. This might take a few minutes..." 189 buildReports <- getEvalBuilds opt eval 190 liftIO do 191 fileName <- reportFileName 192 putStrLn $ "Finished fetching all builds from Hydra, saving report as " <> fileName 193 now <- getCurrentTime 194 encodeFile fileName (eval, now, buildReports) 195 196getEvalBuilds :: HydraSlownessWorkaroundFlag -> Eval -> Req (Seq Build) 197getEvalBuilds NoHydraSlownessWorkaround Eval{id} = 198 hydraJSONQuery mempty ["eval", showT id, "builds"] 199getEvalBuilds HydraSlownessWorkaround Eval{builds} = do 200 forM builds $ \buildId -> do 201 liftIO $ putStrLn $ "Querying build " <> show buildId 202 hydraJSONQuery mempty [ "build", showT buildId ] 203 204hydraQuery :: HttpResponse a => Proxy a -> Option 'Https -> [Text] -> Req (HttpResponseBody a) 205hydraQuery responseType option query = do 206 let customHeaderOpt = 207 header 208 "User-Agent" 209 "hydra-report.hs/v1 (nixpkgs;maintainers/scripts/haskell) pls fix https://github.com/NixOS/nixos-org-configurations/issues/270" 210 customTimeoutOpt = responseTimeout 900_000_000 -- 15 minutes 211 opts = customHeaderOpt <> customTimeoutOpt <> option 212 url = foldl' (/:) (https "hydra.nixos.org") query 213 responseBody <$> req GET url NoReqBody responseType opts 214 215hydraJSONQuery :: FromJSON a => Option 'Https -> [Text] -> Req a 216hydraJSONQuery = hydraQuery jsonResponse 217 218hydraPlainQuery :: [Text] -> Req ByteString 219hydraPlainQuery = hydraQuery bsResponse mempty 220 221hydraEvalCommand :: FilePath 222hydraEvalCommand = "hydra-eval-jobs" 223 224hydraEvalParams :: [String] 225hydraEvalParams = ["-I", ".", "pkgs/top-level/release-haskell.nix"] 226 227nixExprCommand :: FilePath 228nixExprCommand = "nix-instantiate" 229 230nixExprParams :: [String] 231nixExprParams = ["--eval", "--strict", "--json"] 232 233-- | This newtype is used to parse a Hydra job output from @hydra-eval-jobs@. 234-- The only field we are interested in is @maintainers@, which is why this 235-- is just a newtype. 236-- 237-- Note that there are occasionally jobs that don't have a maintainers 238-- field, which is why this has to be @Maybe Text@. 239newtype Maintainers = Maintainers { maintainers :: Maybe Text } 240 deriving stock (Generic, Show) 241 deriving anyclass (FromJSON, ToJSON) 242 243-- | This is a 'Map' from Hydra job name to maintainer email addresses. 244-- 245-- It has values similar to the following: 246-- 247-- @@ 248-- fromList 249-- [ ("arion.aarch64-linux", Maintainers (Just "robert@example.com")) 250-- , ("bench.x86_64-linux", Maintainers (Just "")) 251-- , ("conduit.x86_64-linux", Maintainers (Just "snoy@man.com, web@ber.com")) 252-- , ("lens.x86_64-darwin", Maintainers (Just "ek@category.com")) 253-- ] 254-- @@ 255-- 256-- Note that Hydra jobs without maintainers will have an empty string for the 257-- maintainer list. 258type HydraJobs = Map JobName Maintainers 259 260-- | Map of email addresses to GitHub handles. 261-- This is built from the file @../../maintainer-list.nix@. 262-- 263-- It has values similar to the following: 264-- 265-- @@ 266-- fromList 267-- [ ("robert@example.com", "rob22") 268-- , ("ek@category.com", "edkm") 269-- ] 270-- @@ 271type EmailToGitHubHandles = Map Text Text 272 273-- | Map of Hydra jobs to maintainer GitHub handles. 274-- 275-- It has values similar to the following: 276-- 277-- @@ 278-- fromList 279-- [ ("arion.aarch64-linux", ["rob22"]) 280-- , ("conduit.x86_64-darwin", ["snoyb", "webber"]) 281-- ] 282-- @@ 283type MaintainerMap = Map JobName (NonEmpty Text) 284 285-- | Information about a package which lists its dependencies and whether the 286-- package is marked broken. 287data DepInfo = DepInfo { 288 deps :: Set PkgName, 289 broken :: Bool 290} 291 deriving stock (Generic, Show) 292 deriving anyclass (FromJSON, ToJSON) 293 294-- | Map from package names to their DepInfo. This is the data we get out of a 295-- nix call. 296type DependencyMap = Map PkgName DepInfo 297 298-- | Map from package names to its broken state, number of reverse dependencies (fst) and 299-- unbroken reverse dependencies (snd). 300type ReverseDependencyMap = Map PkgName (Int, Int) 301 302-- | Calculate the (unbroken) reverse dependencies of a package by transitively 303-- going through all packages if it’s a dependency of them. 304calculateReverseDependencies :: DependencyMap -> ReverseDependencyMap 305calculateReverseDependencies depMap = 306 Map.fromDistinctAscList $ zip keys (zip (rdepMap False) (rdepMap True)) 307 where 308 -- This code tries to efficiently invert the dependency map and calculate 309 -- its transitive closure by internally identifying every pkg with its index 310 -- in the package list and then using memoization. 311 keys :: [PkgName] 312 keys = Map.keys depMap 313 314 pkgToIndexMap :: Map PkgName Int 315 pkgToIndexMap = Map.fromDistinctAscList (zip keys [0..]) 316 317 depInfos :: [DepInfo] 318 depInfos = Map.elems depMap 319 320 depInfoToIdx :: DepInfo -> (Bool, [Int]) 321 depInfoToIdx DepInfo{broken,deps} = 322 (broken, mapMaybe (`Map.lookup` pkgToIndexMap) $ Set.toList deps) 323 324 intDeps :: [(Int, (Bool, [Int]))] 325 intDeps = zip [0..] (fmap depInfoToIdx depInfos) 326 327 rdepMap onlyUnbroken = IntSet.size <$> IntMap.elems resultList 328 where 329 resultList = IntMap.fromDistinctAscList [(i, go i) | i <- [0..length keys - 1]] 330 oneStepMap = IntMap.fromListWith IntSet.union $ (\(key,(_,deps)) -> (,IntSet.singleton key) <$> deps) <=< filter (\(_, (broken,_)) -> not (broken && onlyUnbroken)) $ intDeps 331 go pkg = IntSet.unions (oneStep:((resultList IntMap.!) <$> IntSet.toList oneStep)) 332 where oneStep = IntMap.findWithDefault mempty pkg oneStepMap 333 334-- | Generate a mapping of Hydra job names to maintainer GitHub handles. Calls 335-- hydra-eval-jobs and the nix script ./maintainer-handles.nix. 336getMaintainerMap :: IO MaintainerMap 337getMaintainerMap = do 338 hydraJobs :: HydraJobs <- 339 readJSONProcess hydraEvalCommand hydraEvalParams "Failed to decode hydra-eval-jobs output: " 340 handlesMap :: EmailToGitHubHandles <- 341 readJSONProcess nixExprCommand ("maintainers/scripts/haskell/maintainer-handles.nix":nixExprParams) "Failed to decode nix output for lookup of github handles: " 342 pure $ Map.mapMaybe (splitMaintainersToGitHubHandles handlesMap) hydraJobs 343 where 344 -- Split a comma-spearated string of Maintainers into a NonEmpty list of 345 -- GitHub handles. 346 splitMaintainersToGitHubHandles 347 :: EmailToGitHubHandles -> Maintainers -> Maybe (NonEmpty Text) 348 splitMaintainersToGitHubHandles handlesMap (Maintainers maint) = 349 nonEmpty . mapMaybe (`Map.lookup` handlesMap) . Text.splitOn ", " $ fromMaybe "" maint 350 351-- | Get the a map of all dependencies of every package by calling the nix 352-- script ./dependencies.nix. 353getDependencyMap :: IO DependencyMap 354getDependencyMap = 355 readJSONProcess 356 nixExprCommand 357 ("maintainers/scripts/haskell/dependencies.nix" : nixExprParams) 358 "Failed to decode nix output for lookup of dependencies: " 359 360-- | Run a process that produces JSON on stdout and and decode the JSON to a 361-- data type. 362-- 363-- If the JSON-decoding fails, throw the JSON-decoding error. 364readJSONProcess 365 :: FromJSON a 366 => FilePath -- ^ Filename of executable. 367 -> [String] -- ^ Arguments 368 -> String -- ^ String to prefix to JSON-decode error. 369 -> IO a 370readJSONProcess exe args err = do 371 output <- readProcess exe args "" 372 let eitherDecodedOutput = eitherDecodeStrict' . encodeUtf8 . Text.pack $ output 373 case eitherDecodedOutput of 374 Left decodeErr -> error $ err <> decodeErr <> "\nRaw: '" <> take 1000 output <> "'" 375 Right decodedOutput -> pure decodedOutput 376 377-- BuildStates are sorted by subjective importance/concerningness 378data BuildState 379 = Failed 380 | DependencyFailed 381 | OutputLimitExceeded 382 | Unknown (Maybe Int) 383 | TimedOut 384 | Canceled 385 | HydraFailure 386 | Unfinished 387 | Success 388 deriving stock (Show, Eq, Ord) 389 390icon :: BuildState -> Text 391icon = \case 392 Failed -> "" 393 DependencyFailed -> "" 394 OutputLimitExceeded -> "⚠️" 395 Unknown x -> "unknown code " <> showT x 396 TimedOut -> "⌛🚫" 397 Canceled -> "🚫" 398 Unfinished -> "" 399 HydraFailure -> "🚧" 400 Success -> "" 401 402platformIcon :: Platform -> Text 403platformIcon (Platform x) = case x of 404 "x86_64-linux" -> "🐧" 405 "aarch64-linux" -> "📱" 406 "x86_64-darwin" -> "🍎" 407 "aarch64-darwin" -> "🍏" 408 _ -> x 409 410platformIsOS :: OS -> Platform -> Bool 411platformIsOS os (Platform x) = case (os, x) of 412 (Linux, "x86_64-linux") -> True 413 (Linux, "aarch64-linux") -> True 414 (Darwin, "x86_64-darwin") -> True 415 (Darwin, "aarch64-darwin") -> True 416 _ -> False 417 418 419-- | A package name. This is parsed from a 'JobName'. 420-- 421-- Examples: 422-- 423-- - The 'JobName' @"haskellPackages.lens.x86_64-linux"@ produces the 'PkgName' 424-- @"lens"@. 425-- - The 'JobName' @"haskell.packages.ghc925.cabal-install.aarch64-darwin"@ 426-- produces the 'PkgName' @"cabal-install"@. 427-- - The 'JobName' @"pkgsMusl.haskell.compiler.ghc90.x86_64-linux"@ produces 428-- the 'PkgName' @"ghc90"@. 429-- - The 'JobName' @"arion.aarch64-linux"@ produces the 'PkgName' @"arion"@. 430-- 431-- 'PkgName' is also used as a key in 'DependencyMap' and 'ReverseDependencyMap'. 432-- In this case, 'PkgName' originally comes from attribute names in @haskellPackages@ 433-- in Nixpkgs. 434newtype PkgName = PkgName Text 435 deriving stock (Generic, Show) 436 deriving newtype (Eq, FromJSON, FromJSONKey, Ord, ToJSON) 437 438-- | A package set name. This is parsed from a 'JobName'. 439-- 440-- Examples: 441-- 442-- - The 'JobName' @"haskellPackages.lens.x86_64-linux"@ produces the 'PkgSet' 443-- @"haskellPackages"@. 444-- - The 'JobName' @"haskell.packages.ghc925.cabal-install.aarch64-darwin"@ 445-- produces the 'PkgSet' @"haskell.packages.ghc925"@. 446-- - The 'JobName' @"pkgsMusl.haskell.compiler.ghc90.x86_64-linux"@ produces 447-- the 'PkgSet' @"pkgsMusl.haskell.compiler"@. 448-- - The 'JobName' @"arion.aarch64-linux"@ produces the 'PkgSet' @""@. 449-- 450-- As you can see from the last example, 'PkgSet' can be empty (@""@) for 451-- top-level jobs. 452newtype PkgSet = PkgSet Text 453 deriving stock (Generic, Show) 454 deriving newtype (Eq, FromJSON, FromJSONKey, Ord, ToJSON) 455 456data BuildResult = BuildResult {state :: BuildState, id :: Int} deriving (Show, Eq, Ord) 457newtype Platform = Platform {platform :: Text} deriving (Show, Eq, Ord) 458data SummaryEntry = SummaryEntry { 459 summaryBuilds :: Table PkgSet Platform BuildResult, 460 summaryMaintainers :: Set Text, 461 summaryReverseDeps :: Int, 462 summaryUnbrokenReverseDeps :: Int 463} 464type StatusSummary = Map PkgName SummaryEntry 465 466data OS = Linux | Darwin 467 468newtype Table row col a = Table (Map (row, col) a) 469 470singletonTable :: row -> col -> a -> Table row col a 471singletonTable row col a = Table $ Map.singleton (row, col) a 472 473unionTable :: (Ord row, Ord col) => Table row col a -> Table row col a -> Table row col a 474unionTable (Table l) (Table r) = Table $ Map.union l r 475 476filterWithKeyTable :: (row -> col -> a -> Bool) -> Table row col a -> Table row col a 477filterWithKeyTable f (Table t) = Table $ Map.filterWithKey (\(r,c) a -> f r c a) t 478 479nullTable :: Table row col a -> Bool 480nullTable (Table t) = Map.null t 481 482instance (Ord row, Ord col, Semigroup a) => Semigroup (Table row col a) where 483 Table l <> Table r = Table (Map.unionWith (<>) l r) 484instance (Ord row, Ord col, Semigroup a) => Monoid (Table row col a) where 485 mempty = Table Map.empty 486instance Functor (Table row col) where 487 fmap f (Table a) = Table (fmap f a) 488instance Foldable (Table row col) where 489 foldMap f (Table a) = foldMap f a 490 491getBuildState :: Build -> BuildState 492getBuildState Build{finished, buildstatus} = case (finished, buildstatus) of 493 (0, _) -> Unfinished 494 (_, Just 0) -> Success 495 (_, Just 1) -> Failed 496 (_, Just 2) -> DependencyFailed 497 (_, Just 3) -> HydraFailure 498 (_, Just 4) -> Canceled 499 (_, Just 7) -> TimedOut 500 (_, Just 11) -> OutputLimitExceeded 501 (_, i) -> Unknown i 502 503combineStatusSummaries :: Seq StatusSummary -> StatusSummary 504combineStatusSummaries = foldl (Map.unionWith unionSummary) Map.empty 505 where 506 unionSummary :: SummaryEntry -> SummaryEntry -> SummaryEntry 507 unionSummary (SummaryEntry lb lm lr lu) (SummaryEntry rb rm rr ru) = 508 SummaryEntry (unionTable lb rb) (lm <> rm) (max lr rr) (max lu ru) 509 510buildToPkgNameAndSet :: Build -> (PkgName, PkgSet) 511buildToPkgNameAndSet Build{job = JobName jobName, system} = (name, set) 512 where 513 packageName :: Text 514 packageName = fromMaybe jobName (Text.stripSuffix ("." <> system) jobName) 515 516 splitted :: Maybe (NonEmpty Text) 517 splitted = nonEmpty $ Text.splitOn "." packageName 518 519 name :: PkgName 520 name = PkgName $ maybe packageName NonEmpty.last splitted 521 522 set :: PkgSet 523 set = PkgSet $ maybe "" (Text.intercalate "." . NonEmpty.init) splitted 524 525buildToStatusSummary :: MaintainerMap -> ReverseDependencyMap -> Build -> StatusSummary 526buildToStatusSummary maintainerMap reverseDependencyMap build@Build{job, id, system} = 527 Map.singleton pkgName summaryEntry 528 where 529 (pkgName, pkgSet) = buildToPkgNameAndSet build 530 531 maintainers :: Set Text 532 maintainers = maybe mempty (Set.fromList . toList) (Map.lookup job maintainerMap) 533 534 (reverseDeps, unbrokenReverseDeps) = 535 Map.findWithDefault (0,0) pkgName reverseDependencyMap 536 537 buildTable :: Table PkgSet Platform BuildResult 538 buildTable = 539 singletonTable pkgSet (Platform system) (BuildResult (getBuildState build) id) 540 541 summaryEntry = SummaryEntry buildTable maintainers reverseDeps unbrokenReverseDeps 542 543readBuildReports :: IO (Eval, UTCTime, Seq Build) 544readBuildReports = do 545 file <- reportFileName 546 fromMaybe (error $ "Could not decode " <> file) <$> decodeFileStrict' file 547 548sep :: Text 549sep = " | " 550joinTable :: [Text] -> Text 551joinTable t = sep <> Text.intercalate sep t <> sep 552 553type NumSummary = Table Platform BuildState Int 554 555printTable :: (Ord rows, Ord cols) => Text -> (rows -> Text) -> (cols -> Text) -> (entries -> Text) -> Table rows cols entries -> [Text] 556printTable name showR showC showE (Table mapping) = joinTable <$> (name : map showC cols) : replicate (length cols + sepsInName + 1) "---" : map printRow rows 557 where 558 sepsInName = Text.count "|" name 559 printRow row = showR row : map (\col -> maybe "" showE (Map.lookup (row, col) mapping)) cols 560 rows = toList $ Set.fromList (fst <$> Map.keys mapping) 561 cols = toList $ Set.fromList (snd <$> Map.keys mapping) 562 563printJob :: Int -> PkgName -> (Table PkgSet Platform BuildResult, Text) -> [Text] 564printJob evalId (PkgName name) (Table mapping, maintainers) = 565 if length sets <= 1 566 then map printSingleRow sets 567 else ["- [ ] " <> makeJobSearchLink (PkgSet "") name <> " " <> maintainers] <> map printRow sets 568 where 569 printRow :: PkgSet -> Text 570 printRow (PkgSet set) = 571 " - " <> printState (PkgSet set) <> " " <> 572 makeJobSearchLink (PkgSet set) (if Text.null set then "toplevel" else set) 573 574 printSingleRow set = 575 "- [ ] " <> printState set <> " " <> 576 makeJobSearchLink set (makePkgName set) <> " " <> maintainers 577 578 makePkgName :: PkgSet -> Text 579 makePkgName (PkgSet set) = (if Text.null set then "" else set <> ".") <> name 580 581 printState set = 582 Text.intercalate " " $ map (\pf -> maybe "" (label pf) $ Map.lookup (set, pf) mapping) platforms 583 584 makeJobSearchLink :: PkgSet -> Text -> Text 585 makeJobSearchLink set linkLabel = makeSearchLink evalId linkLabel (makePkgName set) 586 587 sets :: [PkgSet] 588 sets = toList $ Set.fromList (fst <$> Map.keys mapping) 589 590 platforms :: [Platform] 591 platforms = toList $ Set.fromList (snd <$> Map.keys mapping) 592 593 label pf (BuildResult s i) = "[[" <> platformIcon pf <> icon s <> "]](https://hydra.nixos.org/build/" <> showT i <> ")" 594 595makeSearchLink :: Int -> Text -> Text -> Text 596makeSearchLink evalId linkLabel query = "[" <> linkLabel <> "](" <> "https://hydra.nixos.org/eval/" <> showT evalId <> "?filter=" <> query <> ")" 597 598statusToNumSummary :: StatusSummary -> NumSummary 599statusToNumSummary = fmap getSum . foldMap (fmap Sum . jobTotals) 600 601jobTotals :: SummaryEntry -> Table Platform BuildState Int 602jobTotals (summaryBuilds -> Table mapping) = getSum <$> Table (Map.foldMapWithKey (\(_, platform) (BuildResult buildstate _) -> Map.singleton (platform, buildstate) (Sum 1)) mapping) 603 604details :: Text -> [Text] -> [Text] 605details summary content = ["<details><summary>" <> summary <> " </summary>", ""] <> content <> ["</details>", ""] 606 607evalLine :: Eval -> UTCTime -> Text 608evalLine Eval{id, jobsetevalinputs = JobsetEvalInputs{nixpkgs = Nixpkgs{revision}}} fetchTime = 609 "*evaluation [" 610 <> showT id 611 <> "](https://hydra.nixos.org/eval/" 612 <> showT id 613 <> ") of nixpkgs commit [" 614 <> Text.take 7 revision 615 <> "](https://github.com/NixOS/nixpkgs/commits/" 616 <> revision 617 <> ") as of " 618 <> Text.pack (formatTime defaultTimeLocale "%Y-%m-%d %H:%M UTC" fetchTime) 619 <> "*" 620 621printBuildSummary :: Eval -> UTCTime -> StatusSummary -> [(PkgName, Int)] -> Text 622printBuildSummary eval@Eval{id} fetchTime summary topBrokenRdeps = 623 Text.unlines $ 624 headline <> [""] <> tldr <> ((" * "<>) <$> (errors <> warnings)) <> [""] 625 <> totals 626 <> optionalList "#### Maintained Linux packages with build failure" (maintainedList (fails summaryLinux)) 627 <> optionalList "#### Maintained Linux packages with failed dependency" (maintainedList (failedDeps summaryLinux)) 628 <> optionalList "#### Maintained Linux packages with unknown error" (maintainedList (unknownErr summaryLinux)) 629 <> optionalHideableList "#### Maintained Darwin packages with build failure" (maintainedList (fails summaryDarwin)) 630 <> optionalHideableList "#### Maintained Darwin packages with failed dependency" (maintainedList (failedDeps summaryDarwin)) 631 <> optionalHideableList "#### Maintained Darwin packages with unknown error" (maintainedList (unknownErr summaryDarwin)) 632 <> optionalHideableList "#### Unmaintained packages with build failure" (unmaintainedList (fails summary)) 633 <> optionalHideableList "#### Unmaintained packages with failed dependency" (unmaintainedList (failedDeps summary)) 634 <> optionalHideableList "#### Unmaintained packages with unknown error" (unmaintainedList (unknownErr summary)) 635 <> optionalHideableList "#### Top 50 broken packages, sorted by number of reverse dependencies" (brokenLine <$> topBrokenRdeps) 636 <> ["","*⤴️: 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.*",""] 637 <> footer 638 where 639 footer = ["*Report generated with [maintainers/scripts/haskell/hydra-report.hs](https://github.com/NixOS/nixpkgs/blob/haskell-updates/maintainers/scripts/haskell/hydra-report.hs)*"] 640 641 headline = 642 [ "### [haskell-updates build report from hydra](https://hydra.nixos.org/jobset/nixpkgs/haskell-updates)" 643 , evalLine eval fetchTime 644 ] 645 646 totals :: [Text] 647 totals = 648 [ "#### Build summary" 649 , "" 650 ] <> 651 printTable 652 "Platform" 653 (\x -> makeSearchLink id (platform x <> " " <> platformIcon x) ("." <> platform x)) 654 (\x -> showT x <> " " <> icon x) 655 showT 656 numSummary 657 658 brokenLine :: (PkgName, Int) -> Text 659 brokenLine (PkgName name, rdeps) = 660 "[" <> name <> "](https://packdeps.haskellers.com/reverse/" <> name <> 661 ") ⤴️ " <> Text.pack (show rdeps) <> " " 662 663 numSummary = statusToNumSummary summary 664 665 summaryLinux :: StatusSummary 666 summaryLinux = withOS Linux summary 667 668 summaryDarwin :: StatusSummary 669 summaryDarwin = withOS Darwin summary 670 671 -- Remove all BuildResult from the Table that have Platform that isn't for 672 -- the given OS. 673 tableForOS :: OS -> Table PkgSet Platform BuildResult -> Table PkgSet Platform BuildResult 674 tableForOS os = filterWithKeyTable (\_ platform _ -> platformIsOS os platform) 675 676 -- Remove all BuildResult from the StatusSummary that have a Platform that 677 -- isn't for the given OS. Completely remove all PkgName from StatusSummary 678 -- that end up with no BuildResults. 679 withOS 680 :: OS 681 -> StatusSummary 682 -> StatusSummary 683 withOS os = 684 Map.mapMaybe 685 (\e@SummaryEntry{summaryBuilds} -> 686 let buildsForOS = tableForOS os summaryBuilds 687 in if nullTable buildsForOS then Nothing else Just e { summaryBuilds = buildsForOS } 688 ) 689 690 jobsByState :: (BuildState -> Bool) -> StatusSummary -> StatusSummary 691 jobsByState predicate = Map.filter (predicate . worstState) 692 693 worstState :: SummaryEntry -> BuildState 694 worstState = foldl' min Success . fmap state . summaryBuilds 695 696 fails :: StatusSummary -> StatusSummary 697 fails = jobsByState (== Failed) 698 699 failedDeps :: StatusSummary -> StatusSummary 700 failedDeps = jobsByState (== DependencyFailed) 701 702 unknownErr :: StatusSummary -> StatusSummary 703 unknownErr = jobsByState (\x -> x > DependencyFailed && x < TimedOut) 704 705 withMaintainer :: StatusSummary -> Map PkgName (Table PkgSet Platform BuildResult, NonEmpty Text) 706 withMaintainer = 707 Map.mapMaybe 708 (\e -> (summaryBuilds e,) <$> nonEmpty (Set.toList (summaryMaintainers e))) 709 710 withoutMaintainer :: StatusSummary -> StatusSummary 711 withoutMaintainer = Map.mapMaybe (\e -> if Set.null (summaryMaintainers e) then Just e else Nothing) 712 713 optionalList :: Text -> [Text] -> [Text] 714 optionalList heading list = if null list then mempty else [heading] <> list 715 716 optionalHideableList :: Text -> [Text] -> [Text] 717 optionalHideableList heading list = if null list then mempty else [heading] <> details (showT (length list) <> " job(s)") list 718 719 maintainedList :: StatusSummary -> [Text] 720 maintainedList = showMaintainedBuild <=< Map.toList . withMaintainer 721 722 summaryEntryGetReverseDeps :: SummaryEntry -> (Int, Int) 723 summaryEntryGetReverseDeps sumEntry = 724 ( negate $ summaryUnbrokenReverseDeps sumEntry 725 , negate $ summaryReverseDeps sumEntry 726 ) 727 728 sortOnReverseDeps :: [(PkgName, SummaryEntry)] -> [(PkgName, SummaryEntry)] 729 sortOnReverseDeps = sortOn (\(_, sumEntry) -> summaryEntryGetReverseDeps sumEntry) 730 731 unmaintainedList :: StatusSummary -> [Text] 732 unmaintainedList = showBuild <=< sortOnReverseDeps . Map.toList . withoutMaintainer 733 734 showBuild :: (PkgName, SummaryEntry) -> [Text] 735 showBuild (name, entry) = 736 printJob 737 id 738 name 739 ( summaryBuilds entry 740 , Text.pack 741 ( if summaryReverseDeps entry > 0 742 then 743 " ⤴️ " <> show (summaryUnbrokenReverseDeps entry) <> 744 " | " <> show (summaryReverseDeps entry) 745 else "" 746 ) 747 ) 748 749 showMaintainedBuild 750 :: (PkgName, (Table PkgSet Platform BuildResult, NonEmpty Text)) -> [Text] 751 showMaintainedBuild (name, (table, maintainers)) = 752 printJob 753 id 754 name 755 ( table 756 , Text.intercalate " " (fmap ("@" <>) (toList maintainers)) 757 ) 758 759 tldr = case (errors, warnings) of 760 ([],[]) -> ["🟢 **Ready to merge** (if there are no [evaluation errors](https://hydra.nixos.org/jobset/nixpkgs/haskell-updates))"] 761 ([],_) -> ["🟡 **Potential issues** (and possibly [evaluation errors](https://hydra.nixos.org/jobset/nixpkgs/haskell-updates))"] 762 _ -> ["🔴 **Branch not mergeable**"] 763 warnings = 764 if' (Unfinished > maybe Success worstState maintainedJob) "`maintained` jobset failed." <> 765 if' (Unfinished == maybe Success worstState mergeableJob) "`mergeable` jobset is not finished." <> 766 if' (Unfinished == maybe Success worstState maintainedJob) "`maintained` jobset is not finished." 767 errors = 768 if' (isNothing mergeableJob) "No `mergeable` job found." <> 769 if' (isNothing maintainedJob) "No `maintained` job found." <> 770 if' (Unfinished > maybe Success worstState mergeableJob) "`mergeable` jobset failed." <> 771 if' (outstandingJobs (Platform "x86_64-linux") > 100) "Too many outstanding jobs on x86_64-linux." <> 772 if' (outstandingJobs (Platform "aarch64-linux") > 100) "Too many outstanding jobs on aarch64-linux." 773 774 if' p e = if p then [e] else mempty 775 776 outstandingJobs platform | Table m <- numSummary = Map.findWithDefault 0 (platform, Unfinished) m 777 778 maintainedJob = Map.lookup (PkgName "maintained") summary 779 mergeableJob = Map.lookup (PkgName "mergeable") summary 780 781printEvalInfo :: IO () 782printEvalInfo = do 783 (eval, fetchTime, _) <- readBuildReports 784 putStrLn (Text.unpack $ evalLine eval fetchTime) 785 786printMaintainerPing :: IO () 787printMaintainerPing = do 788 (maintainerMap, (reverseDependencyMap, topBrokenRdeps)) <- concurrently getMaintainerMap do 789 depMap <- getDependencyMap 790 rdepMap <- evaluate . calculateReverseDependencies $ depMap 791 let tops = take 50 . sortOn (negate . snd) . fmap (second fst) . filter (\x -> maybe False broken $ Map.lookup (fst x) depMap) . Map.toList $ rdepMap 792 pure (rdepMap, tops) 793 (eval, fetchTime, buildReport) <- readBuildReports 794 let statusSummaries = 795 fmap (buildToStatusSummary maintainerMap reverseDependencyMap) buildReport 796 buildSum :: StatusSummary 797 buildSum = combineStatusSummaries statusSummaries 798 textBuildSummary = printBuildSummary eval fetchTime buildSum topBrokenRdeps 799 Text.putStrLn textBuildSummary 800 801printMarkBrokenList :: RequestLogsFlag -> IO () 802printMarkBrokenList reqLogs = do 803 (_, fetchTime, buildReport) <- readBuildReports 804 runReq defaultHttpConfig $ forM_ buildReport \build@Build{job, id} -> 805 case (getBuildState build, Text.splitOn "." $ unJobName job) of 806 (Failed, ["haskellPackages", name, "x86_64-linux"]) -> do 807 -- We use the last probable error cause found in the build log file. 808 error_message <- fromMaybe "failure" <$> 809 case reqLogs of 810 NoRequestLogs -> pure Nothing 811 RequestLogs -> do 812 -- Fetch build log from hydra to figure out the cause of the error. 813 build_log <- ByteString.lines <$> hydraPlainQuery ["build", showT id, "nixlog", "1", "raw"] 814 pure $ safeLast $ mapMaybe probableErrorCause build_log 815 liftIO $ putStrLn $ " - " <> Text.unpack name <> " # " <> error_message <> " in job https://hydra.nixos.org/build/" <> show id <> " at " <> formatTime defaultTimeLocale "%Y-%m-%d" fetchTime 816 _ -> pure () 817 818{- | This function receives a line from a Nix Haskell builder build log and returns a possible error cause. 819 | We might need to add other causes in the future if errors happen in unusual parts of the builder. 820-} 821probableErrorCause :: ByteString -> Maybe String 822probableErrorCause "Setup: Encountered missing or private dependencies:" = Just "dependency missing" 823probableErrorCause "running tests" = Just "test failure" 824probableErrorCause build_line | ByteString.isPrefixOf "Building" build_line = Just ("failure building " <> fromUTF8BS (fst $ ByteString.breakSubstring " for" $ ByteString.drop 9 build_line)) 825probableErrorCause build_line | ByteString.isSuffixOf "Phase" build_line = Just ("failure in " <> fromUTF8BS build_line) 826probableErrorCause _ = Nothing