packages feed

github-actions-0.1.0.0: src/Language/Github/Actions/Shell.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}

-- |
-- Module      : Language.Github.Actions.Shell
-- Description : GitHub Actions shell configuration for steps
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'Shell' type for specifying which shell to use when
-- running commands in GitHub Actions steps.
--
-- Different shells provide different capabilities and are available on different
-- operating systems. The shell can be specified at the workflow, job, or step level.
--
-- For more information about GitHub Actions shell configuration, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>
module Language.Github.Actions.Shell
  ( Platform (..),
    Shell (..),
    arguments,
    gen,
    supportedPlatforms,
  )
where

import Control.Monad.Fail.Hoist (hoistFail')
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as Aeson
import Data.List.NonEmpty (NonEmpty (..))
import Data.String.Interpolate (i)
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

-- | Shell types available for running commands in GitHub Actions steps.
--
-- Each shell has different capabilities and platform availability:
--
-- * 'Bash' - Available on all platforms, most commonly used
-- * 'Sh' - POSIX sh, available on Linux and macOS only
-- * 'Python' - Executes commands as Python code
-- * 'Cmd' - Windows Command Prompt, Windows only
-- * 'Powershell' - Windows PowerShell 5.1, Windows only
-- * 'Pwsh' - PowerShell Core, available on all platforms
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Shell
--
-- -- Use bash with default options
-- bashShell :: Shell
-- bashShell = Bash Nothing
--
-- -- Use bash with specific options
-- bashWithOptions :: Shell
-- bashWithOptions = Bash (Just "--noprofile --norc")
--
-- -- Use PowerShell Core
-- pwshShell :: Shell
-- pwshShell = Pwsh Nothing
-- @
--
-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>
data Shell
  = -- | Bash shell with optional arguments
    Bash (Maybe Text)
  | -- | POSIX sh shell (Linux/macOS only) with optional arguments
    Sh (Maybe Text)
  | -- | Python interpreter with optional arguments
    Python (Maybe Text)
  | -- | Windows cmd.exe with optional arguments
    Cmd (Maybe Text)
  | -- | Windows PowerShell 5.1 with optional arguments
    Powershell (Maybe Text)
  | -- | PowerShell Core with optional arguments
    Pwsh (Maybe Text)
  deriving stock (Eq, Generic, Ord, Show)

instance FromJSON Shell where
  parseJSON =
    Aeson.withText "Shell" $
      hoistFail' . parseShell

instance ToJSON Shell where
  toJSON = Aeson.String . renderShell

renderShell :: Shell -> Text
renderShell = \case
  Bash args -> [i|bash#{maybe "" ((" " <>)) args}|]
  Sh args -> [i|sh#{maybe "" ((" " <>)) args}|]
  Python args -> [i|python#{maybe "" ((" " <>)) args}|]
  Cmd args -> [i|cmd#{maybe "" ((" " <>)) args}|]
  Powershell args -> [i|powershell#{maybe "" ((" " <>)) args}|]
  Pwsh args -> [i|pwsh#{maybe "" ((" " <>)) args}|]

parseShell :: Text -> Either String Shell
parseShell t =
  case tokens of
    "bash" : args -> Right . Bash $ maybeArgs args
    "cmd" : args -> Right . Cmd $ maybeArgs args
    "powershell" : args -> Right . Powershell $ maybeArgs args
    "pwsh" : args -> Right . Pwsh $ maybeArgs args
    "python" : args -> Right . Python $ maybeArgs args
    "sh" : args -> Right . Sh $ maybeArgs args
    _ -> Left [i|Unknown shell: #{t}|]
  where
    tokens = Text.words t
    maybeArgs = \case
      [] -> Nothing
      args -> Just $ Text.unwords args

gen :: (MonadGen m) => m Shell
gen =
  Gen.choice
    [ Bash <$> Gen.maybe genText,
      Sh <$> Gen.maybe genText,
      Python <$> Gen.maybe genText,
      Cmd <$> Gen.maybe genText,
      Powershell <$> Gen.maybe genText,
      Pwsh <$> Gen.maybe genText
    ]
  where
    genText = Gen.text (Range.linear 1 5) Gen.alphaNum

data Platform = Windows | MacOS | Linux

supportedPlatforms :: Shell -> NonEmpty Platform
supportedPlatforms = \case
  Bash _ -> Linux :| [MacOS, Windows]
  Sh _ -> MacOS :| []
  Python _ -> Linux :| [MacOS, Windows]
  Cmd _ -> Windows :| []
  Powershell _ -> Windows :| []
  Pwsh _ -> Windows :| []

-- | A lens into the arguments for a shell, compatible with the "lens" package
--
-- @
-- arguments :: Lens' Shell (Maybe Text)
-- @
arguments :: forall f. (Functor f) => (Maybe Text -> f (Maybe Text)) -> Shell -> f Shell
arguments f = \case
  Bash args -> Bash <$> f args
  Sh args -> Sh <$> f args
  Python args -> Python <$> f args
  Cmd args -> Cmd <$> f args
  Powershell args -> Powershell <$> f args
  Pwsh args -> Pwsh <$> f args