packages feed

hercules-ci-cli-0.2.2: src/Hercules/CLI/Project.hs

module Hercules.CLI.Project where

import qualified Data.Attoparsec.Text as A
import Data.Has (Has)
import qualified Data.UUID
import Hercules.API (Id)
import Hercules.API.Id (Id (Id))
import Hercules.API.Name (Name (Name))
import Hercules.API.Projects (findProjects)
import qualified Hercules.API.Projects as Projects
import Hercules.API.Projects.Project (Project)
import qualified Hercules.API.Projects.Project as Project
import qualified Hercules.API.Repos as Repos
import qualified Hercules.API.Repos.RepoKey as RepoKey
import Hercules.CLI.Client (HerculesClientEnv, HerculesClientToken, projectsClient, reposClient, runHerculesClient, runHerculesClientEither)
import Hercules.CLI.Common (exitMsg)
import qualified Hercules.CLI.Git as Git
import Hercules.CLI.Options (attoparsecReader, packSome)
import Hercules.Error (escalate, escalateAs)
import Network.HTTP.Types (Status (Status, statusCode))
import Options.Applicative (bashCompleter, completer, help, long, metavar, option, strOption)
import qualified Options.Applicative as Optparse
import Protolude hiding (option)
import RIO (RIO)
import Servant.Client.Core (ClientError (FailureResponse), ResponseF (responseStatusCode))
import Servant.Client.Core.Response (ResponseF (Response))
import UnliftIO.Environment (lookupEnv)
import qualified Prelude

data ProjectPath = ProjectPath
  { projectPathSite :: Text,
    projectPathOwner :: Text,
    projectPathProject :: Text
  }

instance Prelude.Show ProjectPath where
  show = toS . projectPathText

projectPathText :: ProjectPath -> Text
projectPathText = projectPathSite <> const "/" <> projectPathOwner <> const "/" <> projectPathProject

projectOption :: Optparse.Parser ProjectPath
projectOption =
  option projectPathReadM $
    long "project" <> metavar "PROJECT" <> help "Project path, e.g. github/my-org/my-project"

nameOption :: Optparse.Parser Text
nameOption = strOption $ long "name" <> metavar "NAME" <> help "Name of the state file"

fileOption :: Optparse.Parser FilePath
fileOption = strOption $ long "file" <> metavar "FILE" <> help "Local path of the state file or - for stdio" <> completer (bashCompleter "file")

projectPathReadM :: Optparse.ReadM ProjectPath
projectPathReadM = attoparsecReader parseProjectPath

parseProjectPath :: A.Parser ProjectPath
parseProjectPath =
  pure ProjectPath
    <*> packSome (A.satisfy (/= '/'))
    <* A.char '/'
    <*> packSome (A.satisfy (/= '/'))
    <* A.char '/'
    <*> packSome (A.satisfy (/= '/'))

parseProjectPathFromText :: Text -> Either [Char] ProjectPath
parseProjectPathFromText = A.parseOnly parseProjectPath

getProjectPath :: (Has HerculesClientToken r, Has HerculesClientEnv r) => Maybe ProjectPath -> RIO r ProjectPath
getProjectPath maybeProjectPathParam =
  case maybeProjectPathParam of
    Nothing -> snd <$> findProjectContextually
    Just projectKey -> pure projectKey

getProjectIdAndPath :: (Has HerculesClientToken r, Has HerculesClientEnv r) => Maybe ProjectPath -> RIO r (Maybe (Id Project), ProjectPath)
getProjectIdAndPath maybeProjectPathParam = do
  case maybeProjectPathParam of
    Nothing -> findProjectContextually
    Just projectKey -> do
      project <- findProjectByKey projectKey
      pure (Project.id <$> project, projectKey)

findProjectByKey :: (Has HerculesClientToken r, Has HerculesClientEnv r) => ProjectPath -> RIO r (Maybe Project.Project)
findProjectByKey path =
  runHerculesClient
    ( Projects.findProjects
        projectsClient
        (Just $ Name $ projectPathSite path)
        (Just $ Name $ projectPathOwner path)
        (Just $ Name $ projectPathProject path)
    )
    <&> head

findProjectContextually :: (Has HerculesClientToken r, Has HerculesClientEnv r) => RIO r (Maybe (Id Project), ProjectPath)
findProjectContextually = do
  projectIdMaybe <- lookupEnv "HERCULES_CI_PROJECT_ID"
  projectIdPathMaybe <- lookupEnv "HERCULES_CI_PROJECT_PATH"
  case (,) <$> projectIdMaybe <*> projectIdPathMaybe of
    Nothing -> findProjectByCurrentRepo
    Just (id, pathText) -> do
      projectPath <- parseProjectPathFromText (toS pathText) & escalateAs (\e -> FatalError $ "Invalid HERCULES_CI_PROJECT_PATH supplied: " <> toS e)
      uuid <- Data.UUID.fromString id & maybeToEither (FatalError "Invalid UUID in HERCULES_CI_PROJECT_ID") & escalate
      pure (Just (Id uuid), projectPath)

findProjectByCurrentRepo :: (Has HerculesClientToken r, Has HerculesClientEnv r) => RIO r (Maybe (Id Project), ProjectPath)
findProjectByCurrentRepo = do
  url <- liftIO Git.getUpstreamURL
  rs <- runHerculesClientEither (Repos.parseGitURL reposClient url)
  case rs of
    Left (FailureResponse _req Response {responseStatusCode = Status {statusCode = 404}}) -> do
      exitMsg "Repository not recognized by Hercules CI. Make sure you're in the right repository, and if you're running Hercules CI Enterprise, make sure you're using the right HERCULES_CI_API_BASE_URL. Alternatively, use the --project option."
    Left e -> throwIO e
    Right r ->
      pure
        ( RepoKey.projectId r,
          ProjectPath
            { projectPathSite = RepoKey.siteName r,
              projectPathOwner = RepoKey.ownerName r,
              projectPathProject = RepoKey.repoName r
            }
        )

findProject :: (Has HerculesClientToken r, Has HerculesClientEnv r) => ProjectPath -> RIO r Project.Project
findProject project = do
  rs <-
    runHerculesClient
      ( findProjects
          projectsClient
          (Just $ Name $ projectPathSite project)
          (Just $ Name $ projectPathOwner project)
          (Just $ Name $ projectPathProject project)
      )
  case rs of
    [] -> do
      exitMsg $ "Project not found: " <> show project
    [p] -> pure p
    _ -> do
      exitMsg $ "Project ambiguous: " <> show project