From db9dc2c93d56d7c1b351352a230f5c7d44eb5155 Mon Sep 17 00:00:00 2001 From: iphydf Date: Tue, 2 Jan 2024 02:04:59 +0000 Subject: [PATCH] feat: Add hub-settings tool to sync settings from yaml to github. --- BUILD.bazel | 4 + admin/settings.yaml | 159 +++++++++++++++++++++++++++++ github-tools.cabal | 3 + spec/.gitignore | 2 + src/GitHub/Paths/Repos.hs | 12 +++ src/GitHub/Paths/Repos/Branches.hs | 12 +++ src/GitHub/Tools/Requests.hs | 27 ++++- src/GitHub/Tools/Settings.hs | 46 +++++++++ src/GitHub/Types/Base/EditRepo.hs | 44 ++++++++ tools/BUILD.bazel | 4 +- tools/hub-settings.hs | 22 ++++ 11 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 admin/settings.yaml create mode 100644 spec/.gitignore create mode 100644 src/GitHub/Paths/Repos.hs create mode 100644 src/GitHub/Paths/Repos/Branches.hs create mode 100644 src/GitHub/Tools/Settings.hs create mode 100644 src/GitHub/Types/Base/EditRepo.hs create mode 100644 tools/hub-settings.hs 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/settings.yaml b/admin/settings.yaml new file mode 100644 index 0000000..3a4486f --- /dev/null +++ b/admin/settings.yaml @@ -0,0 +1,159 @@ +_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 + +c-toxcore: + editRepo: + <<: *editRepo + name: "c-toxcore" + description: "The future of online communications." + homepage: "https://tox.chat" + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "CodeFactor" + - "Hound" + - "Mergeable" + - "Milestone Check" + - "WIP" + - "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" + - "code-review/reviewable" + - "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" + - "restyled" + - "sonar-scan" + +experimental: + editRepo: + <<: *editRepo + name: "experimental" + description: "Experimental - Anyone can submit anything in here" + homepage: "https://toktok.ltd" + + branches: + "master": + <<: *branchProtection + required_status_checks: + strict: true + contexts: + - "Mergeable" + - "Milestone Check" + - "WIP" + - "code-review/reviewable" + - "Hound" + +js-toxcore-c: + editRepo: + <<: *editRepo + name: "js-toxcore-c" + description: "Node bindings for toxcore" + homepage: "https://toktok.ltd" + + 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" + - "CodeQL" + - "codecov/patch" + - "codecov/project" + - "restyled" + - "security/snyk (TokTok)" + +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" 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..036e136 --- /dev/null +++ b/src/GitHub/Tools/Settings.hs @@ -0,0 +1,46 @@ +{-# 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 + -> IO () +syncSettings auth repos = do + -- Initialise HTTP manager so we can benefit from keep-alive connections. + mgr <- newManager tlsManagerSettings + + forM_ (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) 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..a5db271 --- /dev/null +++ b/tools/hub-settings.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ViewPatterns #-} +module Main (main) where + +import qualified Data.ByteString.Char8 as BS8 +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] -> do + yaml <- decodeFileThrow file + syncSettings token yaml + _ -> fail "Usage: hub-settings "