oglyad-0.1.2.0: src/Oglyad/Source/Anda.hs
-- | running the real andax on update.rhai scripts oglyad can't follow on its
-- own, like ones that scrape a page with get() and find().
--
-- anda rewrites specs in place, so it runs on a throwaway copy of the tree.
-- afterwards rpmspec reads the copy's spec, and whatever Version it says now
-- is what `anda update` would have set
module Oglyad.Source.Anda
( withScratchCopy,
projectNames,
updateProject,
scriptErrors,
stripAnsi,
)
where
import Control.Exception (SomeException, finally, try)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Oglyad.Process (run, runCapture)
import Oglyad.Source.Cache (cacheDir)
import System.Directory (createDirectory, createDirectoryIfMissing, removeFile, removePathForcibly)
import System.Exit (ExitCode (..))
import System.FilePath (takeDirectory, (</>))
import System.IO (hClose, openBinaryTempFile)
-- | run an action on a copy of a tree (minus .git) that's deleted afterwards
withScratchCopy :: FilePath -> (FilePath -> IO a) -> IO (Either Text a)
withScratchCopy root act = do
base <- (</> "anda-work") <$> cacheDir
createDirectoryIfMissing True base
(tar, h) <- openBinaryTempFile base "tree.tar"
hClose h
let dir = tar <> ".d"
cleanup = quietly (removeFile tar) >> quietly (removePathForcibly dir)
( do
packed <- run "tar" ["-C", root, "--exclude=./.git", "-cf", tar, "."]
unpacked <- case packed of
Left e -> pure (Left e)
Right _ -> createDirectory dir >> run "tar" ["-xf", tar, "-C", dir]
quietly (removeFile tar)
case unpacked of
Left e -> pure (Left ("can't copy the tree for anda: " <> e))
Right _ -> Right <$> act dir
)
`finally` cleanup
where
quietly :: IO () -> IO ()
quietly a = () <$ (try a :: IO (Either SomeException ()))
-- | package directory to anda project name, from `anda list`, which prints
-- lines like "anda/apps/foo/pkg (apps/foo)"
projectNames :: FilePath -> IO (Either Text (Map FilePath Text))
projectNames copy = do
r <- runCapture copy [("NO_COLOR", "1")] "anda" ["list"]
pure $ case r of
Left e -> Left e
Right (ExitFailure _, _, err) -> Left ("anda list failed: " <> lastLine err)
Right (ExitSuccess, out, _) -> Right (Map.fromList (mapMaybe entry (T.lines (stripAnsi out))))
where
entry l = case T.breakOn " (" (T.strip l) of
(path, rest)
| Just name <- T.stripSuffix ")" (T.drop 2 rest),
not (T.null path),
not (T.null name) ->
Just (takeDirectory (T.unpack path), name)
_ -> Nothing
-- | `anda update` one project in the copy. anda reports a failing script
-- only in its log and still exits 0, so the log is where errors come from.
-- the script gets GITHUB_TOKEN, since anda refuses gh() without one
updateProject :: FilePath -> Maybe Text -> Text -> Text -> IO (Either Text ())
updateProject copy token branch name = do
let env = ("NO_COLOR", "1") : [("GITHUB_TOKEN", T.unpack t) | Just t <- [token]]
r <- runCapture copy env "anda" ["-v", "update", "--labels", "branch=" <> T.unpack branch, T.unpack name]
pure $ case r of
Left e -> Left e
Right (ExitFailure _, _, err) -> Left (failure (stripAnsi err))
Right (ExitSuccess, _, err) -> case scriptErrors (stripAnsi err) of
[] -> Right ()
e : _ -> Left e
where
-- color-eyre reports look like "Error:\n 0: Project not found: x"
failure err = maybe (lastLine err) T.strip (listToMaybe [T.drop 3 l' | l <- T.lines err, let l' = T.stripStart l, "0: " `T.isPrefixOf` l'])
-- | what went wrong in each script exception in anda's -v log: andax logs
-- "ERROR andax::run: Script Exception —— <project>" and then the reason on a
-- line starting with "├─═"
scriptErrors :: Text -> [Text]
scriptErrors = go . T.lines
where
go = \case
[] -> []
l : rest
| "Script Exception" `T.isInfixOf` l ->
case [T.strip (T.drop 1 (snd (T.breakOn "═" r))) | r <- take 5 rest, "├─═" `T.isInfixOf` r] of
msg : _ -> msg : go rest
[] -> "script exception" : go rest
| otherwise -> go rest
-- | drop ANSI color codes, which color-eyre writes even with NO_COLOR set
stripAnsi :: Text -> Text
stripAnsi t = case T.breakOn "\ESC[" t of
(pre, "") -> pre
(pre, rest) -> pre <> stripAnsi (T.drop 1 (T.dropWhile (\c -> c /= 'm' && c /= 'K') (T.drop 2 rest)))
lastLine :: Text -> Text
lastLine t = case reverse (filter (not . T.null) (map T.strip (T.lines t))) of
l : _ -> l
[] -> "(no output)"