diff --git a/BUILD.bazel b/BUILD.bazel index d4c150a..13c55d0 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -18,6 +18,8 @@ haskell_library( "//third_party/haskell:QuickCheck", "//third_party/haskell:aeson", "//third_party/haskell:base", + "//third_party/haskell:casing", + "//third_party/haskell:generic-arbitrary", "//third_party/haskell:quickcheck-text", "//third_party/haskell:text", ], @@ -64,6 +66,7 @@ haskell_library( "//third_party/haskell:aeson", "//third_party/haskell:base", "//third_party/haskell:bytestring", + "//third_party/haskell:casing", "//third_party/haskell:containers", "//third_party/haskell:cryptohash", "//third_party/haskell:directory", @@ -83,6 +86,7 @@ haskell_library( "//third_party/haskell:unordered-containers", "//third_party/haskell:uuid", "//third_party/haskell:vector", + "//third_party/haskell:yaml", ], ) diff --git a/admin/push b/admin/push new file mode 100755 index 0000000..882a7fd --- /dev/null +++ b/admin/push @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" + +set -eux + +bazel run //hs-github-tools/tools:hub-settings -- "$SCRIPT_DIR/settings.yaml" "$@" diff --git a/admin/settings.yaml b/admin/settings.yaml new file mode 100644 index 0000000..e1a2352 --- /dev/null +++ b/admin/settings.yaml @@ -0,0 +1,650 @@ +_common: + editRepo: &editRepo + private: false + has_issues: true + has_projects: false + has_wiki: false + default_branch: "master" + delete_branch_on_merge: true + is_template: false + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: false + archived: false + security_and_analysis: + secret_scanning: + status: "enabled" + secret_scanning_push_protection: + status: "enabled" + + branchProtection: &branchProtection + enforce_admins: true + required_pull_request_reviews: + require_code_owner_reviews: true + required_signatures: true + restrictions: + users: [] + teams: ["reviewers"] + required_linear_history: true + allow_force_pushes: false + allow_deletions: false + block_creations: true + required_conversation_resolution: true + lock_branch: false + +btox: + editRepo: + <<: *editRepo + name: btox + description: Official TokTok mobile Tox client + homepage: https://tox.chat/ + topics: toxcore, network, p2p, security, encryption, cryptography, android, ios, web + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + # Common + - "Hound" + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "restyled" + # Custom + - "analyze" + - "android-build" + - "format" + - "ios-build" + - "linux-build" + - "macos-build" + - "test" + - "web-build" + - "webtox" + - "windows-build" + +ci-tools: + editRepo: + <<: *editRepo + name: ci-tools + description: Common tools for building TokTok repositories on CI + topics: haskell, ci + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + # Common + - "Hound" + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "restyled" + +c-toxcore: + editRepo: + <<: *editRepo + name: "c-toxcore" + description: "The future of online communications." + homepage: "https://tox.chat" + topics: "toxcore, network, p2p, security, encryption, cryptography" + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + # Common + - "Hound" + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "restyled" + # Custom + - "CodeFactor" + - "bazel-dbg" + - "bazel-opt" + - "build-android" + - "build-macos" + - "ci/circleci: asan" + - "ci/circleci: bazel-asan" + - "ci/circleci: bazel-msan" + - "ci/circleci: bazel-tsan" + - "ci/circleci: clang-analyze" + - "ci/circleci: cpplint" + - "ci/circleci: infer" + - "ci/circleci: static-analysis" + - "ci/circleci: tsan" + - "ci/circleci: ubsan" + - "cimple" + - "cimplefmt" + - "common / buildifier" + - "coverage-linux" + - "docker-bootstrap-node" + - "docker-bootstrap-node-websocket" + - "docker-clusterfuzz" + - "docker-esp32" + - "docker-fuzzer" + - "docker-toxcore-js" + - "docker-win32" + - "docker-win64" + - "freebsd" + - "mypy" + - "program-analysis" + - "sonar-scan" + +c-toxcore-hs: + editRepo: + <<: *editRepo + name: c-toxcore-hs + description: C bindings to the Haskell implementation of the Tox protocol + topics: toxcore + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + +dockerfiles: + editRepo: + <<: *editRepo + name: dockerfiles + description: Dockerfiles for (cross-)compiling TokTok projects for various platforms + topics: docker, ghc, android, windows, qt, buildfarm, bazel + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "build (alpine-s390x)" + - "build (bazel)" + - "build (bazel-cache)" + - "build (flutter-web)" + - "build (frama-c)" + - "build (freebsd)" + - "build (haskell)" + - "ci/circleci: compcert" + - "ci/circleci: infer" + - "CodeFactor" + - "code-review/reviewable" + - "ghc-base" + - "ghc-android (aarch64)" + - "ghc-android (arm)" + +experimental: + editRepo: + <<: *editRepo + name: "experimental" + description: "Experimental - Anyone can submit anything in here" + homepage: "https://toktok.ltd" + + branches: + "master": + <<: *branchProtection + # No code reviews required in experimental. + required_pull_request_reviews: null + required_status_checks: + strict: true + contexts: + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "Hound" + +go-toxcore-c: + editRepo: + <<: *editRepo + name: go-toxcore-c + description: The golang bindings for libtoxcore (Project Tox). + topics: toxcore + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - bazel-opt + - Codacy Static Code Analysis + - code-review/reviewable + +hs-apigen: + editRepo: + <<: *editRepo + name: hs-apigen + description: FFI API generator for TokTok style C API headers. + topics: c, ffi, codegen + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + +hs-cimple: + editRepo: + <<: *editRepo + name: hs-cimple + description: Cimple and Apidsl language parsers and tools + topics: c, dsl, parser + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + +hs-github-tools: + editRepo: + <<: *editRepo + name: hs-github-tools + description: Small GitHub utilities like pull-status and changelog generator + topics: github, haskell + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + # Common + - "Hound" + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "restyled" + # Custom + - "bazel-opt" + - "build / stack" + - "common / buildifier" + - "checks / check-release" + +hs-msgpack-arbitrary: + editRepo: + <<: *editRepo + name: hs-msgpack-arbitrary + description: Arbitrary instance for Data.MessagePack.Types.Object. + topics: msgpack, haskell + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +hs-msgpack-binary: + editRepo: + <<: *editRepo + name: hs-msgpack-binary + description: Haskell implementation of MessagePack / msgpack.org + homepage: http://msgpack.org/ + topics: msgpack, haskell + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +hs-msgpack-rpc-conduit: + editRepo: + <<: *editRepo + name: hs-msgpack-rpc-conduit + description: A MessagePack RPC implementation in Haskell + topics: msgpack, rpc, protocol, network + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +hs-msgpack-testsuite: + editRepo: + <<: *editRepo + name: hs-msgpack-testsuite + description: Haskell implementation of MessagePack / msgpack.org + homepage: http://msgpack.org/ + topics: msgpack, haskell + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +hs-msgpack-types: + editRepo: + <<: *editRepo + name: hs-msgpack-types + description: Abstract data types and type classes for Haskell to MessagePack value converters + topics: msgpack, haskell + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +hs-schema: + editRepo: + <<: *editRepo + name: hs-schema + description: Encoding-independent schemas for Haskell data types + homepage: https://hackage.haskell.org/package/schema + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + +hs-tokstyle: + editRepo: + <<: *editRepo + name: hs-tokstyle + description: Style checker for TokTok C projects + topics: linter, style, c + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "Hound" + - "Mergeable" + - "bazel-opt" + - "build / stack" + - "buildifier" + - "check-release" + - "restyled" + +hs-toxcore-c: + editRepo: + <<: *editRepo + name: hs-toxcore-c + description: Haskell bindings to C toxcore implementation + topics: toxcore + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +hs-toxcore: + editRepo: + <<: *editRepo + name: hs-toxcore + description: Haskell Tox protocol implementation + homepage: https://toktok.ltd/spec + topics: haskell, toxcore, network, p2p + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "android-aarch64" + - "android-arm" + - "android-i686" + - "android-x86_64" + - "bazel-opt" + - "build / stack" + - "checks / check-release" + - "common / buildifier" + +#hs-toxxi: +# editRepo: +# <<: *editRepo +# name: hs-toxxi +# description: A simple console based text client for Tox. +# homepage: https://hackage.haskell.org/package/toxxi +# +# branches: +# "master": +# <<: *branchProtection +# required_status_checks: +# strict: true +# contexts: +# - "bazel-opt" +# - "build / stack" + +js-toxcore-c: + editRepo: + <<: *editRepo + name: "js-toxcore-c" + description: "Node bindings for toxcore" + homepage: "https://toktok.ltd" + topics: "toxcore, js, ffi" + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "Hound" + - "DeepScan" + - "build (12.x)" + - "build (13.x)" + - "docker" + - "codecov/patch" + - "codecov/project" + - "restyled" + - "security/snyk (TokTok)" + +py-toxcore-c: + editRepo: + <<: *editRepo + name: py-toxcore-c + description: Python binding for Project-Tox the skype replacement. + topics: toxcore, python, ffi + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "code-review/reviewable" + +qTox: + editRepo: + <<: *editRepo + name: qTox + description: Powerful Tox chat client that follows the Tox design guidelines. + homepage: https://qtox.github.io/ + topics: tox, chat + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - Codacy/PR Quality Review + +spec: + editRepo: + <<: *editRepo + name: spec + description: Tox Protocol Specification + homepage: https://toktok.ltd/spec + topics: tox, protocol, toxcore + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "Codacy/PR Quality Review" + - "WIP" + - "code-review/reviewable" + +toktok-stack: + editRepo: + <<: *editRepo + name: "toktok-stack" + description: "A snapshot of the complete software stack (excluding some external libraries and programs)" + homepage: "https://toktok.ltd" + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "Analyze (go)" + - "Analyze (python)" + - "CodeQL" + - "Hound" + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "common / buildifier" + - "docker-haskell" + - "docker-test" + - "hie-bios" + - "mypy" + - "restyled" + +toxic: + editRepo: + <<: *editRepo + name: toxic + description: An ncurses-based Tox client + topics: tox, console, chat + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - build + - build-static + - bazel-opt + - Codacy Static Code Analysis + - code-review/reviewable + - infer + +toxins: + editRepo: + <<: *editRepo + name: toxins + description: A collection of small programs using toxcore. + topics: toxcore + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "bazel-opt" + - "Codacy Static Code Analysis" + - "code-review/reviewable" + +website: + editRepo: + <<: *editRepo + name: website + description: The TokTok website + topics: toktok, tox + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "code-review/reviewable" + - "docker" + # Netlify tests: + - "Header rules - toktok" + - "Pages changed - toktok" + - "Redirect rules - toktok" + - "netlify/toktok/deploy-preview" + +zig-toxcore-c: + editRepo: + <<: *editRepo + name: "zig-toxcore-c" + description: "Zig wrapper for c-toxcore library." + homepage: "https://toktok.ltd" + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "restyled" diff --git a/github-tools.cabal b/github-tools.cabal index 098ca12..29ef423 100644 --- a/github-tools.cabal +++ b/github-tools.cabal @@ -128,10 +128,12 @@ library aeson >=2 , base >=4 && <5 , bytestring + , casing , containers , cryptohash , directory , exceptions + , generic-arbitrary , github >=0.25 && <0.29 , html , http-client >=0.4.30 @@ -148,6 +150,7 @@ library , unordered-containers , uuid , vector + , yaml executable hub-automerge main-is: hub-automerge.hs diff --git a/spec/.gitignore b/spec/.gitignore new file mode 100644 index 0000000..fe5b5c8 --- /dev/null +++ b/spec/.gitignore @@ -0,0 +1,2 @@ +/*.json +/*.yaml diff --git a/src/GitHub/Paths/Repos.hs b/src/GitHub/Paths/Repos.hs new file mode 100644 index 0000000..3f876c1 --- /dev/null +++ b/src/GitHub/Paths/Repos.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +module GitHub.Paths.Repos where + +import Data.Aeson (Value, encode) +import Data.Text (Text) +import GitHub.Data.Request (CommandMethod (Patch), RW (..), Request, + command) + +editRepoR :: Text -> Text -> Value -> Request 'RW Value +editRepoR user repo = + command Patch ["repos", user, repo] . encode diff --git a/src/GitHub/Paths/Repos/Branches.hs b/src/GitHub/Paths/Repos/Branches.hs new file mode 100644 index 0000000..5490726 --- /dev/null +++ b/src/GitHub/Paths/Repos/Branches.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +module GitHub.Paths.Repos.Branches where + +import Data.Aeson (Value, encode) +import Data.Text (Text) +import GitHub.Data.Request (CommandMethod (Put), RW (..), Request, + command) + +addProtectionR :: Text -> Text -> Text -> Value -> Request 'RW Value +addProtectionR user repo branch = + command Put ["repos", user, repo, "branches", branch, "protection"] . encode diff --git a/src/GitHub/Tools/Requests.hs b/src/GitHub/Tools/Requests.hs index 1e28a94..dd939b3 100644 --- a/src/GitHub/Tools/Requests.hs +++ b/src/GitHub/Tools/Requests.hs @@ -2,10 +2,23 @@ module GitHub.Tools.Requests where import Control.Monad.Catch (throwM) -import Data.Aeson (FromJSON) +import Data.Aeson (FromJSON, ToJSON (toJSON), + Value (Array, Null, Object)) +import qualified Data.Aeson.KeyMap as KeyMap +import qualified Data.Vector as V import qualified GitHub import Network.HTTP.Client (Manager) +removeNulls :: ToJSON a => a -> Value +removeNulls = go . toJSON + where + go (Array x) = Array . V.map go $ x + go (Object x) = Object . KeyMap.map go . KeyMap.filter (not . isEmpty) $ x + go x = x + + isEmpty Null = True + isEmpty (Array x) = null x + isEmpty _ = False request :: FromJSON a @@ -24,3 +37,15 @@ request auth mgr req = do case auth of Nothing -> GitHub.executeRequestWithMgr' mgr req Just tk -> GitHub.executeRequestWithMgr mgr tk req + +mutate + :: FromJSON a + => GitHub.Auth + -> Manager + -> GitHub.Request 'GitHub.RW a + -> IO a +mutate auth mgr req = do + response <- GitHub.executeRequestWithMgr mgr auth req + case response of + Left err -> throwM err + Right res -> return res diff --git a/src/GitHub/Tools/Settings.hs b/src/GitHub/Tools/Settings.hs new file mode 100644 index 0000000..23be3b8 --- /dev/null +++ b/src/GitHub/Tools/Settings.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +module GitHub.Tools.Settings + ( syncSettings + ) where + +import Control.Monad (forM_) +import Data.Aeson.TH (Options (fieldLabelModifier), + defaultOptions, deriveJSON) +import qualified Data.ByteString.Char8 as BS +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HashMap +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Yaml (Value, encode) +import qualified GitHub +import qualified GitHub.Paths.Repos as Repos +import qualified GitHub.Paths.Repos.Branches as Branches +import GitHub.Tools.Requests (mutate) +import Network.HTTP.Client (newManager) +import Network.HTTP.Client.TLS (tlsManagerSettings) +import Text.Casing (camel) + +data Settings = Settings + { settingsEditRepo :: Value + , settingsBranches :: Maybe (HashMap Text Value) + } +$(deriveJSON defaultOptions{fieldLabelModifier = camel . drop (Text.length "Settings")} ''Settings) + +syncSettings + :: GitHub.Auth + -> HashMap Text Settings + -> Text + -> IO () +syncSettings auth repos repoFilter = do + -- Initialise HTTP manager so we can benefit from keep-alive connections. + mgr <- newManager tlsManagerSettings + + forM_ (filterRepos $ each repos) $ \(repo, Settings{..}) -> do + editRes <- mutate auth mgr (Repos.editRepoR "TokTok" repo settingsEditRepo) + BS.putStrLn $ encode editRes + forM_ (maybe [] each settingsBranches) $ \(branch, update) -> do + protRes <- mutate auth mgr (Branches.addProtectionR "TokTok" repo branch update) + BS.putStrLn $ encode protRes + where + each = HashMap.toList . HashMap.filterWithKey (\k _ -> not $ "_" `Text.isPrefixOf` k) + filterRepos = filter ((repoFilter `Text.isPrefixOf`) . fst) diff --git a/src/GitHub/Types/Base/EditRepo.hs b/src/GitHub/Types/Base/EditRepo.hs new file mode 100644 index 0000000..bcec85d --- /dev/null +++ b/src/GitHub/Types/Base/EditRepo.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE Strict #-} +{-# LANGUAGE TemplateHaskell #-} +module GitHub.Types.Base.EditRepo where + +import Data.Aeson.TH (Options (fieldLabelModifier), + defaultOptions, deriveJSON) +import GHC.Generics (Generic) +import Test.QuickCheck.Arbitrary (Arbitrary (..)) +import Test.QuickCheck.Arbitrary.Generic (genericArbitrary) +import Text.Casing (quietSnake) + +------------------------------------------------------------------------------ +-- EditRepo + +data EditRepo = EditRepo + { editRepoArchived :: Maybe Bool + , editRepoHasProjects :: Maybe Bool + , editRepoDeleteBranchOnMerge :: Maybe Bool + , editRepoIsTemplate :: Maybe Bool + , editRepoAllowSquashMerge :: Maybe Bool + , editRepoHasWiki :: Maybe Bool + , editRepoAllowForking :: Maybe Bool + , editRepoSquashMergeCommitMessage :: Maybe String + , editRepoAllowMergeCommit :: Maybe Bool + , editRepoWebCommitSignoffRequired :: Maybe Bool + , editRepoUseSquashPrTitleAsDefault :: Maybe Bool + , editRepoDescription :: Maybe String + , editRepoSquashMergeCommitTitle :: Maybe String + , editRepoMergeCommitMessage :: Maybe String + , editRepoAllowUpdateBranch :: Maybe Bool + , editRepoPrivate :: Maybe Bool + , editRepoName :: Maybe String + , editRepoMergeCommitTitle :: Maybe String + , editRepoAllowRebaseMerge :: Maybe Bool + , editRepoHasIssues :: Maybe Bool + , editRepoHomepage :: Maybe String + , editRepoDefaultBranch :: Maybe String + , editRepoVisibility :: Maybe String + } deriving (Eq, Generic) +$(deriveJSON defaultOptions{fieldLabelModifier = quietSnake . drop (length "EditRepo")} ''EditRepo) + +instance Arbitrary EditRepo where + arbitrary = genericArbitrary diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 222423f..54c25bf 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -10,6 +10,8 @@ load("@rules_haskell//haskell:defs.bzl", "haskell_binary") "//third_party/haskell:base", "//third_party/haskell:bytestring", "//third_party/haskell:github", + "//third_party/haskell:groom", "//third_party/haskell:text", + "//third_party/haskell:yaml", ], -) for file in glob(["*.hs"])] +) for file in glob(["hub-*.hs"])] diff --git a/tools/hub-settings.hs b/tools/hub-settings.hs new file mode 100644 index 0000000..57d53da --- /dev/null +++ b/tools/hub-settings.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ViewPatterns #-} +module Main (main) where + +import qualified Data.ByteString.Char8 as BS8 +import Data.Maybe (fromMaybe, listToMaybe) +import qualified Data.Text as Text +import Data.Yaml (decodeFileThrow) +import qualified GitHub +import GitHub.Tools.Settings (syncSettings) +import System.Environment (getArgs, lookupEnv) + +main :: IO () +main = do + -- Get auth token from the $GITHUB_TOKEN environment variable. + args <- getArgs + lookupEnv "GITHUB_TOKEN" >>= \case + Nothing -> fail "GITHUB_TOKEN environment variable must be set" + Just (GitHub.OAuth . BS8.pack -> token) -> + case args of + file:repoFilter -> do + yaml <- decodeFileThrow file + syncSettings token yaml (Text.pack . fromMaybe "" . listToMaybe $ repoFilter) + _ -> fail "Usage: hub-settings "