diff --git a/admin/BUILD.bazel b/admin/BUILD.bazel index fd3d92b..de9a15c 100644 --- a/admin/BUILD.bazel +++ b/admin/BUILD.bazel @@ -1,15 +1,15 @@ sh_test( - name = "settings_test", + name = "repos_test", size = "small", srcs = ["@perl"], args = [ - "$(location settings_test.pl)", + "$(location repos_test.pl)", "$(location //:.gitmodules)", - "$(location settings.yaml)", + "$(location repos.yaml)", ], data = [ - "settings.yaml", - "settings_test.pl", + "repos.yaml", + "repos_test.pl", "//:.gitmodules", ], ) diff --git a/admin/push b/admin/push index 882a7fd..8c2a8bc 100755 --- a/admin/push +++ b/admin/push @@ -4,4 +4,4 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" set -eux -bazel run //hs-github-tools/tools:hub-settings -- "$SCRIPT_DIR/settings.yaml" "$@" +bazel run //hs-github-tools/tools:hub-settings -- "$SCRIPT_DIR/repos.yaml" "$@" diff --git a/admin/settings.yaml b/admin/repos.yaml similarity index 90% rename from admin/settings.yaml rename to admin/repos.yaml index 1149a71..391d6de 100644 --- a/admin/settings.yaml +++ b/admin/repos.yaml @@ -43,6 +43,44 @@ _common: - "common / restyled" - "release / update_release_draft" + rulesets: &rulesets + "master": + target: "branch" + enforcement: "active" + conditions: + ref_name: + exclude: [] + include: + - "~ALL" + rules: + - type: "deletion" + - type: "non_fast_forward" + - type: "creation" + - type: "required_linear_history" + - type: "required_signatures" + - type: "required_status_checks" + - type: "pull_request" + parameters: + required_approving_review_count: 1 + dismiss_stale_reviews_on_push: false + require_code_owner_review: true + require_last_push_approval: false + required_review_thread_resolution: true + automatic_copilot_code_review_enabled: false + allowed_merge_methods: + - "squash" + - "rebase" + bypass_actors: + # Imgbot + - actor_id: 4706 + actor_type: "Integration" + bypass_mode: "always" + # Dependabot + - actor_id: 29110 + actor_type: "Integration" + bypass_mode: "always" + current_user_can_bypass: "never" + # GitHub Issue/PR labels. labels: &labels ############################################################################# @@ -187,6 +225,7 @@ _common: topics: tox labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -215,6 +254,7 @@ btox: color: "ededed" description: "Work in progress" + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -247,6 +287,7 @@ ci-tools: topics: haskell, ci labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -298,6 +339,7 @@ c-toxcore: color: "0052cc" description: "Audio/video" + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -313,35 +355,43 @@ c-toxcore: - "release / update_release_draft" # Custom - "CodeFactor" + - "TokTok.c-toxcore" + - "TokTok.c-toxcore (vcpkg shared)" + - "TokTok.c-toxcore (vcpkg static)" - "analysis (autotools)" - "analysis (clang-tidy)" - "analysis (compcert)" - "analysis (cppcheck)" - "analysis (doxygen)" + - "analysis (freebsd)" - "analysis (goblint)" - "analysis (infer)" - "analysis (misra)" - "analysis (modules)" + - "analysis (pkgsrc)" - "analysis (rpm)" + - "analysis (slimcc)" + - "analysis (sparse)" - "analysis (tcc)" - "analysis (tokstyle)" + - "build-freebsd" + - "build-netbsd" - "build-windows-msvc (2019)" - "build-windows-msvc (2022)" - "bazel-dbg" - "bazel-opt" - "build-android" - "build-macos" - - "ci/circleci: asan" + - "checks / check-release" - "ci/circleci: bazel-asan" - "ci/circleci: bazel-msan" - - "ci/circleci: bazel-tsan" + - "ci/circleci: cimplefmt" - "ci/circleci: clang-analyze" - "ci/circleci: cpplint" + - "ci/circleci: generate-events" - "ci/circleci: static-analysis" - - "ci/circleci: tsan" - - "ci/circleci: ubsan" - "cimple" - - "cimplefmt" + - "circleci" - "coverage-linux" - "docker-bootstrap-node" - "docker-bootstrap-node-websocket" @@ -353,7 +403,9 @@ c-toxcore: - "docker-windows-mingw (64)" - "freebsd" - "mypy" - - "program-analysis" + - "sanitizer (asan)" + - "sanitizer (tsan)" + - "sanitizer (ubsan)" - "sonar-scan" c-toxcore-hs: @@ -364,6 +416,7 @@ c-toxcore-hs: topics: toxcore labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -388,6 +441,7 @@ dockerfiles: topics: docker, android, windows, qt, buildfarm, bazel labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -415,13 +469,24 @@ dockerfiles: - "buildfarm (worker)" - "CodeFactor" - "qtox (alpine)" - - "qtox (android-builder)" + - "qtox (alpine-appimage)" + - "qtox (alpine-static)" - "qtox (debian)" - "qtox (fedora)" - "qtox (flatpak-builder)" - "qtox (ubuntu-lts)" - "qtox (windows-builder.i686)" - "qtox (windows-builder.x86_64)" + - "qtox-android (arm64-v8a, debug, 6.2.4)" + - "qtox-android (arm64-v8a, debug, 6.8.1)" + - "qtox-android (arm64-v8a, release, 6.2.4)" + - "qtox-android (arm64-v8a, release, 6.8.1)" + - "qtox-android (armeabi-v7a, debug, 6.2.4)" + - "qtox-android (armeabi-v7a, debug, 6.8.1)" + - "qtox-android (armeabi-v7a, release, 6.2.4)" + - "qtox-android (armeabi-v7a, release, 6.8.1)" + - "qtox-host-qt (6.2.4)" + - "qtox-host-qt (6.8.1)" experimental: editRepo: @@ -431,6 +496,7 @@ experimental: homepage: "https://toktok.ltd" labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -451,6 +517,7 @@ go-toxcore-c: topics: toxcore labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -475,6 +542,7 @@ hs-apigen: topics: c, ffi, codegen labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -501,6 +569,7 @@ hs-cimple: topics: c, dsl, parser labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -527,6 +596,7 @@ hs-github-tools: topics: github, haskell labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -553,6 +623,7 @@ hs-happy-arbitrary: topics: grammar, parsers labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -579,6 +650,7 @@ hs-msgpack-arbitrary: topics: msgpack, haskell labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -606,6 +678,7 @@ hs-msgpack-binary: topics: msgpack, haskell labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -632,6 +705,7 @@ hs-msgpack-json: topics: json, msgpack labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -658,6 +732,7 @@ hs-msgpack-rpc-conduit: topics: msgpack, rpc, protocol, network labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -685,6 +760,7 @@ hs-msgpack-testsuite: topics: msgpack, haskell labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -711,6 +787,7 @@ hs-msgpack-types: topics: msgpack, haskell labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -737,6 +814,7 @@ hs-schema: homepage: https://hackage.haskell.org/package/schema labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -763,6 +841,7 @@ hs-tokstyle: topics: linter, style, c labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -789,6 +868,7 @@ hs-toxcore-c: topics: toxcore labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -816,6 +896,7 @@ hs-toxcore: topics: haskell, toxcore, network, p2p labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -851,6 +932,7 @@ hs-toxcore: # homepage: https://hackage.haskell.org/package/toxxi # # labels: *labels +# rulesets: *rulesets # branches: # "master": # <<: *branchProtection @@ -878,6 +960,7 @@ js-toxcore-c: topics: "toxcore, js, ffi" labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -910,6 +993,7 @@ jvm-toxcore-c: topics: "toxcore, tox, java, kotlin, ffi" labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -942,6 +1026,7 @@ py-toxcore-c: color: "ededed" description: "Error" + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -970,6 +1055,7 @@ qTox: has_wiki: true labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -986,21 +1072,19 @@ qTox: # Custom - "bazel-opt" - "Alpine (full, Debug)" - - "Android (arm64-v8a, Release)" + - "Alpine (static) (Release)" + - "Android (arm64-v8a, Release, 6.2.4)" - "Check for translatable strings" - "Clang-Tidy" - "Debian (minimal, Debug)" - "Docs" - "Fedora with ASAN (full, Debug)" - "Flatpak" + - "Test macOS distributable (arm64)" - "Ubuntu LTS (full, Release)" - "Update nightly release tag" - - "Windows (i686, Debug)" - - "Windows (i686, Release)" - - "Windows (x86_64, Debug)" - "Windows (x86_64, Release)" - "macOS distributable (arm64)" - - "macOS distributable (x86_64)" - "macOS user (x86_64)" spec: @@ -1012,6 +1096,7 @@ spec: topics: tox, protocol, toxcore labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -1034,6 +1119,7 @@ toktok-stack: homepage: "https://toktok.ltd" labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -1061,6 +1147,7 @@ toxic: topics: tox, console, chat labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -1089,6 +1176,7 @@ toxins: topics: toxcore labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -1113,6 +1201,7 @@ website: topics: toktok, tox labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection @@ -1143,6 +1232,7 @@ zig-toxcore-c: homepage: "https://toktok.ltd" labels: *labels + rulesets: *rulesets branches: "master": <<: *branchProtection diff --git a/admin/settings_test.pl b/admin/repos_test.pl similarity index 55% rename from admin/settings_test.pl rename to admin/repos_test.pl index 5706af7..49e5dbf 100644 --- a/admin/settings_test.pl +++ b/admin/repos_test.pl @@ -3,7 +3,7 @@ use strict; use warnings FATAL => 'all'; -my ( $GITMODULES, $SETTINGS ) = @ARGV; +my ( $GITMODULES, $REPOS ) = @ARGV; sub read_file { my $file = shift; @@ -13,33 +13,33 @@ sub read_file { } my @gitmodules = read_file($GITMODULES); # .gitmodules -my @settings = read_file($SETTINGS); # settings.yaml +my @repos = read_file($REPOS); # repos.yaml my @git_modules = grep { !m!\.wiki$! } map { m!\turl = https://github.com/TokTok/([^/]+)\n! ? lc $1 : () } @gitmodules; -my @settings_modules = - map { m!^ name: "?([^"]+)"?\n! ? lc $1 : () } @settings; +my @repos_modules = + map { m!^ name: "?([^"]+)"?\n! ? lc $1 : () } @repos; # This is the container, so isn't in .gitmodules. push @git_modules, 'toktok-stack'; -# Not in settings.yaml because this is just a storage repo. +# Not in repos.yaml because this is just a storage repo. # We don't need checks for it. -push @settings_modules, 'toktok-fuzzer'; +push @repos_modules, 'toktok-fuzzer'; my $ok = 1; -for my $module (@settings_modules) { +for my $module (@repos_modules) { if ( !grep { $_ eq $module } @git_modules ) { - print "Module $module is in settings.yaml but not in .gitmodules\n"; + print "Module $module is in repos.yaml but not in .gitmodules\n"; $ok = 0; } } for my $module (@git_modules) { - if ( !grep { $_ eq $module } @settings_modules ) { - print "Module $module is in .gitmodules but not in settings.yaml\n"; + if ( !grep { $_ eq $module } @repos_modules ) { + print "Module $module is in .gitmodules but not in repos.yaml\n"; $ok = 0; } } diff --git a/src/GitHub/Paths/Repos/Rulesets.hs b/src/GitHub/Paths/Repos/Rulesets.hs new file mode 100644 index 0000000..914fe88 --- /dev/null +++ b/src/GitHub/Paths/Repos/Rulesets.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +module GitHub.Paths.Repos.Rulesets where + +import Data.Aeson (Value, encode) +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Vector (Vector) +import GitHub.Data.Request (CommandMethod (Patch, Post, Put), + FetchCount (FetchAll), RW (..), Request, + command, pagedQuery) +import GitHub.Types.Settings (Ruleset (Ruleset, rulesetId)) + +addRulesetR :: Text -> Text -> Ruleset -> Request 'RW Value +addRulesetR user repo = + command Post ["repos", user, repo, "rulesets"] . encode + +getRulesetsR :: Text -> Text -> Request 'RO (Vector Ruleset) +getRulesetsR user repo = + pagedQuery ["repos", user, repo, "rulesets"] [] FetchAll + +updateRulesetR :: Text -> Text -> Int -> Ruleset -> Request 'RW Value +updateRulesetR user repo rId = + command Put ["repos", user, repo, "rulesets", Text.pack $ show rId] . encode diff --git a/src/GitHub/Tools/Settings.hs b/src/GitHub/Tools/Settings.hs index 2efe446..210e3c3 100644 --- a/src/GitHub/Tools/Settings.hs +++ b/src/GitHub/Tools/Settings.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -23,9 +22,10 @@ import qualified GitHub import qualified GitHub.Paths.Repos as Repos import qualified GitHub.Paths.Repos.Branches as Branches import qualified GitHub.Paths.Repos.Labels as Labels +import qualified GitHub.Paths.Repos.Rulesets as Rulesets import GitHub.Tools.Requests (mutate, mutate_, request) import GitHub.Types.Settings (Label (Label, labelName), - Settings (..)) + RepoSettings (..), Ruleset (..)) import Network.HTTP.Client (Manager, newManager) import Network.HTTP.Client.TLS (tlsManagerSettings) @@ -35,22 +35,34 @@ debug = False delete :: Bool delete = False +getRulesetId :: V.Vector Ruleset -> Text -> Maybe Int +getRulesetId rulesets name = case V.find ((Just name ==) . rulesetName) rulesets of + Just Ruleset{rulesetId = Just rId} -> return rId + _ -> Nothing + syncSettings :: GitHub.Auth - -> HashMap Text Settings + -> HashMap Text RepoSettings -> Text -> IO () syncSettings auth repos repoFilter = do -- Initialise HTTP manager so we can benefit from keep-alive connections. mgr <- newManager tlsManagerSettings - forM_ (sortOn fst . filterRepos . each $ repos) $ \(repo, Settings{..}) -> do - editRes <- mutate auth mgr (Repos.editRepoR "TokTok" repo settingsEditRepo) + forM_ (sortOn fst . filterRepos . each $ repos) $ \(repo, RepoSettings{..}) -> do + editRes <- mutate auth mgr (Repos.editRepoR "TokTok" repo repoSettingsEditRepo) when debug $ BS.putStrLn $ encode editRes - syncLabels auth mgr repo settingsLabels - forM_ (maybe [] each settingsBranches) $ \(branch, update) -> do + syncLabels auth mgr repo repoSettingsLabels + forM_ (maybe [] each repoSettingsBranches) $ \(branch, update) -> do protRes <- mutate auth mgr (Branches.addProtectionR "TokTok" repo branch update) when debug $ BS.putStrLn $ encode protRes + rulesets <- request (Just auth) mgr (Rulesets.getRulesetsR "TokTok" repo) + forM_ (maybe [] each repoSettingsRulesets) $ \(name, ruleset) -> do + let namedRuleset = ruleset{rulesetName = Just name} + rulesetRes <- case getRulesetId rulesets name of + Just rId -> mutate auth mgr (Rulesets.updateRulesetR "TokTok" repo rId namedRuleset) + Nothing -> mutate auth mgr (Rulesets.addRulesetR "TokTok" repo namedRuleset) + when debug $ BS.putStrLn $ encode rulesetRes where filterRepos = filter ((repoFilter `Text.isPrefixOf`) . fst) @@ -75,17 +87,17 @@ syncLabels auth mgr repo labels = do BS.putStrLn $ encode res -validateSettings :: MonadFail m => HashMap Text Settings -> m () +validateSettings :: MonadFail m => HashMap Text RepoSettings -> m () validateSettings repos = do - commonBranches <- case HashMap.lookup "_common" repos >>= settingsBranches of + commonBranches <- case HashMap.lookup "_common" repos >>= repoSettingsBranches of Nothing -> fail "no _common section found" Just ok -> return ok commonContexts <- case HashMap.lookup "master" commonBranches of Nothing -> fail "no \"master\" branch in _common section found" Just ok -> getContexts "_common" "master" =<< getRequiredStatusChecks "_common" "master" ok -- Check that each repo's branch protection contexts start with the common ones. - forM_ (filterRepos . each $ repos) $ \(repo, Settings{..}) -> - forM_ (maybe [] each settingsBranches) $ \(branch, update) -> do + forM_ (filterRepos . each $ repos) $ \(repo, RepoSettings{..}) -> + forM_ (maybe [] each repoSettingsBranches) $ \(branch, update) -> do contexts <- getContexts repo branch =<< getRequiredStatusChecks repo branch update let ctx = repo <> ".branches." <> branch <> ".required_status_checks.contexts" unless (commonContexts `isPrefixOf` contexts) $ diff --git a/src/GitHub/Types/Settings.hs b/src/GitHub/Types/Settings.hs index 010f1cf..6941c2e 100644 --- a/src/GitHub/Types/Settings.hs +++ b/src/GitHub/Types/Settings.hs @@ -1,7 +1,8 @@ {-# LANGUAGE TemplateHaskell #-} module GitHub.Types.Settings ( Label (..) - , Settings (..) + , Ruleset (..) + , RepoSettings (..) ) where import Data.Aeson (Value) @@ -9,18 +10,37 @@ import Data.Aeson.TH (Options (fieldLabelModifier), defaultOptions, deriveJSON) import Data.HashMap.Strict (HashMap) import Data.Text (Text) -import Text.Casing (camel) +import Text.Casing (camel, quietSnake) data Label = Label { labelName :: Maybe Text , labelDescription :: Maybe Text , labelColor :: Text } deriving (Show, Eq) -$(deriveJSON defaultOptions{fieldLabelModifier = camel . drop (length "Label")} ''Label) +$(deriveJSON defaultOptions{fieldLabelModifier = quietSnake . drop (length "Label")} ''Label) -data Settings = Settings - { settingsEditRepo :: Value - , settingsBranches :: Maybe (HashMap Text Value) - , settingsLabels :: HashMap Text Label +data Ruleset = Ruleset + { rulesetId :: Maybe Int + , rulesetName :: Maybe Text + , rulesetTarget :: Text + , rulesetSourceType :: Maybe Text + , rulesetSource :: Maybe Text + , rulesetEnforcement :: Text + , rulesetNodeId :: Maybe Text + , rulesetConditions :: Maybe (HashMap Text Value) + , rulesetRules :: Maybe [HashMap Text Value] + , rulesetCreatedAt :: Maybe Text + , rulesetUpdatedAt :: Maybe Text + , rulesetBypassActors :: Maybe [HashMap Text Value] + , rulesetCurrentUserCanBypass :: Maybe Text + , rulesetLinks :: Maybe (HashMap Text Value) } deriving (Show) -$(deriveJSON defaultOptions{fieldLabelModifier = camel . drop (length "Settings")} ''Settings) +$(deriveJSON defaultOptions{fieldLabelModifier = quietSnake . drop (length "Ruleset")} ''Ruleset) + +data RepoSettings = RepoSettings + { repoSettingsEditRepo :: Value + , repoSettingsBranches :: Maybe (HashMap Text Value) + , repoSettingsRulesets :: Maybe (HashMap Text Ruleset) + , repoSettingsLabels :: HashMap Text Label + } deriving (Show) +$(deriveJSON defaultOptions{fieldLabelModifier = camel . drop (length "RepoSettings")} ''RepoSettings)