packages feed

hercules-ci-cli 0.1.0 → 0.2.0

raw patch · 9 files changed

+97/−27 lines, 9 files

Files

CHANGELOG.md view
@@ -5,14 +5,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased]+## 0.2.0 - 2021-04-21 -## 0.1.0+### Added -Added+ - `hci secret add`: Add `--json-env` and `--string-env`: more secure alternative for literals - - First version of the `hci` command+### Changes + - Remove `-h` and `--help` from tab completion and help text.+ - User-friendly error when `ci.nix` or similar can not be found. -[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...HEAD+## 0.1.0 +### Added++ - First version of the `hci` command
hercules-ci-cli.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12  name:           hercules-ci-cli-version:        0.1.0+version:        0.2.0 synopsis:       The hci command for working with Hercules CI homepage:       https://docs.hercules-ci.com bug-reports:    https://github.com/hercules-ci/hercules-ci-agent/issues
src/Hercules/CLI/Effect.hs view
@@ -14,7 +14,7 @@ import Hercules.CLI.Exception (exitMsg) import Hercules.CLI.Git (getAllBranches, getHypotheticalRefs) import Hercules.CLI.Nix (attrByPath, callCiNix, ciNixAttributeCompleter, withNix)-import Hercules.CLI.Options (flatCompleter, mkCommand)+import Hercules.CLI.Options (flatCompleter, mkCommand, subparser) import Hercules.CLI.Project (ProjectPath, getProjectIdAndPath, projectOption) import Hercules.CLI.Secret (getSecretsFilePath) import Hercules.CNix (Store)@@ -35,7 +35,7 @@  commandParser, runParser :: Optparse.Parser (IO ()) commandParser =-  Optparse.subparser+  subparser     ( mkCommand         "run"         (Optparse.progDesc "Run an effect")
src/Hercules/CLI/JSON.hs view
@@ -12,9 +12,11 @@ import qualified Data.HashMap.Strict as HM import qualified Data.List.NonEmpty as NEL import qualified Data.Text as T+import Hercules.CLI.Exception (UserException (UserException)) import qualified Options.Applicative as Optparse import Protolude import System.AtomicWrite.Writer.ByteString (atomicWriteFile)+import System.Environment (getEnvironment, lookupEnv)  mergePaths :: [([Text], Value)] -> Either Text Value mergePaths = mergeLeafPaths [] . toLeafPaths@@ -94,48 +96,98 @@               <> Optparse.completer2 (Optparse.bashCompleter "file")           )       )+  stringEnvs <-+    many+      ( Optparse.biOption+          Optparse.str+          Optparse.str+          ( Optparse.long "string-env"+              <> Optparse.help "Define a string at dot-separated PATH in the secret data, by reading environment variable ENV_NAME"+              <> Optparse.metavar "PATH"+              <> Optparse.metavar2 "ENV_NAME"+              <> Optparse.completer2 envCompleter+          )+      )+  jsonEnvs <-+    many+      ( Optparse.biOption+          Optparse.str+          Optparse.str+          ( Optparse.long "json-env"+              <> Optparse.help "Define a JSON value at dot-separated PATH in the secret data, by reading environment variable ENV_NAME"+              <> Optparse.metavar "PATH"+              <> Optparse.metavar2 "ENV_NAME"+              <> Optparse.completer2 envCompleter+          )+      )   pure do     fileStrings <- for stringFiles readFileWithKey     fileJsons <- for jsonFiles readJsonFileWithKey+    envStrings <- for stringEnvs readEnvWithKey+    envJsons <- for jsonEnvs readJsonEnvWithKey     validJsons <-       for         jsons         ( \(key, value) ->             case eitherDecode $ BL.fromStrict $ encodeUtf8 value of-              Left e -> throwIO $ FatalError $ "Value for key " <> key <> " is not valid JSON: " <> show e+              Left e -> throwIO $ UserException $ "Value for key " <> key <> " is not valid JSON: " <> show e               Right r -> pure (key, r :: Value)         )     let items =-          (fmap String <$> strings) <> (fmap String <$> fileStrings) <> validJsons <> fileJsons-            & map (first split)+          map (first split) $+            (fmap String <$> strings)+              <> (fmap String <$> envStrings)+              <> (fmap String <$> fileStrings)+              <> validJsons+              <> envJsons+              <> fileJsons+         split "." = []         split "" = []         split p = T.split (== '.') p     when (items & any \(path, _) -> path & any (T.any (== '\"'))) do       throwIO $ FatalError "Quotes in field names are not allowed, so proper quotation can be implemented in the future. Write the field name in the value of --json or --json-file instead."     case mergePaths items of-      Left e -> throwIO $ FatalError $ show e+      Left e -> throwIO $ UserException $ show e       Right r -> pure r +readJsonEnvWithKey :: (Text, [Char]) -> IO (Text, Value)+readJsonEnvWithKey (key, envVar) =+  lookupEnv envVar >>= \case+    Nothing -> throwIO $ UserException $ "Environment variable does not exist: " <> show envVar+    Just x -> case eitherDecode (BL.fromStrict $ encodeUtf8 $ toS x) of+      Left e -> throwIO $ UserException $ "Environment variable " <> show envVar <> " has invalid JSON: " <> show e+      Right r -> pure (key, r)++readEnvWithKey :: (Text, [Char]) -> IO (Text, Text)+readEnvWithKey (key, envVar) =+  lookupEnv envVar >>= \case+    Nothing -> throwIO $ UserException $ "Environment variable does not exist: " <> show envVar+    Just x -> pure (key, toS x)++envCompleter :: Optparse.Completer+envCompleter = Optparse.listIOCompleter do+  map fst <$> getEnvironment+ readFileWithKey :: (Text, FilePath) -> IO (Text, Text) readFileWithKey (key, file) = do   bs <- BS.readFile file   case decodeUtf8' bs of-    Left _e -> throwIO $ FatalError $ "File " <> show file <> " for key " <> key <> " is not valid UTF-8."+    Left _e -> throwIO $ UserException $ "File " <> show file <> " for key " <> key <> " is not valid UTF-8."     Right s -> pure (key, s)  readJsonFileWithKey :: FromJSON b => (Text, FilePath) -> IO (Text, b) readJsonFileWithKey (key, file) = do   bs <- BS.readFile file   case eitherDecode (BL.fromStrict bs) of-    Left e -> throwIO $ FatalError $ "File " <> show file <> " for key " <> key <> " is not valid JSON: " <> show e+    Left e -> throwIO $ UserException $ "File " <> show file <> " for key " <> key <> " is not valid JSON: " <> show e     Right s -> pure (key, s)  readJsonFile :: FromJSON b => FilePath -> IO b readJsonFile file = do   bs <- BS.readFile file   case eitherDecode (BL.fromStrict bs) of-    Left e -> throwIO $ FatalError $ "File " <> show file <> " is not valid JSON: " <> show e+    Left e -> throwIO $ UserException $ "File " <> show file <> " is not valid JSON: " <> show e     Right s -> pure s  writeJsonFile :: ToJSON a => FilePath -> a -> IO ()
src/Hercules/CLI/Main.hs view
@@ -9,7 +9,7 @@ import qualified Hercules.CLI.Effect as Effect import qualified Hercules.CLI.Exception as Exception import qualified Hercules.CLI.Login as Login-import Hercules.CLI.Options (mkCommand)+import Hercules.CLI.Options (execParser, helper, mkCommand, subparser) import qualified Hercules.CLI.Secret as Secret import qualified Hercules.CLI.State as State import qualified Options.Applicative as Optparse@@ -20,7 +20,7 @@   prettyPrintErrors $     Exception.handleUserException $       prettyPrintHttpErrors $ do-        join $ Optparse.execParser opts+        join $ execParser opts  prettyPrintErrors :: IO a -> IO a prettyPrintErrors = handle \e ->@@ -33,12 +33,12 @@ opts :: Optparse.ParserInfo (IO ()) opts =   Optparse.info-    (commands <**> Optparse.helper)+    (commands <**> helper)     (Optparse.fullDesc <> Optparse.header "Command line interface to Hercules CI")  commands :: Optparse.Parser (IO ()) commands =-  Optparse.subparser+  subparser     ( mkCommand         "login"         (Optparse.progDesc "Configure token for authentication to hercules-ci.com")
src/Hercules/CLI/Nix.hs view
@@ -6,6 +6,7 @@ import qualified Data.Map as M import qualified Data.Text as T import Hercules.Agent.NixFile (findNixFile)+import Hercules.CLI.Exception (UserException (UserException)) import Hercules.CLI.Git (getGitRoot, getRef, getRev) import Hercules.CLI.Options (scanOption) import Hercules.CNix (Store)@@ -20,7 +21,7 @@   gitRoot <- getGitRoot   gitRev <- getRev   gitRef <- getRef-  nixFile <- findNixFile gitRoot >>= escalateAs FatalError+  nixFile <- findNixFile gitRoot >>= escalateAs UserException   let ref = fromMaybe gitRef passedRef   args <- evalArgs evalState ["--arg", "src", "{ ref = ''" <> encodeUtf8 ref <> "''; rev = ''" <> encodeUtf8 gitRev <> "''; outPath = ''" <> encodeUtf8 (toS gitRoot) <> "''; }"]   rootValueOrFunction <- evalFile evalState nixFile
src/Hercules/CLI/Options.hs view
@@ -4,8 +4,20 @@  import qualified Data.Attoparsec.Text as A import qualified Data.Text as T-import Options.Applicative+import Options.Applicative hiding (helper)+import qualified Options.Applicative as Optparse import Protolude++-- | Custom execParser that provides help text when input is incomplete.+execParser :: ParserInfo a -> IO a+execParser = Optparse.customExecParser (Optparse.prefs Optparse.showHelpOnEmpty)++-- | We omit --help from the help text and completion.+helper :: Parser (a -> a)+helper = helperWith (short 'h' <> long "help" <> internal)++subparser :: Mod CommandFields a -> Parser a+subparser = Optparse.subparser  mkCommand ::   [Char] ->
src/Hercules/CLI/Secret.hs view
@@ -8,7 +8,7 @@ import qualified Data.Text as T import Hercules.CLI.Common (exitMsg, runAuthenticated) import Hercules.CLI.JSON as JSON-import Hercules.CLI.Options (mkCommand)+import Hercules.CLI.Options (mkCommand, subparser) import Hercules.CLI.Project (ProjectPath (projectPathOwner, projectPathSite), getProjectPath, projectOption) import qualified Options.Applicative as Optparse import Protolude@@ -17,7 +17,7 @@  commandParser, initLocal, add :: Optparse.Parser (IO ()) commandParser =-  Optparse.subparser+  subparser     ( mkCommand         "init-local"         (Optparse.progDesc "Create a local secrets file in ~/.config/hercules-ci/secrets/<site>/<owner>")@@ -40,9 +40,9 @@         liftIO $ writeFile secretsFilePath "{}"         putErrText $ "hci: Secrets file created. Path: " <> toS secretsFilePath add = do-  projectOptionMaybe <- optional projectOption-  mkJson <- JSON.options   secretName <- Optparse.strArgument (Optparse.metavar "SECRET_NAME" <> Optparse.help "Organization/account-wide name for the secret")+  mkJson <- JSON.options+  projectOptionMaybe <- optional projectOption   pure $ runAuthenticated do     secretData <- liftIO mkJson     projectPath <- getProjectPath projectOptionMaybe
src/Hercules/CLI/State.hs view
@@ -8,7 +8,7 @@ import Hercules.API.State import Hercules.CLI.Client import Hercules.CLI.Common (runAuthenticated)-import Hercules.CLI.Options (mkCommand)+import Hercules.CLI.Options (mkCommand, subparser) import Hercules.CLI.Project (findProject, projectOption) import Options.Applicative (bashCompleter, completer, help, long, metavar, strOption) import qualified Options.Applicative as Optparse@@ -19,7 +19,7 @@  commandParser, getCommandParser, putCommandParser :: Optparse.Parser (IO ()) commandParser =-  Optparse.subparser+  subparser     ( mkCommand         "get"         (Optparse.progDesc "Download a state file")