Merge pull request #174 from stackbuilders/keep-failed-releases

Added `keep_one_failed` option that deletes failed releases except for the latest one
This commit is contained in:
David Mazarro 2022-02-08 16:48:45 +01:00 committed by GitHub
commit 2bfde28d69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 396 additions and 245 deletions

View File

@ -108,6 +108,10 @@ The following parameters are *optional*:
'--keep-releases' argument passed via the CLI takes precedence over this '--keep-releases' argument passed via the CLI takes precedence over this
value. If neither CLI nor configuration file value is specified, it defaults value. If neither CLI nor configuration file value is specified, it defaults
to '5' to '5'
* `keep_one_failed` - A boolean specifying whether to keep all failed releases
or just one (the latest failed release), the '--keep-one-failed' flag passed via
the CLI takes precedence over this value. If neither CLI nor configuration file value is specified,
it defaults to false (i.e. keep all failed releases).
* `linked_files:`- Listed files that will be symlinked from the `{deploy_path}/shared` folder * `linked_files:`- Listed files that will be symlinked from the `{deploy_path}/shared` folder
into each release directory during deployment. Can be used for configuration files into each release directory during deployment. Can be used for configuration files
that need to be persisted (e.g. dotenv files). **NOTE:** The directory structure _must_ that need to be persisted (e.g. dotenv files). **NOTE:** The directory structure _must_

View File

@ -8,7 +8,6 @@ module Main (main) where
import Control.Concurrent.Async import Control.Concurrent.Async
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad import Control.Monad
#if !MIN_VERSION_base(4,13,0) #if !MIN_VERSION_base(4,13,0)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
#endif #endif
@ -16,7 +15,6 @@ import Data.Version (showVersion)
import qualified Data.Yaml.Config as Yaml import qualified Data.Yaml.Config as Yaml
import Development.GitRev import Development.GitRev
import Formatting (formatToString, string, (%)) import Formatting (formatToString, string, (%))
import Numeric.Natural
import Options.Applicative hiding (str) import Options.Applicative hiding (str)
import Path import Path
import Path.IO import Path.IO
@ -28,23 +26,10 @@ import qualified System.Hapistrano.Config as C
import qualified System.Hapistrano.Core as Hap import qualified System.Hapistrano.Core as Hap
import System.Hapistrano.Types import System.Hapistrano.Types
import System.IO import System.IO
import System.Hapistrano (createHapistranoDeployState)
import Control.Monad.Error.Class (throwError, catchError)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- Command line options
-- | Command line options.
data Opts = Opts
{ optsCommand :: Command
, optsConfigFile :: FilePath
}
-- | Command to execute and command-specific options.
data Command
= Deploy (Maybe ReleaseFormat) (Maybe Natural) -- ^ Deploy a new release (with timestamp
-- format and how many releases to keep)
| Rollback Natural -- ^ Rollback to Nth previous release
parserInfo :: ParserInfo Opts parserInfo :: ParserInfo Opts
parserInfo = parserInfo =
@ -95,6 +80,10 @@ deployParser = Deploy
<> help "How many releases to keep, default is '5'" <> help "How many releases to keep, default is '5'"
) )
) )
<*> switch
( long "keep-one-failed"
<> help "Keep all failed releases or just one -the latest-, default (without using this flag) is to keep all failed releases."
)
rollbackParser :: Parser Command rollbackParser :: Parser Command
rollbackParser = Rollback rollbackParser = Rollback
@ -125,7 +114,7 @@ data Message
main :: IO () main :: IO ()
main = do main = do
Opts {..} <- execParser parserInfo Opts{..} <- execParser parserInfo
C.Config{..} <- Yaml.loadYamlSettings [optsConfigFile] [] Yaml.useEnv C.Config{..} <- Yaml.loadYamlSettings [optsConfigFile] [] Yaml.useEnv
chan <- newTChanIO chan <- newTChanIO
let task rf = Task { taskDeployPath = configDeployPath let task rf = Task { taskDeployPath = configDeployPath
@ -133,43 +122,55 @@ main = do
, taskReleaseFormat = rf } , taskReleaseFormat = rf }
let printFnc dest str = atomically $ let printFnc dest str = atomically $
writeTChan chan (PrintMsg dest str) writeTChan chan (PrintMsg dest str)
hap shell sshOpts = do hap shell sshOpts = do
r <- Hap.runHapistrano sshOpts shell printFnc $ r <- Hap.runHapistrano sshOpts shell printFnc $
case optsCommand of case optsCommand of
Deploy cliReleaseFormat cliKeepReleases -> do Deploy cliReleaseFormat cliKeepReleases cliKeepOneFailed ->
let releaseFormat = fromMaybeReleaseFormat cliReleaseFormat configReleaseFormat let releaseFormat = fromMaybeReleaseFormat cliReleaseFormat configReleaseFormat
keepReleases = fromMaybeKeepReleases cliKeepReleases configKeepReleases keepReleases = fromMaybeKeepReleases cliKeepReleases configKeepReleases
forM_ configRunLocally Hap.playScriptLocally keepOneFailed = cliKeepOneFailed || configKeepOneFailed
release <- if configVcAction -- We define the handler for when an exception happens inside a deployment
then Hap.pushRelease (task releaseFormat) failStateAndThrow e@(_, maybeRelease) = do
else Hap.pushReleaseWithoutVc (task releaseFormat) case maybeRelease of
rpath <- Hap.releasePath configDeployPath release configWorkingDir (Just release) -> do
forM_ (toMaybePath configSource) $ \src -> createHapistranoDeployState configDeployPath release Fail
Hap.scpDir src rpath Hap.dropOldReleases configDeployPath keepReleases keepOneFailed
forM_ configCopyFiles $ \(C.CopyThing src dest) -> do throwError e
srcPath <- resolveFile' src Nothing -> do
destPath <- parseRelFile dest throwError e
let dpath = rpath </> destPath in do
(Hap.exec . Hap.MkDir . parent) dpath forM_ configRunLocally Hap.playScriptLocally
Hap.scpFile srcPath dpath release <- if configVcAction
forM_ configCopyDirs $ \(C.CopyThing src dest) -> do then Hap.pushRelease (task releaseFormat)
srcPath <- resolveDir' src else Hap.pushReleaseWithoutVc (task releaseFormat)
destPath <- parseRelDir dest rpath <- Hap.releasePath configDeployPath release configWorkingDir
let dpath = rpath </> destPath forM_ (toMaybePath configSource) $ \src ->
(Hap.exec . Hap.MkDir . parent) dpath Hap.scpDir src rpath (Just release)
Hap.scpDir srcPath dpath forM_ configCopyFiles $ \(C.CopyThing src dest) -> do
forM_ configLinkedFiles srcPath <- resolveFile' src
(Hap.linkToShared configTargetSystem rpath configDeployPath) destPath <- parseRelFile dest
forM_ configLinkedDirs let dpath = rpath </> destPath
(Hap.linkToShared configTargetSystem rpath configDeployPath) (flip Hap.exec (Just release) . Hap.MkDir . parent) dpath
forM_ configBuildScript (Hap.playScript configDeployPath release configWorkingDir) Hap.scpFile srcPath dpath (Just release)
Hap.registerReleaseAsComplete configDeployPath release forM_ configCopyDirs $ \(C.CopyThing src dest) -> do
Hap.activateRelease configTargetSystem configDeployPath release srcPath <- resolveDir' src
Hap.dropOldReleases configDeployPath keepReleases destPath <- parseRelDir dest
forM_ configRestartCommand Hap.exec let dpath = rpath </> destPath
(flip Hap.exec (Just release) . Hap.MkDir . parent) dpath
Hap.scpDir srcPath dpath (Just release)
forM_ configLinkedFiles
$ flip (Hap.linkToShared configTargetSystem rpath configDeployPath) (Just release)
forM_ configLinkedDirs
$ flip (Hap.linkToShared configTargetSystem rpath configDeployPath) (Just release)
forM_ configBuildScript (Hap.playScript configDeployPath release configWorkingDir)
Hap.activateRelease configTargetSystem configDeployPath release
forM_ configRestartCommand (flip Hap.exec $ Just release)
Hap.createHapistranoDeployState configDeployPath release System.Hapistrano.Types.Success
Hap.dropOldReleases configDeployPath keepReleases keepOneFailed
`catchError` failStateAndThrow
Rollback n -> do Rollback n -> do
Hap.rollback configTargetSystem configDeployPath n Hap.rollback configTargetSystem configDeployPath n
forM_ configRestartCommand Hap.exec forM_ configRestartCommand (flip Hap.exec Nothing)
atomically (writeTChan chan FinishMsg) atomically (writeTChan chan FinishMsg)
return r return r
printer :: Int -> IO () printer :: Int -> IO ()
@ -187,7 +188,7 @@ main = do
case configHosts of case configHosts of
[] -> [hap Bash Nothing] -- localhost, no SSH [] -> [hap Bash Nothing] -- localhost, no SSH
xs -> xs ->
let runHap (C.Target{..}) = let runHap C.Target{..} =
hap targetShell (Just $ SshOptions targetHost targetPort targetSshArgs) hap targetShell (Just $ SshOptions targetHost targetPort targetSshArgs)
in runHap <$> xs in runHap <$> xs
results <- (runConcurrently . traverse Concurrently) results <- (runConcurrently . traverse Concurrently)

View File

@ -5,7 +5,7 @@ description:
This is an example project that has been created in order to test This is an example project that has been created in order to test
the deployment process using the working_dir feature of hapistrano. the deployment process using the working_dir feature of hapistrano.
author: Justin Leitgeb author: Justin Leitgeb
maintainer: jpaucar@stackbuilders.com maintainer: cmotoche@stackbuilders.com
copyright: 2015-Present Stack Builders Inc. copyright: 2015-Present Stack Builders Inc.
license: MIT license: MIT
license-file: ../LICENSE license-file: ../LICENSE

View File

@ -21,7 +21,7 @@ description:
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Justin Leitgeb author: Justin Leitgeb
maintainer: jpaucar@stackbuilders.com maintainer: cmotoche@stackbuilders.com
copyright: 2015-Present Stack Builders Inc. copyright: 2015-Present Stack Builders Inc.
category: System category: System
homepage: https://github.com/stackbuilders/hapistrano homepage: https://github.com/stackbuilders/hapistrano
@ -82,6 +82,7 @@ executable hap
, formatting >= 6.2 && < 8.0 , formatting >= 6.2 && < 8.0
, gitrev >= 1.2 && < 1.4 , gitrev >= 1.2 && < 1.4
, hapistrano , hapistrano
, mtl >= 2.0 && < 3.0
, optparse-applicative >= 0.11 && < 0.17 , optparse-applicative >= 0.11 && < 0.17
, path >= 0.5 && < 0.9 , path >= 0.5 && < 0.9
, path-io >= 1.2 && < 1.7 , path-io >= 1.2 && < 1.7

View File

@ -62,5 +62,6 @@ defaultConfiguration =
, configTargetSystem = GNULinux , configTargetSystem = GNULinux
, configReleaseFormat = Nothing , configReleaseFormat = Nothing
, configKeepReleases = Nothing , configKeepReleases = Nothing
, configKeepOneFailed = False
, configWorkingDir = Nothing , configWorkingDir = Nothing
} }

View File

@ -1,5 +1,6 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module System.HapistranoSpec module System.HapistranoSpec
( spec ( spec
@ -11,9 +12,9 @@ import Data.List (isPrefixOf)
import Data.Maybe (mapMaybe) import Data.Maybe (mapMaybe)
import Numeric.Natural import Numeric.Natural
import Path import Path
#if !MIN_VERSION_base(4,13,0)
import Path.Internal (Path(..))
#endif
import Path.IO import Path.IO
import System.Directory (getCurrentDirectory, listDirectory) import System.Directory (getCurrentDirectory, listDirectory)
@ -27,7 +28,9 @@ import System.Info (os)
import Test.Hspec hiding (shouldBe, shouldReturn) import Test.Hspec hiding (shouldBe, shouldReturn)
import qualified Test.Hspec as Hspec import qualified Test.Hspec as Hspec
import Test.Hspec.QuickCheck import Test.Hspec.QuickCheck
import Test.QuickCheck import Test.QuickCheck hiding (Success)
import System.Hapistrano (releasePath)
import System.Hapistrano.Config (deployStateFilename)
testBranchName :: String testBranchName :: String
testBranchName = "another_branch" testBranchName = "another_branch"
@ -46,7 +49,7 @@ spec = do
let (Just commandTest) = let (Just commandTest) =
Hap.mkGenericCommand Hap.mkGenericCommand
"echo \"hapistrano\"; sleep 2; echo \"onartsipah\"" "echo \"hapistrano\"; sleep 2; echo \"onartsipah\""
commandExecution = Hap.execWithInheritStdout commandTest commandExecution = Hap.execWithInheritStdout commandTest Nothing
expectedOutput = "hapistrano\nonartsipah" expectedOutput = "hapistrano\nonartsipah"
in do actualOutput <- capture_ (runHap commandExecution) in do actualOutput <- capture_ (runHap commandExecution)
expectedOutput `Hspec.shouldSatisfy` (`isPrefixOf` actualOutput) expectedOutput `Hspec.shouldSatisfy` (`isPrefixOf` actualOutput)
@ -154,14 +157,25 @@ spec = do
("test `git rev-parse --abbrev-ref HEAD` = " ++ testBranchName) ("test `git rev-parse --abbrev-ref HEAD` = " ++ testBranchName)
-- This fails if there are unstaged changes -- This fails if there are unstaged changes
justExec rpath "git diff --exit-code" justExec rpath "git diff --exit-code"
describe "registerReleaseAsComplete" $ describe "createHapistranoDeployState" $ do
it "creates the token all right" $ \(deployPath, repoPath) -> it ("creates the " <> deployStateFilename <> " file correctly") $ \(deployPath, repoPath) ->
runHap $ do runHap $ do
let task = mkTask deployPath repoPath let task = mkTask deployPath repoPath
release <- Hap.pushRelease task release <- Hap.pushRelease task
Hap.registerReleaseAsComplete deployPath release parseStatePath <- parseRelFile deployStateFilename
(Hap.ctokenPath deployPath release >>= doesFileExist) `shouldReturn` actualReleasePath <- releasePath deployPath release Nothing
let stateFilePath = actualReleasePath </> parseStatePath
Hap.createHapistranoDeployState deployPath release Success
Path.IO.doesFileExist stateFilePath `shouldReturn`
True True
it "when created in a successful deploy, the contents are \"Success\"" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
Hap.createHapistranoDeployState deployPath release Success
Hap.deployState deployPath Nothing release `shouldReturn`
Success
describe "activateRelease" $ describe "activateRelease" $
it "creates the current symlink correctly" $ \(deployPath, repoPath) -> it "creates the current symlink correctly" $ \(deployPath, repoPath) ->
runHap $ do runHap $ do
@ -172,8 +186,8 @@ spec = do
let rc :: Hap.Readlink Dir let rc :: Hap.Readlink Dir
rc = rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath) Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath Hap.exec rc (Just release) `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False Path.IO.doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
describe "playScriptLocally (successful run)" $ describe "playScriptLocally (successful run)" $
it "check that local scripts are run and deployment is successful" $ \(deployPath, repoPath) -> it "check that local scripts are run and deployment is successful" $ \(deployPath, repoPath) ->
runHap $ do runHap $ do
@ -181,8 +195,11 @@ spec = do
task = mkTask deployPath repoPath task = mkTask deployPath repoPath
Hap.playScriptLocally localCommands Hap.playScriptLocally localCommands
release <- Hap.pushRelease task release <- Hap.pushRelease task
Hap.registerReleaseAsComplete deployPath release parseStatePath <- parseRelFile deployStateFilename
(Hap.ctokenPath deployPath release >>= doesFileExist) `shouldReturn` actualReleasePath <- releasePath deployPath release Nothing
let stateFilePath = actualReleasePath </> parseStatePath
Hap.createHapistranoDeployState deployPath release Success
Path.IO.doesFileExist stateFilePath `shouldReturn`
True True
describe "playScriptLocally (error exit)" $ describe "playScriptLocally (error exit)" $
it "check that deployment isn't done" $ \(deployPath, repoPath) -> it "check that deployment isn't done" $ \(deployPath, repoPath) ->
@ -192,55 +209,51 @@ spec = do
task = mkTask deployPath repoPath task = mkTask deployPath repoPath
Hap.playScriptLocally localCommands Hap.playScriptLocally localCommands
release <- Hap.pushRelease task release <- Hap.pushRelease task
Hap.registerReleaseAsComplete deployPath release) `shouldThrow` Hap.createHapistranoDeployState deployPath release Success) `shouldThrow`
anyException anyException
describe "rollback" $ do describe "rollback" $ do
context "without completion tokens" $ it "resets the current symlink correctly" $ \(deployPath, repoPath) ->
it "resets the current symlink correctly" $ \(deployPath, repoPath) -> runHap $ do
runHap $ do let task = mkTask deployPath repoPath
let task = mkTask deployPath repoPath rs <- replicateM 5 (Hap.pushRelease task)
rs <- replicateM 5 (Hap.pushRelease task) Hap.rollback currentSystem deployPath 2
Hap.rollback currentSystem deployPath 2 rpath <- Hap.releasePath deployPath (rs !! 2) Nothing
rpath <- Hap.releasePath deployPath (rs !! 2) Nothing let rc :: Hap.Readlink Dir
let rc :: Hap.Readlink Dir rc =
rc = Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath) Hap.exec rc Nothing `shouldReturn` rpath
Hap.exec rc `shouldReturn` rpath Path.IO.doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False describe "dropOldReleases" $ do
context "with completion tokens" $
it "resets the current symlink correctly" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
rs <- replicateM 5 (Hap.pushRelease task)
forM_ (take 3 rs) (Hap.registerReleaseAsComplete deployPath)
Hap.rollback currentSystem deployPath 2
rpath <- Hap.releasePath deployPath (rs !! 0) Nothing
let rc :: Hap.Readlink Dir
rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
describe "dropOldReleases" $
it "works" $ \(deployPath, repoPath) -> it "works" $ \(deployPath, repoPath) ->
runHap $ do runHap $ do
rs <- rs <-
replicateM 7 $ do replicateM 7 $ do
r <- Hap.pushRelease (mkTask deployPath repoPath) r <- Hap.pushRelease (mkTask deployPath repoPath)
Hap.registerReleaseAsComplete deployPath r Hap.createHapistranoDeployState deployPath r Success
return r return r
Hap.dropOldReleases deployPath 5 Hap.dropOldReleases deployPath 5 False
-- two oldest releases should not survive: -- two oldest releases should not survive:
forM_ (take 2 rs) $ \r -> forM_ (take 2 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` False (Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` False
-- 5 most recent releases should stay alive: -- 5 most recent releases should stay alive:
forM_ (drop 2 rs) $ \r -> forM_ (drop 2 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True (Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True
-- two oldest completion tokens should not survive: context "when the --keep-one-failed flag is active" $
forM_ (take 2 rs) $ \r -> it "should delete failed releases other than the most recent" $ \(deployPath, repoPath) ->
(Hap.ctokenPath deployPath r >>= doesFileExist) `shouldReturn` False let successfulRelease = mkReleaseWithState deployPath repoPath Success
-- 5 most recent completion tokens should stay alive: failedRelease = mkReleaseWithState deployPath repoPath Fail in
forM_ (drop 2 rs) $ \r -> runHap $ do
(Hap.ctokenPath deployPath r >>= doesFileExist) `shouldReturn` True rs <- sequence [successfulRelease, successfulRelease, failedRelease, failedRelease, failedRelease]
Hap.dropOldReleases deployPath 5 True
-- The two successful releases should survive
forM_ (take 2 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True
-- The latest failed release should survive:
forM_ (drop 4 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True
-- The two older failed releases should not survive:
forM_ (take 2 . drop 2 $ rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` False
describe "linkToShared" $ do describe "linkToShared" $ do
context "when the deploy_path/shared directory doesn't exist" $ context "when the deploy_path/shared directory doesn't exist" $
it "should create the link anyway" $ \(deployPath, repoPath) -> it "should create the link anyway" $ \(deployPath, repoPath) ->
@ -249,8 +262,8 @@ spec = do
sharedDir = Hap.sharedPath deployPath sharedDir = Hap.sharedPath deployPath
release <- Hap.pushRelease task release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing rpath <- Hap.releasePath deployPath release Nothing
Hap.exec $ Hap.Rm sharedDir Hap.exec (Hap.Rm sharedDir) (Just release)
Hap.linkToShared currentSystem rpath deployPath "thing" `shouldReturn` Hap.linkToShared currentSystem rpath deployPath "thing" (Just release) `shouldReturn`
() ()
context "when the file/directory to link exists in the repository" $ context "when the file/directory to link exists in the repository" $
it "should throw an error" $ \(deployPath, repoPath) -> it "should throw an error" $ \(deployPath, repoPath) ->
@ -258,7 +271,7 @@ spec = do
(do let task = mkTask deployPath repoPath (do let task = mkTask deployPath repoPath
release <- Hap.pushRelease task release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing rpath <- Hap.releasePath deployPath release Nothing
Hap.linkToShared currentSystem rpath deployPath "foo.txt") `shouldThrow` Hap.linkToShared currentSystem rpath deployPath "foo.txt" $ Just release) `shouldThrow`
anyException anyException
context "when it attempts to link a file" $ do context "when it attempts to link a file" $ do
context "when the file is not at the root of the shared directory" $ context "when the file is not at the root of the shared directory" $
@ -270,7 +283,7 @@ spec = do
rpath <- Hap.releasePath deployPath release Nothing rpath <- Hap.releasePath deployPath release Nothing
justExec sharedDir "mkdir foo/" justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt" justExec sharedDir "echo 'Bar!' > foo/bar.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/bar.txt") `shouldThrow` Hap.linkToShared currentSystem rpath deployPath "foo/bar.txt" $ Just release) `shouldThrow`
anyException anyException
context "when the file is at the root of the shared directory" $ context "when the file is at the root of the shared directory" $
it "should link the file successfully" $ \(deployPath, repoPath) -> it "should link the file successfully" $ \(deployPath, repoPath) ->
@ -280,7 +293,7 @@ spec = do
release <- Hap.pushRelease task release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing rpath <- Hap.releasePath deployPath release Nothing
justExec sharedDir "echo 'Bar!' > bar.txt" justExec sharedDir "echo 'Bar!' > bar.txt"
Hap.linkToShared currentSystem rpath deployPath "bar.txt" Hap.linkToShared currentSystem rpath deployPath "bar.txt" (Just release)
(liftIO . readFile . fromAbsFile) (liftIO . readFile . fromAbsFile)
(rpath </> $(mkRelFile "bar.txt")) `shouldReturn` (rpath </> $(mkRelFile "bar.txt")) `shouldReturn`
"Bar!\n" "Bar!\n"
@ -295,7 +308,7 @@ spec = do
justExec sharedDir "mkdir foo/" justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt" justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt" justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/") `shouldThrow` Hap.linkToShared currentSystem rpath deployPath "foo/" $ Just release) `shouldThrow`
anyException anyException
it "should link the file successfully" $ \(deployPath, repoPath) -> it "should link the file successfully" $ \(deployPath, repoPath) ->
runHap $ do runHap $ do
@ -306,7 +319,7 @@ spec = do
justExec sharedDir "mkdir foo/" justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt" justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt" justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo" Hap.linkToShared currentSystem rpath deployPath "foo" (Just release)
files <- files <-
(liftIO . listDirectory . fromAbsDir) (liftIO . listDirectory . fromAbsDir)
(rpath </> $(mkRelDir "foo")) (rpath </> $(mkRelDir "foo"))
@ -359,8 +372,8 @@ populateTestRepo path =
justExec :: Path Abs Dir -> String -> Hapistrano () justExec :: Path Abs Dir -> String -> Hapistrano ()
justExec path cmd' = justExec path cmd' =
case Hap.mkGenericCommand cmd' of case Hap.mkGenericCommand cmd' of
Nothing -> Hap.failWith 1 (Just $ "Failed to parse the command: " ++ cmd') Nothing -> Hap.failWith 1 (Just $ "Failed to parse the command: " ++ cmd') Nothing
Just cmd -> Hap.exec (Hap.Cd path cmd) Just cmd -> Hap.exec (Hap.Cd path cmd) Nothing
-- | Run 'Hapistrano' monad locally. -- | Run 'Hapistrano' monad locally.
runHap :: Hapistrano a -> IO a runHap :: Hapistrano a -> IO a
@ -398,6 +411,14 @@ mkTaskWithCustomRevision deployPath repoPath revision =
, taskReleaseFormat = ReleaseLong , taskReleaseFormat = ReleaseLong
} }
-- | Creates a release tagged with 'Success' or 'Fail'
mkReleaseWithState :: Path Abs Dir -> Path Abs Dir -> DeployState -> Hapistrano Release
mkReleaseWithState deployPath repoPath state = do
r <- Hap.pushRelease (mkTask deployPath repoPath)
Hap.createHapistranoDeployState deployPath r state
return r
currentSystem :: TargetSystem currentSystem :: TargetSystem
currentSystem = currentSystem =
if os == "linux" if os == "linux"

View File

@ -15,11 +15,12 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module System.Hapistrano module System.Hapistrano
( pushRelease ( runHapistrano
, pushRelease
, pushReleaseWithoutVc , pushReleaseWithoutVc
, registerReleaseAsComplete
, activateRelease , activateRelease
, linkToShared , linkToShared
, createHapistranoDeployState
, rollback , rollback
, dropOldReleases , dropOldReleases
, playScript , playScript
@ -29,14 +30,14 @@ module System.Hapistrano
, sharedPath , sharedPath
, currentSymlinkPath , currentSymlinkPath
, tempSymlinkPath , tempSymlinkPath
, ctokenPath ) , deployState )
where where
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.Reader (local) import Control.Monad.Reader (local, runReaderT)
import Data.List (dropWhileEnd, genericDrop, sortOn) import Data.List (dropWhileEnd, genericDrop, sortOn)
import Data.Maybe (mapMaybe) import Data.Maybe (mapMaybe, fromMaybe)
import Data.Ord (Down (..)) import Data.Ord (Down (..))
import Data.Time import Data.Time
import Numeric.Natural import Numeric.Natural
@ -44,8 +45,35 @@ import Path
import System.Hapistrano.Commands import System.Hapistrano.Commands
import System.Hapistrano.Core import System.Hapistrano.Core
import System.Hapistrano.Types import System.Hapistrano.Types
import System.Hapistrano.Config (deployStateFilename)
import Text.Read (readMaybe)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- | Run the 'Hapistrano' monad. The monad hosts 'exec' actions.
runHapistrano ::
MonadIO m
=> Maybe SshOptions -- ^ SSH options to use or 'Nothing' if we run locally
-> Shell -- ^ Shell to run commands
-> (OutputDest -> String -> IO ()) -- ^ How to print messages
-> Hapistrano a -- ^ The computation to run
-> m (Either Int a) -- ^ Status code in 'Left' on failure, result in
-- 'Right' on success
runHapistrano sshOptions shell' printFnc m =
liftIO $ do
let config =
Config
{ configSshOptions = sshOptions
, configShellOptions = shell'
, configPrint = printFnc
}
r <- runReaderT (runExceptT m) config
case r of
Left (Failure n msg, _) -> do
forM_ msg (printFnc StderrDest)
return (Left n)
Right x -> return (Right x)
-- High-level functionality -- High-level functionality
-- | Perform basic setup for a project, making sure necessary directories -- | Perform basic setup for a project, making sure necessary directories
@ -60,7 +88,7 @@ pushRelease Task {..} = do
-- When the configuration is set for a local directory, it will only create -- When the configuration is set for a local directory, it will only create
-- the release directory without any version control operations. -- the release directory without any version control operations.
pushReleaseForRepository GitRepository {..} = do pushReleaseForRepository GitRepository {..} = do
ensureCacheInPlace gitRepositoryURL taskDeployPath ensureCacheInPlace gitRepositoryURL taskDeployPath Nothing
release <- newRelease taskReleaseFormat release <- newRelease taskReleaseFormat
cloneToRelease taskDeployPath release cloneToRelease taskDeployPath release
setReleaseRevision taskDeployPath release gitRepositoryRevision setReleaseRevision taskDeployPath release gitRepositoryRevision
@ -76,17 +104,6 @@ pushReleaseWithoutVc Task {..} = do
setupDirs taskDeployPath setupDirs taskDeployPath
newRelease taskReleaseFormat newRelease taskReleaseFormat
-- | Create a file-token that will tell rollback function that this release
-- should be considered successfully compiled\/completed.
registerReleaseAsComplete
:: Path Abs Dir -- ^ Deploy path
-> Release -- ^ Release identifier to activate
-> Hapistrano ()
registerReleaseAsComplete deployPath release = do
cpath <- ctokenPath deployPath release
exec (Touch cpath)
-- | Switch the current symlink to point to the specified release. May be -- | Switch the current symlink to point to the specified release. May be
-- used in deploy or rollback cases. -- used in deploy or rollback cases.
@ -99,8 +116,23 @@ activateRelease ts deployPath release = do
rpath <- releasePath deployPath release Nothing rpath <- releasePath deployPath release Nothing
let tpath = tempSymlinkPath deployPath let tpath = tempSymlinkPath deployPath
cpath = currentSymlinkPath deployPath cpath = currentSymlinkPath deployPath
exec (Ln ts rpath tpath) -- create a symlink for the new candidate exec (Ln ts rpath tpath) (Just release) -- create a symlink for the new candidate
exec (Mv ts tpath cpath) -- atomically replace the symlink exec (Mv ts tpath cpath) (Just release) -- atomically replace the symlink
-- | Creates the file @.hapistrano__state@ containing
-- @fail@ or @success@ depending on how the deployment ended.
createHapistranoDeployState
:: Path Abs Dir -- ^ Deploy path
-> Release -- ^ Release being deployed
-> DeployState -- ^ Indicates how the deployment went
-> Hapistrano ()
createHapistranoDeployState deployPath release state = do
parseStatePath <- parseRelFile deployStateFilename
actualReleasePath <- releasePath deployPath release Nothing
let stateFilePath = actualReleasePath </> parseStatePath
exec (Touch stateFilePath) (Just release) -- creates '.hapistrano_deploy_state'
exec (BasicWrite stateFilePath $ show state) (Just release) -- writes the deploy state to '.hapistrano_deploy_state'
-- | Activates one of already deployed releases. -- | Activates one of already deployed releases.
@ -110,14 +142,9 @@ rollback
-> Natural -- ^ How many releases back to go, 0 re-activates current -> Natural -- ^ How many releases back to go, 0 re-activates current
-> Hapistrano () -> Hapistrano ()
rollback ts deployPath n = do rollback ts deployPath n = do
crs <- completedReleases deployPath releases <- releasesWithState Success deployPath
drs <- deployedReleases deployPath case genericDrop n releases of
-- NOTE If we don't have any completed releases, then perhaps the [] -> failWith 1 (Just "Could not find the requested release to rollback to.") Nothing
-- application was used with older versions of Hapistrano that did not
-- have this functionality. We then fall back and use collection of “just”
-- deployed releases.
case genericDrop n (if null crs then drs else crs) of
[] -> failWith 1 (Just "Could not find the requested release to rollback to.")
(x:_) -> activateRelease ts deployPath x (x:_) -> activateRelease ts deployPath x
-- | Remove older releases to avoid filling up the target host filesystem. -- | Remove older releases to avoid filling up the target host filesystem.
@ -125,16 +152,19 @@ rollback ts deployPath n = do
dropOldReleases dropOldReleases
:: Path Abs Dir -- ^ Deploy path :: Path Abs Dir -- ^ Deploy path
-> Natural -- ^ How many releases to keep -> Natural -- ^ How many releases to keep
-> Hapistrano () -- ^ Deleted Releases -> Bool -- ^ Whether the @--keep-one-failed@ flag is present or not
dropOldReleases deployPath n = do -> Hapistrano ()
dropOldReleases deployPath n keepOneFailed = do
failedReleases <- releasesWithState Fail deployPath
when (keepOneFailed && length failedReleases > 1) $
-- Remove every failed release except the most recent one
forM_ (tail failedReleases) $ \release -> do
rpath <- releasePath deployPath release Nothing
exec (Rm rpath) Nothing
dreleases <- deployedReleases deployPath dreleases <- deployedReleases deployPath
forM_ (genericDrop n dreleases) $ \release -> do forM_ (genericDrop n dreleases) $ \release -> do
rpath <- releasePath deployPath release Nothing rpath <- releasePath deployPath release Nothing
exec (Rm rpath) exec (Rm rpath) Nothing
creleases <- completedReleases deployPath
forM_ (genericDrop n creleases) $ \release -> do
cpath <- ctokenPath deployPath release
exec (Rm cpath)
-- | Play the given script switching to directory of given release. -- | Play the given script switching to directory of given release.
@ -146,18 +176,18 @@ playScript
-> Hapistrano () -> Hapistrano ()
playScript deployDir release mWorkingDir cmds = do playScript deployDir release mWorkingDir cmds = do
rpath <- releasePath deployDir release mWorkingDir rpath <- releasePath deployDir release mWorkingDir
forM_ cmds (execWithInheritStdout . Cd rpath) forM_ cmds (flip execWithInheritStdout (Just release) . Cd rpath)
-- | Plays the given script on your machine locally. -- | Plays the given script on your machine locally.
playScriptLocally :: [GenericCommand] -> Hapistrano () playScriptLocally :: [GenericCommand] -> Hapistrano ()
playScriptLocally cmds = playScriptLocally cmds =
local local
(\c -> (\c ->
c c
{ configSshOptions = Nothing { configSshOptions = Nothing
}) $ }) $
forM_ cmds execWithInheritStdout forM_ cmds $ flip execWithInheritStdout Nothing
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- Helpers -- Helpers
@ -168,10 +198,9 @@ setupDirs
:: Path Abs Dir -- ^ Deploy path :: Path Abs Dir -- ^ Deploy path
-> Hapistrano () -> Hapistrano ()
setupDirs deployPath = do setupDirs deployPath = do
(exec . MkDir . releasesPath) deployPath (flip exec Nothing . MkDir . releasesPath) deployPath
(exec . MkDir . cacheRepoPath) deployPath (flip exec Nothing . MkDir . cacheRepoPath) deployPath
(exec . MkDir . ctokensPath) deployPath (flip exec Nothing . MkDir . sharedPath) deployPath
(exec . MkDir . sharedPath) deployPath
-- | Ensure that the specified repo is cloned and checked out on the given -- | Ensure that the specified repo is cloned and checked out on the given
-- revision. Idempotent. -- revision. Idempotent.
@ -179,17 +208,18 @@ setupDirs deployPath = do
ensureCacheInPlace ensureCacheInPlace
:: String -- ^ Repo URL :: String -- ^ Repo URL
-> Path Abs Dir -- ^ Deploy path -> Path Abs Dir -- ^ Deploy path
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano () -> Hapistrano ()
ensureCacheInPlace repo deployPath = do ensureCacheInPlace repo deployPath maybeRelease = do
let cpath = cacheRepoPath deployPath let cpath = cacheRepoPath deployPath
refs = cpath </> $(mkRelDir "refs") refs = cpath </> $(mkRelDir "refs")
exists <- (exec (Ls refs) >> return True) exists <- (exec (Ls refs) Nothing >> return True)
`catchError` const (return False) `catchError` const (return False)
unless exists $ unless exists $
exec (GitClone True (Left repo) cpath) exec (GitClone True (Left repo) cpath) maybeRelease
exec (Cd cpath (GitFetch "origin")) -- TODO store this in task description? exec (Cd cpath (GitFetch "origin")) maybeRelease -- TODO store this in task description?
-- | Create a new realese identifier based on current timestamp. -- | Create a new release identifier based on current timestamp.
newRelease :: ReleaseFormat -> Hapistrano Release newRelease :: ReleaseFormat -> Hapistrano Release
newRelease releaseFormat = newRelease releaseFormat =
@ -204,7 +234,7 @@ cloneToRelease
cloneToRelease deployPath release = do cloneToRelease deployPath release = do
rpath <- releasePath deployPath release Nothing rpath <- releasePath deployPath release Nothing
let cpath = cacheRepoPath deployPath let cpath = cacheRepoPath deployPath
exec (GitClone False (Right cpath) rpath) exec (GitClone False (Right cpath) rpath) (Just release)
-- | Set the release to the correct revision by checking out a branch or -- | Set the release to the correct revision by checking out a branch or
-- a commit. -- a commit.
@ -216,7 +246,7 @@ setReleaseRevision
-> Hapistrano () -> Hapistrano ()
setReleaseRevision deployPath release revision = do setReleaseRevision deployPath release revision = do
rpath <- releasePath deployPath release Nothing rpath <- releasePath deployPath release Nothing
exec (Cd rpath (GitCheckout revision)) exec (Cd rpath (GitCheckout revision)) (Just release)
-- | Return a list of all currently deployed releases sorted newest first. -- | Return a list of all currently deployed releases sorted newest first.
@ -225,22 +255,27 @@ deployedReleases
-> Hapistrano [Release] -> Hapistrano [Release]
deployedReleases deployPath = do deployedReleases deployPath = do
let rpath = releasesPath deployPath let rpath = releasesPath deployPath
xs <- exec (Find 1 rpath :: Find Dir) xs <- exec (Find 1 rpath :: Find Dir) Nothing
ps <- stripDirs rpath (filter (/= rpath) xs) ps <- stripDirs rpath (filter (/= rpath) xs)
(return . sortOn Down . mapMaybe parseRelease) (return . sortOn Down . mapMaybe parseRelease)
(dropWhileEnd (== '/') . fromRelDir <$> ps) (dropWhileEnd (== '/') . fromRelDir <$> ps)
-- | Return a list of successfully completed releases sorted newest first. -- | Return a list of successfully completed releases sorted newest first.
completedReleases releasesWithState
:: Path Abs Dir -- ^ Deploy path :: DeployState -- ^ Selector for failed or successful releases
-> Path Abs Dir -- ^ Deploy path
-> Hapistrano [Release] -> Hapistrano [Release]
completedReleases deployPath = do releasesWithState selectedState deployPath = do
let cpath = ctokensPath deployPath releases <- deployedReleases deployPath
xs <- exec (Find 1 cpath :: Find File) filterM (
ps <- stripDirs cpath xs fmap ((\bool -> if selectedState == Success then bool else not bool) . stateToBool)
(return . sortOn Down . mapMaybe parseRelease) . deployState deployPath Nothing
(dropWhileEnd (== '/') . fromRelFile <$> ps) ) releases
where
stateToBool :: DeployState -> Bool
stateToBool Fail = False
stateToBool _ = True
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- Path helpers -- Path helpers
@ -268,12 +303,13 @@ linkToShared
-> Path Abs Dir -- ^ Release path -> Path Abs Dir -- ^ Release path
-> Path Abs Dir -- ^ Deploy path -> Path Abs Dir -- ^ Deploy path
-> FilePath -- ^ Thing to link in share -> FilePath -- ^ Thing to link in share
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano () -> Hapistrano ()
linkToShared configTargetSystem rpath configDeployPath thingToLink = do linkToShared configTargetSystem rpath configDeployPath thingToLink maybeRelease = do
destPath <- parseRelFile thingToLink destPath <- parseRelFile thingToLink
let dpath = rpath </> destPath let dpath = rpath </> destPath
sharedPath' = sharedPath configDeployPath </> destPath sharedPath' = sharedPath configDeployPath </> destPath
exec $ Ln configTargetSystem sharedPath' dpath exec (Ln configTargetSystem sharedPath' dpath) maybeRelease
-- | Construct path to a particular 'Release'. -- | Construct path to a particular 'Release'.
@ -285,7 +321,7 @@ releasePath
releasePath deployPath release mWorkingDir = releasePath deployPath release mWorkingDir =
let rendered = renderRelease release let rendered = renderRelease release
in case parseRelDir rendered of in case parseRelDir rendered of
Nothing -> failWith 1 (Just $ "Could not append path: " ++ rendered) Nothing -> failWith 1 (Just $ "Could not append path: " ++ rendered) (Just release)
Just rpath -> Just rpath ->
return $ case mWorkingDir of return $ case mWorkingDir of
Nothing -> releasesPath deployPath </> rpath Nothing -> releasesPath deployPath </> rpath
@ -313,24 +349,25 @@ tempSymlinkPath
-> Path Abs File -> Path Abs File
tempSymlinkPath deployPath = deployPath </> $(mkRelFile "current_tmp") tempSymlinkPath deployPath = deployPath </> $(mkRelFile "current_tmp")
-- | Get path to the directory that contains tokens of build completion. -- | Checks if a release was deployed properly or not
-- by looking into the @.hapistrano_deploy_state@ file.
-- If the file doesn't exist or the contents are anything other than
-- 'Fail' or 'Success', it returns 'Nothing'.
ctokensPath deployState
:: Path Abs Dir -- ^ Deploy path :: Path Abs Dir -- ^ Deploy path
-> Path Abs Dir -> Maybe (Path Rel Dir) -- ^ Working directory
ctokensPath deployPath = deployPath </> $(mkRelDir "ctokens") -> Release -- ^ 'Release' identifier
-> Hapistrano DeployState -- ^ Whether the release was deployed successfully or not
-- | Get path to completion token file for particular release. deployState deployPath mWorkingDir release = do
parseStatePath <- parseRelFile deployStateFilename
ctokenPath actualReleasePath <- releasePath deployPath release mWorkingDir
:: Path Abs Dir -- ^ Deploy path let stateFilePath = actualReleasePath </> parseStatePath
-> Release -- ^ 'Release' identifier doesExist <- exec (CheckExists stateFilePath) (Just release)
-> Hapistrano (Path Abs File) if doesExist then do
ctokenPath deployPath release = do deployStateContents <- exec (Cat stateFilePath) (Just release)
let rendered = renderRelease release return $ (fromMaybe Unknown . readMaybe) deployStateContents
case parseRelFile rendered of else return Unknown
Nothing -> failWith 1 (Just $ "Could not append path: " ++ rendered)
Just rpath -> return (ctokensPath deployPath </> rpath)
stripDirs :: Path Abs Dir -> [Path Abs t] -> Hapistrano [Path Rel t] stripDirs :: Path Abs Dir -> [Path Abs t] -> Hapistrano [Path Rel t]
stripDirs path = stripDirs path =

View File

@ -3,7 +3,7 @@
-- Copyright : © 2015-Present Stack Builders -- Copyright : © 2015-Present Stack Builders
-- License : MIT -- License : MIT
-- --
-- Maintainer : Juan Paucar <jpaucar@stackbuilders.com> -- Maintainer : Cristhian Motoche <cmotoche@stackbuilders.com>
-- Stability : experimental -- Stability : experimental
-- Portability : portable -- Portability : portable
-- --
@ -26,6 +26,9 @@ module System.Hapistrano.Commands
, Readlink(..) , Readlink(..)
, Find(..) , Find(..)
, Touch(..) , Touch(..)
, Cat(..)
, CheckExists(..)
, BasicWrite(..)
, GitCheckout(..) , GitCheckout(..)
, GitClone(..) , GitClone(..)
, GitFetch(..) , GitFetch(..)

View File

@ -3,7 +3,7 @@
-- Copyright : © 2015-Present Stack Builders -- Copyright : © 2015-Present Stack Builders
-- License : MIT -- License : MIT
-- --
-- Maintainer : Juan Paucar <jpaucar@stackbuilders.com> -- Maintainer : Cristhian Motoche <cmotoche@stackbuilders.com>
-- Stability : experimental -- Stability : experimental
-- Portability : portable -- Portability : portable
-- --
@ -67,7 +67,7 @@ instance Command cmd => Command (Cd cmd) where
parseResult Proxy = parseResult (Proxy :: Proxy cmd) parseResult Proxy = parseResult (Proxy :: Proxy cmd)
-- | Create a directory. Does not fail if the directory already exists. -- | Create a directory. Does not fail if the directory already exists.
data MkDir = newtype MkDir =
MkDir (Path Abs Dir) MkDir (Path Abs Dir)
instance Command MkDir where instance Command MkDir where
@ -151,7 +151,7 @@ instance Command (Readlink Dir) where
-- | @ls@, so far used only to check existence of directories, so it's not -- | @ls@, so far used only to check existence of directories, so it's not
-- very functional right now. -- very functional right now.
data Ls = newtype Ls =
Ls (Path Abs Dir) Ls (Path Abs Dir)
instance Command Ls where instance Command Ls where
@ -190,7 +190,7 @@ instance Command (Find File) where
parseResult Proxy = mapMaybe (parseAbsFile . trim) . lines parseResult Proxy = mapMaybe (parseAbsFile . trim) . lines
-- | @touch@. -- | @touch@.
data Touch = newtype Touch =
Touch (Path Abs File) Touch (Path Abs File)
instance Command Touch where instance Command Touch where
@ -198,8 +198,49 @@ instance Command Touch where
renderCommand (Touch path) = formatCmd "touch" [Just (fromAbsFile path)] renderCommand (Touch path) = formatCmd "touch" [Just (fromAbsFile path)]
parseResult Proxy _ = () parseResult Proxy _ = ()
-- | Command that checks for the existance of a particular
-- file in the host.
newtype CheckExists =
CheckExists
(Path Abs File) -- ^ The absolute path to the file you want to check for existence
instance Command CheckExists where
type Result CheckExists = Bool
renderCommand (CheckExists path) =
"([ -r " <> fromAbsFile path <> " ] && echo True) || echo False"
parseResult Proxy = read
-- | Command used to read the contents of a particular
-- file in the host.
newtype Cat =
Cat
(Path Abs File) -- ^ The absolute path to the file you want to read
instance Command Cat where
type Result Cat = String
renderCommand (Cat path) = formatCmd "cat" [Just (fromAbsFile path)]
parseResult Proxy = id
-- | Basic command that writes to a file some contents.
-- It uses the @file > contents@ shell syntax and the @contents@ is
-- represented as a 'String', so it shouldn't be used for
-- bigger writing operations. Currently used to write @fail@ or @success@
-- to the @.hapistrano_deploy_state@ file.
data BasicWrite =
BasicWrite
(Path Abs File) -- ^ The absolute path to the file to which you want to write
String -- ^ The contents that will be written to the file
instance Command BasicWrite where
type Result BasicWrite = ()
renderCommand (BasicWrite path contents) =
"echo \"" <> contents <> "\"" <> " > " <> fromAbsFile path
parseResult Proxy _ = ()
-- | Git checkout. -- | Git checkout.
data GitCheckout = newtype GitCheckout =
GitCheckout String GitCheckout String
instance Command GitCheckout where instance Command GitCheckout where
@ -230,7 +271,7 @@ instance Command GitClone where
parseResult Proxy _ = () parseResult Proxy _ = ()
-- | Git fetch (simplified). -- | Git fetch (simplified).
data GitFetch = newtype GitFetch =
GitFetch String GitFetch String
instance Command GitFetch where instance Command GitFetch where
@ -242,7 +283,7 @@ instance Command GitFetch where
parseResult Proxy _ = () parseResult Proxy _ = ()
-- | Git reset. -- | Git reset.
data GitReset = newtype GitReset =
GitReset String GitReset String
instance Command GitReset where instance Command GitReset where
@ -252,7 +293,7 @@ instance Command GitReset where
parseResult Proxy _ = () parseResult Proxy _ = ()
-- | Weakly-typed generic command, avoid using it directly. -- | Weakly-typed generic command, avoid using it directly.
data GenericCommand = newtype GenericCommand =
GenericCommand String GenericCommand String
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)

View File

@ -1,3 +1,14 @@
-- |
-- Module : System.Config
-- Copyright : © 2015-Present Stack Builders
-- License : MIT
--
-- Maintainer : Cristhian Motoche <cmotoche@stackbuilders.com>
-- Stability : experimental
-- Portability : portable
--
-- Definitions for types and functions related to the configuration
-- of the Hapistrano tool.
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
@ -7,7 +18,8 @@
module System.Hapistrano.Config module System.Hapistrano.Config
( Config (..) ( Config (..)
, CopyThing (..) , CopyThing (..)
, Target (..)) , Target (..)
, deployStateFilename)
where where
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
@ -47,7 +59,7 @@ data Config = Config
, configLinkedDirs :: ![FilePath] , configLinkedDirs :: ![FilePath]
-- ^ Collection of directories to link from each release to _shared_ -- ^ Collection of directories to link from each release to _shared_
, configVcAction :: !Bool , configVcAction :: !Bool
-- ^ Perform version control related actions. By default, it's assumed to be True. -- ^ Perform version control related actions. By default, it's assumed to be `True`.
, configRunLocally :: !(Maybe [GenericCommand]) , configRunLocally :: !(Maybe [GenericCommand])
-- ^ Perform a series of commands on the local machine before communication -- ^ Perform a series of commands on the local machine before communication
-- with target server starts -- with target server starts
@ -55,13 +67,19 @@ data Config = Config
-- ^ Optional parameter to specify the target system. It's GNU/Linux by -- ^ Optional parameter to specify the target system. It's GNU/Linux by
-- default -- default
, configReleaseFormat :: !(Maybe ReleaseFormat) , configReleaseFormat :: !(Maybe ReleaseFormat)
-- ^ The release timestamp format, the '--release-format' argument passed via -- ^ The release timestamp format, the @--release-format@ argument passed via
-- the CLI takes precedence over this value. If neither CLI or configuration -- the CLI takes precedence over this value. If neither CLI or configuration
-- file value is specified, it defaults to short -- file value is specified, it defaults to short
, configKeepReleases :: !(Maybe Natural) , configKeepReleases :: !(Maybe Natural)
-- ^ The number of releases to keep, the '--keep-releases' argument passed via -- ^ The number of releases to keep, the @--keep-releases@ argument passed via
-- the CLI takes precedence over this value. If neither CLI or configuration -- the CLI takes precedence over this value. If neither CLI or configuration
-- file value is specified, it defaults to 5 -- file value is specified, it defaults to 5
, configKeepOneFailed :: !Bool
-- ^ Specifies whether to keep all failed releases along with the successful releases
-- or just the latest failed (at least this one should be kept for debugging purposes).
-- The @--keep-one-failed@ argument passed via the CLI takes precedence over this value.
-- If neither CLI or configuration file value is specified, it defaults to `False`
-- (i.e. keep all failed releases).
, configWorkingDir :: !(Maybe (Path Rel Dir)) , configWorkingDir :: !(Maybe (Path Rel Dir))
} deriving (Eq, Ord, Show) } deriving (Eq, Ord, Show)
@ -71,6 +89,8 @@ data Config = Config
data CopyThing = CopyThing FilePath FilePath data CopyThing = CopyThing FilePath FilePath
deriving (Eq, Ord, Show) deriving (Eq, Ord, Show)
-- | Datatype that holds information about the target host.
data Target = data Target =
Target Target
{ targetHost :: String { targetHost :: String
@ -116,6 +136,7 @@ instance FromJSON Config where
configTargetSystem <- o .:? "linux" .!= GNULinux configTargetSystem <- o .:? "linux" .!= GNULinux
configReleaseFormat <- o .:? "release_format" configReleaseFormat <- o .:? "release_format"
configKeepReleases <- o .:? "keep_releases" configKeepReleases <- o .:? "keep_releases"
configKeepOneFailed <- o .:? "keep_one_failed" .!= False
configWorkingDir <- o .:? "working_directory" configWorkingDir <- o .:? "working_directory"
return Config {..} return Config {..}
@ -134,3 +155,9 @@ mkCmd raw =
case mkGenericCommand raw of case mkGenericCommand raw of
Nothing -> fail "invalid restart command" Nothing -> fail "invalid restart command"
Just cmd -> return cmd Just cmd -> return cmd
-- | Constant with the name of the file used to store
-- the deployment state information.
deployStateFilename :: String
deployStateFilename = ".hapistrano_deploy_state"

View File

@ -3,7 +3,7 @@
-- Copyright : © 2015-Present Stack Builders -- Copyright : © 2015-Present Stack Builders
-- License : MIT -- License : MIT
-- --
-- Maintainer : Juan Paucar <jpaucar@stackbuilders.com> -- Maintainer : Cristhian Motoche <cmotoche@stackbuilders.com>
-- Stability : experimental -- Stability : experimental
-- Portability : portable -- Portability : portable
-- --
@ -14,8 +14,7 @@
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
module System.Hapistrano.Core module System.Hapistrano.Core
( runHapistrano ( failWith
, failWith
, exec , exec
, execWithInheritStdout , execWithInheritStdout
, scpFile , scpFile
@ -32,38 +31,14 @@ import Path
import System.Console.ANSI import System.Console.ANSI
import System.Exit import System.Exit
import System.Hapistrano.Commands import System.Hapistrano.Commands
import System.Hapistrano.Types import System.Hapistrano.Types hiding (Command)
import System.Process import System.Process
import System.Process.Typed (ProcessConfig) import System.Process.Typed (ProcessConfig)
import qualified System.Process.Typed as SPT import qualified System.Process.Typed as SPT
-- | Run the 'Hapistrano' monad. The monad hosts 'exec' actions.
runHapistrano ::
MonadIO m
=> Maybe SshOptions -- ^ SSH options to use or 'Nothing' if we run locally
-> Shell -- ^ Shell to run commands
-> (OutputDest -> String -> IO ()) -- ^ How to print messages
-> Hapistrano a -- ^ The computation to run
-> m (Either Int a) -- ^ Status code in 'Left' on failure, result in
-- 'Right' on success
runHapistrano sshOptions shell' printFnc m =
liftIO $ do
let config =
Config
{ configSshOptions = sshOptions
, configShellOptions = shell'
, configPrint = printFnc
}
r <- runReaderT (runExceptT m) config
case r of
Left (Failure n msg) -> do
forM_ msg (printFnc StderrDest)
return (Left n)
Right x -> return (Right x)
-- | Fail returning the following status code and message. -- | Fail returning the following status code and message.
failWith :: Int -> Maybe String -> Hapistrano a failWith :: Int -> Maybe String -> Maybe Release -> Hapistrano a
failWith n msg = throwError (Failure n msg) failWith n msg maybeRelease = throwError (Failure n msg, maybeRelease)
-- | Run the given sequence of command. Whether to use SSH or not is -- | Run the given sequence of command. Whether to use SSH or not is
-- determined from settings contained in the 'Hapistrano' monad -- determined from settings contained in the 'Hapistrano' monad
@ -74,20 +49,25 @@ failWith n msg = throwError (Failure n msg)
-- parse the result. -- parse the result.
exec :: exec ::
forall a. Command a forall a. Command a
=> a => a -- ^ Command being executed
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano (Result a) -> Hapistrano (Result a)
exec typedCmd = do exec typedCmd maybeRelease = do
let cmd = renderCommand typedCmd let cmd = renderCommand typedCmd
(prog, args) <- getProgAndArgs cmd (prog, args) <- getProgAndArgs cmd
parseResult (Proxy :: Proxy a) <$> parseResult (Proxy :: Proxy a) <$>
exec' cmd (readProcessWithExitCode prog args "") exec' cmd (readProcessWithExitCode prog args "") maybeRelease
-- | Same as 'exec' but it streams to stdout only for _GenericCommand_s -- | Same as 'exec' but it streams to stdout only for _GenericCommand_s
execWithInheritStdout :: Command a => a -> Hapistrano () execWithInheritStdout ::
execWithInheritStdout typedCmd = do Command a
=> a -- ^ Command being executed
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano ()
execWithInheritStdout typedCmd maybeRelease = do
let cmd = renderCommand typedCmd let cmd = renderCommand typedCmd
(prog, args) <- getProgAndArgs cmd (prog, args) <- getProgAndArgs cmd
void $ exec' cmd (readProcessWithExitCode' (SPT.proc prog args)) void $ exec' cmd (readProcessWithExitCode' (SPT.proc prog args)) maybeRelease
where where
-- | Prepares a process, reads @stdout@ and @stderr@ and returns exit code -- | Prepares a process, reads @stdout@ and @stderr@ and returns exit code
-- NOTE: @strdout@ and @stderr@ are empty string because we're writing -- NOTE: @strdout@ and @stderr@ are empty string because we're writing
@ -120,6 +100,7 @@ getProgAndArgs cmd = do
scpFile :: scpFile ::
Path Abs File -- ^ Location of the file to copy Path Abs File -- ^ Location of the file to copy
-> Path Abs File -- ^ Where to put the file on target machine -> Path Abs File -- ^ Where to put the file on target machine
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano () -> Hapistrano ()
scpFile src dest = scp' (fromAbsFile src) (fromAbsFile dest) ["-q"] scpFile src dest = scp' (fromAbsFile src) (fromAbsFile dest) ["-q"]
@ -127,11 +108,12 @@ scpFile src dest = scp' (fromAbsFile src) (fromAbsFile dest) ["-q"]
scpDir :: scpDir ::
Path Abs Dir -- ^ Location of the directory to copy Path Abs Dir -- ^ Location of the directory to copy
-> Path Abs Dir -- ^ Where to put the dir on target machine -> Path Abs Dir -- ^ Where to put the dir on target machine
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano () -> Hapistrano ()
scpDir src dest = scp' (fromAbsDir src) (fromAbsDir dest) ["-qr"] scpDir src dest = scp' (fromAbsDir src) (fromAbsDir dest) ["-qr"]
scp' :: FilePath -> FilePath -> [String] -> Hapistrano () scp' :: FilePath -> FilePath -> [String] -> Maybe Release -> Hapistrano ()
scp' src dest extraArgs = do scp' src dest extraArgs maybeRelease = do
Config {..} <- ask Config {..} <- ask
let prog = "scp" let prog = "scp"
portArg = portArg =
@ -144,7 +126,7 @@ scp' src dest extraArgs = do
Just x -> x ++ ":" Just x -> x ++ ":"
args = extraArgs ++ portArg ++ [src, hostPrefix ++ dest] args = extraArgs ++ portArg ++ [src, hostPrefix ++ dest]
void void
(exec' (prog ++ " " ++ unwords args) (readProcessWithExitCode prog args "")) (exec' (prog ++ " " ++ unwords args) (readProcessWithExitCode prog args "") maybeRelease)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- Helpers -- Helpers
@ -152,8 +134,9 @@ scp' src dest extraArgs = do
exec' :: exec' ::
String -- ^ How to show the command in print-outs String -- ^ How to show the command in print-outs
-> IO (ExitCode, String, String) -- ^ Handler to get (ExitCode, Output, Error) it can change accordingly to @stdout@ and @stderr@ of child process -> IO (ExitCode, String, String) -- ^ Handler to get (ExitCode, Output, Error) it can change accordingly to @stdout@ and @stderr@ of child process
-> Maybe Release -- ^ Release that was being attempted, if it was defined
-> Hapistrano String -- ^ Raw stdout output of that program -> Hapistrano String -- ^ Raw stdout output of that program
exec' cmd readProcessOutput = do exec' cmd readProcessOutput maybeRelease = do
Config {..} <- ask Config {..} <- ask
time <- liftIO getZonedTime time <- liftIO getZonedTime
let timeStampFormat = "%T, %F (%Z)" let timeStampFormat = "%T, %F (%Z)"
@ -171,7 +154,7 @@ exec' cmd readProcessOutput = do
unless (null stderr') . liftIO $ configPrint StderrDest stderr' unless (null stderr') . liftIO $ configPrint StderrDest stderr'
case exitCode' of case exitCode' of
ExitSuccess -> return stdout' ExitSuccess -> return stdout'
ExitFailure n -> failWith n Nothing ExitFailure n -> failWith n Nothing maybeRelease
-- | Put something “inside” a line, sort-of beautifully. -- | Put something “inside” a line, sort-of beautifully.
putLine :: String -> String putLine :: String -> String

View File

@ -3,7 +3,7 @@
-- Copyright : © 2015-Present Stack Builders -- Copyright : © 2015-Present Stack Builders
-- License : MIT -- License : MIT
-- --
-- Maintainer : Juan Paucar <jpaucar@stackbuilders.com> -- Maintainer : Cristhian Motoche <cmotoche@stackbuilders.com>
-- Stability : experimental -- Stability : experimental
-- Portability : portable -- Portability : portable
-- --
@ -22,7 +22,10 @@ module System.Hapistrano.Types
, OutputDest(..) , OutputDest(..)
, Release , Release
, TargetSystem(..) , TargetSystem(..)
, DeployState(..)
, Shell(..) , Shell(..)
, Opts(..)
, Command(..)
-- * Types helpers -- * Types helpers
, mkRelease , mkRelease
, releaseTime , releaseTime
@ -43,7 +46,7 @@ import Numeric.Natural
import Path import Path
-- | Hapistrano monad. -- | Hapistrano monad.
type Hapistrano a = ExceptT Failure (ReaderT Config IO) a type Hapistrano a = ExceptT (Failure, Maybe Release) (ReaderT Config IO) a
-- | Failure with status code and a message. -- | Failure with status code and a message.
data Failure = data Failure =
@ -133,12 +136,41 @@ data Release =
Release ReleaseFormat UTCTime Release ReleaseFormat UTCTime
deriving (Eq, Show, Ord) deriving (Eq, Show, Ord)
-- | Target's system where application will be deployed -- | Target's system where application will be deployed.
data TargetSystem data TargetSystem
= GNULinux = GNULinux
| BSD | BSD
deriving (Eq, Show, Read, Ord, Bounded, Enum) deriving (Eq, Show, Read, Ord, Bounded, Enum)
-- | State of the deployment after running @hap deploy@.
-- __note:__ the 'Unknown' value is not intended to be
-- written to the @.hapistrano_deploy_state@ file; instead,
-- it's intended to represent whenever Hapistrano couldn't
-- get the information on the deployment state (e.g. the file is not present).
data DeployState
= Fail
| Success
| Unknown
deriving (Eq, Show, Read, Ord, Bounded, Enum)
-- Command line options
-- | Command line options.
data Opts = Opts
{ optsCommand :: Command
, optsConfigFile :: FilePath
}
-- | Command to execute and command-specific options.
data Command
= Deploy (Maybe ReleaseFormat) (Maybe Natural) Bool -- ^ Deploy a new release (with timestamp
-- format, how many releases to keep, and whether the failed releases except the latest one
-- get deleted or not)
| Rollback Natural -- ^ Rollback to Nth previous release
-- | Create a 'Release' indentifier. -- | Create a 'Release' indentifier.
mkRelease :: ReleaseFormat -> UTCTime -> Release mkRelease :: ReleaseFormat -> UTCTime -> Release
mkRelease = Release mkRelease = Release