hercules-ci-cli 0.1.0 → 0.2.0
raw patch · 9 files changed
+97/−27 lines, 9 files
Files
- CHANGELOG.md +10/−5
- hercules-ci-cli.cabal +1/−1
- src/Hercules/CLI/Effect.hs +2/−2
- src/Hercules/CLI/JSON.hs +59/−7
- src/Hercules/CLI/Main.hs +4/−4
- src/Hercules/CLI/Nix.hs +2/−1
- src/Hercules/CLI/Options.hs +13/−1
- src/Hercules/CLI/Secret.hs +4/−4
- src/Hercules/CLI/State.hs +2/−2
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")