diff --git a/.gitignore b/.gitignore index 82f3a88..55d6fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +ci-info.cabal dist dist-* cabal-dev diff --git a/README.md b/README.md index 50dcd44..521937e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # ci-info-hs - Get details about the current Continuous Integration environment - Haskell port of https://github.com/watson/ci-info + +Checks if the current environment is a Continuous Integration server. + +This is a Haskell port of [watson/ci-info](https://github.com/watson/ci-info). + +## Usage + +Check if the current environment is a CI server: + +```haskell +isCI :: IO Bool +``` + +Get the name of the CI vendor. Returns `Nothing` if no CI could be detected. Returns `Just CI_UNKNOWN_VENDOR` if a CI was detected, but the vendor name could not be determined: + +```haskell +getCI :: IO (Maybe CI) +``` diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..ebdc00e --- /dev/null +++ b/Setup.hs @@ -0,0 +1,3 @@ +import Distribution.Simple + +main = defaultMain diff --git a/package.yaml b/package.yaml new file mode 100644 index 0000000..7ec4e93 --- /dev/null +++ b/package.yaml @@ -0,0 +1,36 @@ +name: ci-info +version: 0.1.0.0 +github: "hasura/ci-info-hs" +license: MIT +author: "Ajeet D'Souza" +maintainer: "build@hasura.io" +copyright: "2019 Hasura Inc." + +extra-source-files: +- README.md + +description: Please see the README on GitHub at + +dependencies: +- base >= 4.7 && < 5 +- aeson +- aeson-casing +- hashable +- template-haskell +- th-lift-instances +- text +- unordered-containers + +ghc-options: +- -Wall +- -Wincomplete-uni-patterns +- -Wincomplete-record-updates +- -Wcompat +- -Widentities +- -Wredundant-constraints +- -Wmissing-export-lists +- -Wpartial-fields + +library: + exposed-modules: CI + source-dirs: src diff --git a/res/vendors.json b/res/vendors.json new file mode 100644 index 0000000..2e691b0 --- /dev/null +++ b/res/vendors.json @@ -0,0 +1,159 @@ +[ + { + "name": "AppVeyor", + "constant": "APPVEYOR", + "env": "APPVEYOR", + "pr": "APPVEYOR_PULL_REQUEST_NUMBER" + }, + { + "name": "Azure Pipelines", + "constant": "AZURE_PIPELINES", + "env": "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", + "pr": "SYSTEM_PULLREQUEST_PULLREQUESTID" + }, + { + "name": "Bamboo", + "constant": "BAMBOO", + "env": "bamboo_planKey" + }, + { + "name": "Bitbucket Pipelines", + "constant": "BITBUCKET", + "env": "BITBUCKET_COMMIT", + "pr": "BITBUCKET_PR_ID" + }, + { + "name": "Bitrise", + "constant": "BITRISE", + "env": "BITRISE_IO", + "pr": "BITRISE_PULL_REQUEST" + }, + { + "name": "Buddy", + "constant": "BUDDY", + "env": "BUDDY_WORKSPACE_ID", + "pr": "BUDDY_EXECUTION_PULL_REQUEST_ID" + }, + { + "name": "Buildkite", + "constant": "BUILDKITE", + "env": "BUILDKITE", + "pr": { "env": "BUILDKITE_PULL_REQUEST", "ne": "false" } + }, + { + "name": "CircleCI", + "constant": "CIRCLE", + "env": "CIRCLECI", + "pr": "CIRCLE_PULL_REQUEST" + }, + { + "name": "Cirrus CI", + "constant": "CIRRUS", + "env": "CIRRUS_CI", + "pr": "CIRRUS_PR" + }, + { + "name": "AWS CodeBuild", + "constant": "CODEBUILD", + "env": "CODEBUILD_BUILD_ARN" + }, + { + "name": "Codeship", + "constant": "CODESHIP", + "env": { "CI_NAME": "codeship" } + }, + { + "name": "Drone", + "constant": "DRONE", + "env": "DRONE", + "pr": { "DRONE_BUILD_EVENT": "pull_request" } + }, + { + "name": "dsari", + "constant": "DSARI", + "env": "DSARI" + }, + { + "name": "GitLab CI", + "constant": "GITLAB", + "env": "GITLAB_CI" + }, + { + "name": "GoCD", + "constant": "GOCD", + "env": "GO_PIPELINE_LABEL" + }, + { + "name": "Hudson", + "constant": "HUDSON", + "env": "HUDSON_URL" + }, + { + "name": "Jenkins", + "constant": "JENKINS", + "env": ["JENKINS_URL", "BUILD_ID"], + "pr": { "any": ["ghprbPullId", "CHANGE_ID"] } + }, + { + "name": "Magnum CI", + "constant": "MAGNUM", + "env": "MAGNUM" + }, + { + "name": "Netlify CI", + "constant": "NETLIFY", + "env": "NETLIFY_BUILD_BASE", + "pr": { "env": "PULL_REQUEST", "ne": "false" } + }, + { + "name": "Nevercode", + "constant": "NEVERCODE", + "env": "NEVERCODE", + "pr": { "env": "NEVERCODE_PULL_REQUEST", "ne": "false" } + }, + { + "name": "Sail CI", + "constant": "SAIL", + "env": "SAILCI", + "pr": "SAIL_PULL_REQUEST_NUMBER" + }, + { + "name": "Semaphore", + "constant": "SEMAPHORE", + "env": "SEMAPHORE", + "pr": "PULL_REQUEST_NUMBER" + }, + { + "name": "Shippable", + "constant": "SHIPPABLE", + "env": "SHIPPABLE", + "pr": { "IS_PULL_REQUEST": "true" } + }, + { + "name": "Solano CI", + "constant": "SOLANO", + "env": "TDDIUM", + "pr": "TDDIUM_PR_ID" + }, + { + "name": "Strider CD", + "constant": "STRIDER", + "env": "STRIDER" + }, + { + "name": "TaskCluster", + "constant": "TASKCLUSTER", + "env": ["TASK_ID", "RUN_ID"] + }, + { + "name": "TeamCity", + "constant": "TEAMCITY", + "env": "TEAMCITY_VERSION" + }, + { + "name": "Travis CI", + "constant": "TRAVIS", + "env": "TRAVIS", + "pr": { "env": "TRAVIS_PULL_REQUEST", "ne": "false" } + } +] diff --git a/src/CI.hs b/src/CI.hs new file mode 100644 index 0000000..1d203a5 --- /dev/null +++ b/src/CI.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module CI + ( Types.CI(..) + , isCI + , getCI + ) where + +import Control.Arrow ((***)) +import Data.Bool (bool) +import Data.Foldable (find) +import Data.Maybe (isJust) +import System.Environment (getEnvironment) + +import CI.TH (getVendors) + +import qualified Data.HashMap.Strict as HashMap +import qualified Data.Text as T + +import qualified CI.Types as Types + + +vendors :: [Types.Vendor] +vendors = $(getVendors) + +getCI :: IO (Maybe Types.CI) +getCI = do + env <- mkEnvMap <$> getEnvironment + let maybeVendor = find (checkVendor env) vendors + return + $ case maybeVendor of + Nothing -> bool Nothing (Just Types.CI_UNKNOWN_VENDOR) + $ checkUnknownVendor env + Just vendor -> Just $ Types.vendorConstant vendor + where + checkVendor env vendor = case Types.vendorEnv vendor of + (Types.VendorEnvString text) -> HashMap.member text env + (Types.VendorEnvList list) -> all (`HashMap.member` env) list + (Types.VendorEnvObject hashMap) -> all + (\(k, v) -> HashMap.lookup k env == Just v) + $ HashMap.toList hashMap + + -- check vendor neutral environment variables + checkUnknownVendor env = any (`HashMap.member` env) unknownVendorEnvVars + + unknownVendorEnvVars = map + Types.EnvVarName + [ "CI" -- Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari + , "CONTINUOUS_INTEGRATION" -- Travis CI, Cirrus CI + , "BUILD_NUMBER" -- Jenkins, TeamCity + , "RUN_ID" -- TaskCluster, dsari + ] + + mkEnvMap = HashMap.fromList + . map (Types.EnvVarName . T.pack *** Types.EnvVarValue . T.pack) + +isCI :: IO Bool +isCI = isJust <$> getCI diff --git a/src/CI/TH.hs b/src/CI/TH.hs new file mode 100644 index 0000000..2a615d2 --- /dev/null +++ b/src/CI/TH.hs @@ -0,0 +1,22 @@ +module CI.TH + ( getVendors + ) where + +import qualified Data.Aeson as Aeson +import qualified Language.Haskell.TH as TH +import qualified Language.Haskell.TH.Syntax as TH + +import qualified CI.Types as Types + + +getVendors :: TH.Q TH.Exp +getVendors = TH.runIO readVendors >>= TH.lift + where + vendorsPath = "res/vendors.json" + + readVendors :: IO [Types.Vendor] + readVendors = do + vendors <- Aeson.eitherDecodeFileStrict' vendorsPath + case vendors of + Left e -> fail $ "parsing vendors.json failed: " <> e + Right v -> return v diff --git a/src/CI/Types.hs b/src/CI/Types.hs new file mode 100644 index 0000000..02eec7c --- /dev/null +++ b/src/CI/Types.hs @@ -0,0 +1,103 @@ +{-# LANGUAGE DeriveLift #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module CI.Types + ( CI(..) + , Vendor(..) + , VendorEnv(..) + , EnvVarName(..) + , EnvVarValue(..)) where + +import Data.Hashable (Hashable) +import Data.HashMap.Strict (HashMap) +import Data.Text (Text) +import Instances.TH.Lift () + +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Casing as Aeson +import qualified Data.Aeson.TH as Aeson +import qualified Data.HashMap.Strict as HashMap +import qualified Data.Text as T +import qualified Language.Haskell.TH.Syntax as TH + + +data CI = + CI_APPVEYOR -- http://www.appveyor.com/ + | CI_AZURE_PIPELINES -- https://azure.microsoft.com/en-us/services/devops/pipelines/ + | CI_BAMBOO -- https://www.atlassian.com/software/bamboo/ + | CI_BITBUCKET -- https://bitbucket.org/product/features/pipelines/ + | CI_BITRISE -- https://www.bitrise.io/ + | CI_BUDDY -- https://buddy.works/ + | CI_BUILDKITE -- https://buildkite.com/ + | CI_CIRCLE -- http://circleci.com/ + | CI_CIRRUS -- https://cirrus-ci.org/ + | CI_CODEBUILD -- https://aws.amazon.com/codebuild/ + | CI_CODESHIP -- https://codeship.com/ + | CI_DRONE -- https://drone.io/ + | CI_DSARI -- https://github.com/rfinnie/dsari/ + | CI_GITLAB -- https://about.gitlab.com/gitlab-ci/ + | CI_GOCD -- https://www.go.cd/ + | CI_HUDSON -- http://hudson-ci.org/ + | CI_JENKINS -- https://jenkins-ci.org/ + | CI_MAGNUM -- https://magnum-ci.com/ + | CI_NETLIFY -- https://www.netlify.com/ + | CI_NEVERCODE -- http://nevercode.io/ + | CI_SAIL -- https://sail.ci/ + | CI_SEMAPHORE -- https://semaphoreci.com/ + | CI_SHIPPABLE -- https://www.shippable.com/ + | CI_SOLANO -- https://www.solanolabs.com/ + | CI_STRIDER -- https://strider-cd.github.io/ + | CI_TASKCLUSTER -- http://docs.taskcluster.net/ + | CI_TEAMCITY -- https://www.jetbrains.com/teamcity/ + | CI_TRAVIS -- http://travis-ci.org/ + | CI_UNKNOWN_VENDOR + deriving (Eq, Show, TH.Lift) + +$(Aeson.deriveJSON + Aeson.defaultOptions { Aeson.constructorTagModifier = drop $ T.length "CI_" } + ''CI) + +instance (TH.Lift k, TH.Lift v) => TH.Lift (HashMap k v) where + lift hashMap = [|HashMap.fromList $(TH.lift $ HashMap.toList hashMap)|] + +newtype EnvVarName = EnvVarName { unEnvVarName :: Text } + deriving (Eq, Hashable, Show, Aeson.FromJSON, Aeson.FromJSONKey, Aeson.ToJSON + , Aeson.ToJSONKey, TH.Lift) + +newtype EnvVarValue = EnvVarValue { unEnvVarValue :: Text } + deriving (Eq, Show, Aeson.FromJSON, Aeson.ToJSON, TH.Lift) + +data VendorEnv = VendorEnvString !EnvVarName + | VendorEnvList ![EnvVarName] + | VendorEnvObject !(HashMap EnvVarName EnvVarValue) + deriving (Eq, Show, TH.Lift) + +instance Aeson.FromJSON VendorEnv where + parseJSON val = case val of + Aeson.String _ -> VendorEnvString <$> Aeson.parseJSON val + Aeson.Array _ -> VendorEnvList <$> Aeson.parseJSON val + Aeson.Object _ -> VendorEnvObject <$> Aeson.parseJSON val + _ -> fail + "expected String, List[String], or Map[String, String] in vendor env" + +instance Aeson.ToJSON VendorEnv where + toJSON val = case val of + VendorEnvString name -> Aeson.toJSON name + VendorEnvList list -> Aeson.toJSON list + VendorEnvObject object -> Aeson.toJSON object + +newtype VendorName = VendorName { unVendorName :: Text } + deriving (Eq, Show, Aeson.FromJSON, Aeson.ToJSON, TH.Lift) + +data Vendor = Vendor { vendorName :: !VendorName + , vendorConstant :: !CI + , vendorEnv :: !VendorEnv + } + deriving (Eq, Show, TH.Lift) + +$(Aeson.deriveJSON (Aeson.aesonPrefix Aeson.snakeCase) ''Vendor) diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..d56f56d --- /dev/null +++ b/stack.yaml @@ -0,0 +1,64 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: lts-13.25 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# using the same syntax as the packages field. +# (e.g., acme-missiles-0.3) +# extra-deps: [] + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.9" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor