releaser-0.3.0.1: src/Releaser/Primitives.hs
{-# LANGUAGE CPP #-}
module Releaser.Primitives
( -- cabal utilities
CabalInfo (..),
cabalRead,
cabalWriteVersion,
cabalBumpVersion,
cabalSdist,
cabalUpload,
cabalMakeHaddocks,
cabalUploadDocs,
-- git primitives
gitCheckout,
gitGetTags,
gitTag,
gitCommit,
gitPush,
gitPushTags,
gitAssertEmptyStaging,
-- utilities
prompt,
abort,
logStep,
changelogPrepare,
)
where
import qualified Data.ByteString as BS
import Data.Foldable (toList)
import Data.Functor (void)
import Data.List (intercalate)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Version (parseVersion)
import Distribution.PackageDescription.Parsec
import Distribution.Parsec (PWarning (..), showPError)
import Distribution.Simple.Utils (die', tryFindPackageDesc)
import Distribution.Types.GenericPackageDescription (GenericPackageDescription, packageDescription)
import Distribution.Types.PackageDescription (package)
import Distribution.Types.PackageId (pkgName, pkgVersion)
import Distribution.Types.PackageName (unPackageName)
import Distribution.Types.Version (mkVersion', versionNumbers)
import Distribution.Verbosity (Verbosity, normal, silent)
import System.Console.Pretty (Color (..), color)
import System.Environment (lookupEnv)
import System.Exit (ExitCode (..), exitFailure)
import System.IO
import System.Process
import Text.ParserCombinators.ReadP (ReadP, readP_to_S)
import Text.Regex.TDFA
import Text.Regex.TDFA.Text
logStep :: String -> IO ()
logStep str =
putStrLn $ color Green ">> " <> str
prompt :: String -> IO String
prompt str = do
putStr $ color Blue ">> " <> str
hFlush stdout
getLine
promptRetry :: String -> IO ()
promptRetry str =
void $ prompt $ str <> ". Retry? (press enter) "
abort :: String -> IO a
abort str = do
putStrLnErr $ color Red ">> " <> str
exitFailure
data CabalInfo = CabalInfo
{ name :: String,
version :: String
}
#if MIN_VERSION_Cabal(3,8,0)
readGenericPackageDescription :: Verbosity -> FilePath -> IO GenericPackageDescription
readGenericPackageDescription v p = do
bs <- BS.readFile p
case runParseResult (parseGenericPackageDescription bs) of
(_warnings, Right gpd) -> pure gpd
(_warnings, Left (v, e)) -> fail $ "Cabal file " ++ p ++ " has problems:\n" ++ unlines (map (showPError p) (toList e))
#endif
-- | Given a folder, find a Cabal file and read the package version
cabalRead :: FilePath -> IO CabalInfo
cabalRead dir = do
logStep $ "Looking for a cabal file in " <> dir
cabalFile <- tryFindPackageDesc silent dir
genericPackageDescription <- readGenericPackageDescription silent cabalFile
let pkgversion = pkgVersion $ package $ packageDescription genericPackageDescription
pkgname = pkgName $ package $ packageDescription genericPackageDescription
cabalinfo =
CabalInfo
{ version = intercalate "." $ show <$> versionNumbers pkgversion,
name = unPackageName pkgname
}
logStep $ "Found " <> name cabalinfo <> "-" <> version cabalinfo
return cabalinfo
-- | Given a folder, find a Cabal file and update the package version
cabalWriteVersion :: FilePath -> String -> IO ()
cabalWriteVersion dir versionStr = do
if validCabalVersion versionStr
then do
cabalFile <- tryFindPackageDesc silent dir
cabalinfo <- cabalRead dir
cabal <- T.readFile cabalFile
let versionPrev :: T.Text
versionPrev = cabal =~ ("version:[ \t]*" ++ version cabalinfo)
if versionPrev == ""
then abort $ "Failed to replace version in " <> cabalFile <> ", please open an issue at https://github.com/domenkozar/releaser/issues"
else do
T.writeFile cabalFile $ T.replace versionPrev ("version: " <> T.pack versionStr) cabal
logStep $ "Bumped " <> name cabalinfo <> " to " <> versionStr
else do
promptRetry "Cabal version does not match /^[0-9]+([.][0-9]+)*$/"
void $ cabalBumpVersion dir
validCabalVersion :: String -> Bool
validCabalVersion version =
version =~ ("^[0-9]+([.][0-9]+)*$" :: String)
putStrLnErr :: String -> IO ()
putStrLnErr = hPutStrLn stderr
cabalBumpVersion :: FilePath -> IO String
cabalBumpVersion dir = do
cabalinfo <- cabalRead dir
version <- prompt $ "Bump cabal version from " <> version cabalinfo <> " to: "
cabalWriteVersion dir version
return version
cabalSdist :: FilePath -> IO FilePath
cabalSdist dir = do
logStep "Running $ cabal dist"
cabalinfo <- cabalRead dir
void $ readProcess "cabal" ["v2-sdist"] mempty
let sdistTarball = "dist-newstyle/sdist/" <> name cabalinfo <> "-" <> version cabalinfo <> ".tar.gz"
logStep $ "Created " <> sdistTarball
return sdistTarball
cabalMakeHaddocks :: FilePath -> IO FilePath
cabalMakeHaddocks dir = do
logStep "Running $ cabal haddock"
cabalinfo <- cabalRead dir
void $ readProcess "cabal" ["v2-haddock", "--haddock-for-hackage"] mempty
let docsTarball = "dist-newstyle/" <> name cabalinfo <> "-" <> version cabalinfo <> "-docs.tar.gz"
logStep $ "Created " <> docsTarball
return docsTarball
cabalUploadDocs :: FilePath -> IO ()
cabalUploadDocs docsTarball = do
logStep "Running $ cabal upload -d"
-- TODO: recommend that credentials are configured via ~/cabal/config
interactiveProcess (proc "cabal" ["upload", "-d", "--publish", docsTarball]) $ \_ -> do
promptRetry "cabal upload -d"
cabalUploadDocs docsTarball
cabalUpload :: FilePath -> IO ()
cabalUpload sdistTarball = do
logStep "Running $ cabal upload"
-- TODO: recommend that credentials are configured via ~/cabal/config
interactiveProcess (proc "cabal" ["upload", "--publish", sdistTarball]) $ \_ -> do
promptRetry "cabal upload"
cabalUpload sdistTarball
gitGetTags :: IO [String]
gitGetTags = do
lines <$> readProcess "git" ["tag"] mempty
-- TODO: what can we do if previous release process terminated and branch exists?
gitCheckout :: String -> IO ()
gitCheckout tag = do
logStep $ "Running $ git checkout -b " <> tag
-- TODO: check for existing branch
interactiveProcess (proc "git" ["checkout", "-b", tag]) $ \i -> do
promptRetry "git checkout failed"
gitCheckout tag
gitTag :: String -> IO ()
gitTag tag = do
logStep $ "Running $ git tag --annotate --sign " <> tag
tags <- gitGetTags
if elem tag tags
then abort "git tag already exists, please delete it and start over"
else interactiveProcess (proc "git" ["tag", "--annotate", "--sign", tag]) $ \i -> do
promptRetry "git tag failed"
gitTag tag
gitCommit :: String -> IO ()
gitCommit message = do
logStep $ "Running $ git commit "
interactiveProcess (proc "git" ["commit", "-a", "-m", message]) $ \i -> do
promptRetry "git commit failed"
gitCommit message
gitPush :: String -> IO ()
gitPush remote = do
logStep $ "Pushing git to " <> remote
interactiveProcess (proc "git" ["push", remote, "HEAD"]) $ \i -> do
promptRetry "git push"
gitPush remote
gitPushTags :: String -> IO ()
gitPushTags remote = do
logStep $ "Pushing git tags to " <> remote
void $ readProcess "git" ["push", remote, "--tags"] mempty
gitAssertEmptyStaging :: IO ()
gitAssertEmptyStaging = do
logStep "Assserting there are no uncommitted files"
output <- readProcess "git" ["status", "--untracked-files=no", "--porcelain"] mempty
if output == ""
then return ()
else abort "git status is not clean"
changelogPrepare :: IO ()
changelogPrepare = do
logStep "Assserting there are no uncommitted files"
editorEnv <- lookupEnv "EDITOR"
case editorEnv of
Nothing -> abort "please make sure $EDITOR is set"
Just editor -> do
-- TODO: prepare the changelog
interactiveProcess (proc editor ["CHANGELOG.md"]) $ \i -> do
logStep $ editor <> " failed with " <> show i <> ", retrying"
changelogPrepare
-- internal
interactiveProcess :: CreateProcess -> (Int -> IO ()) -> IO ()
interactiveProcess cmd bad = do
(_, _, _, ph) <- createProcess cmd
exitcode <- waitForProcess ph
case exitcode of
ExitSuccess -> return ()
ExitFailure i -> bad i