freckle-app-1.0.2.3: library/Freckle/App/Version.hs
-- | Facilities for inferring an application version
--
-- Various inputs are checked: files written during a docker build, git
-- information, or falling back to an unknown version. This is useful for
-- Bugsnag reports, client age comparison, etc.
--
module Freckle.App.Version
( AppVersion(..)
, getAppVersion
, tryGetAppVersion
) where
import Freckle.App.Prelude
import Control.Error.Util (hoistEither, note)
import Control.Monad.Trans.Except
import Data.Char (isSpace)
import Data.List (dropWhileEnd)
import Data.Text (pack)
import qualified Data.Text as T
import Data.Time.Format (defaultTimeLocale, parseTimeM)
import System.Exit (ExitCode(..))
import System.FilePath ((</>))
import System.Process (readProcessWithExitCode)
import UnliftIO.Exception (tryIO)
data AppVersion = AppVersion
{ avName :: Text
, avCreatedAt :: UTCTime
}
deriving stock (Eq, Show)
-- | Attempt to infer an @'AppVersion'@
--
-- - If files exist under @\/app-version@ they ar read, otherwise
-- - If we're in a Git repository commit information is used, otherwise
-- - An /Unknown/ version as of the current time is returned
--
getAppVersion :: MonadUnliftIO m => m AppVersion
getAppVersion =
either (const getAppVersionUnknown) pure =<< tryGetAppVersion "/app-version"
-- | A more testable version of @'getAppVersion'@
--
-- - Reports what didn't work in @'Left'@
-- - Accepts a parent path, for file-system version information
--
tryGetAppVersion
:: MonadUnliftIO m => FilePath -> m (Either [String] AppVersion)
tryGetAppVersion parent =
runExceptT
$ withExceptT pure (getAppVersionFiles parent)
<|> withExceptT pure getAppVersionGit
getAppVersionFiles :: MonadIO m => FilePath -> ExceptT String m AppVersion
getAppVersionFiles parent = do
name <- readFileExceptT $ parent </> "name"
seconds <- readFileExceptT $ parent </> "created-at"
hoistEither $ toAppVersion name seconds
getAppVersionGit :: MonadIO m => ExceptT String m AppVersion
getAppVersionGit = do
name <- git ["rev-parse", "HEAD"]
seconds <- git ["show", "--no-patch", "--no-notes", "--pretty=%at"]
hoistEither $ toAppVersion name seconds
toAppVersion :: String -> String -> Either String AppVersion
toAppVersion name seconds = do
createdAt <- parseUnixSeconds $ strip seconds
pure AppVersion { avName = T.strip $ pack name, avCreatedAt = createdAt }
parseUnixSeconds :: String -> Either String UTCTime
parseUnixSeconds x = note err $ parseTimeM True defaultTimeLocale "%s" x
where err = x <> " does not parse as UTCTime with format %s"
getAppVersionUnknown :: MonadIO m => m AppVersion
getAppVersionUnknown = AppVersion "Unknown" <$> liftIO getCurrentTime
readFileExceptT :: MonadIO m => FilePath -> ExceptT String m String
readFileExceptT path = ExceptT $ liftIO $ first err <$> tryIO (readFile path)
where err ex = "readFile: " <> show ex
git :: MonadIO m => [String] -> ExceptT String m String
git args = do
(ec, stdout, stderr) <- exceptIO $ readProcessWithExitCode "git" args []
case ec of
ExitSuccess -> pure stdout
ExitFailure n ->
throwE $ "[" <> show n <> "] git " <> unwords args <> ": " <> stderr
exceptIO :: MonadIO m => IO a -> ExceptT String m a
exceptIO = withExceptT show . ExceptT . liftIO . tryIO
strip :: String -> String
strip = dropWhile isSpace . dropWhileEnd isSpace