cloud-platform-repository-checker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46a88955fe8e5cbfdc0df49317cc0bc5706e37319df3467107696ff40da9167f
4
+ data.tar.gz: dcbd98ac827af616d4ad056d5dca408d8d76d2d476f439b6d230685061fd1eef
5
+ SHA512:
6
+ metadata.gz: fedd1dba7604e13daa9d6db89fc1c3e63f616a4f3cf5fbf8479bf0f77ac137cecc605ec51a8fc5fd5ef9b5e0fdc352bbfbdd9045e040b8c326bd67fde0590d56
7
+ data.tar.gz: 16e38af1b932b204dae2e3e46d52677433e1904b76fedd5b54a1895eb27c381afb66416049ff36f25cc3c42a9cc3d44cff53ad50319ef2d05667eb3ca75552a0
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem "octokit"
8
+
9
+ group :development do
10
+ gem "pry-byebug"
11
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.7.0)
5
+ public_suffix (>= 2.0.2, < 5.0)
6
+ byebug (11.1.3)
7
+ coderay (1.1.2)
8
+ faraday (1.0.1)
9
+ multipart-post (>= 1.2, < 3)
10
+ method_source (1.0.0)
11
+ multipart-post (2.1.1)
12
+ octokit (4.18.0)
13
+ faraday (>= 0.9)
14
+ sawyer (~> 0.8.0, >= 0.5.3)
15
+ pry (0.13.1)
16
+ coderay (~> 1.1)
17
+ method_source (~> 1.0)
18
+ pry-byebug (3.9.0)
19
+ byebug (~> 11.0)
20
+ pry (~> 0.13.0)
21
+ public_suffix (4.0.5)
22
+ sawyer (0.8.2)
23
+ addressable (>= 2.3.5)
24
+ faraday (> 0.8, < 2.0)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ octokit
31
+ pry-byebug
32
+
33
+ BUNDLED WITH
34
+ 2.1.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Ministry of Justice
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # cloud-platform-repository-checker
2
+ Checks all Cloud Platform repositories for compliance
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Script to list repositories in the ministryofjustice organisation whose names
4
+ # match a regular expression, and output a JSON report of how well they
5
+ # do/don't comply with our team-wide standards for how github repositories
6
+ # should be configured.
7
+
8
+ require "bundler/setup"
9
+ require "json"
10
+ require "net/http"
11
+ require "uri"
12
+ require "octokit"
13
+
14
+ libdir = File.join(".", File.dirname(__FILE__), "..", "lib")
15
+ require File.join(libdir, "github_graph_ql_client")
16
+ require File.join(libdir, "repository_lister")
17
+ require File.join(libdir, "repository_report")
18
+
19
+ ############################################################
20
+
21
+ params = {
22
+ organization: ENV.fetch("ORGANIZATION"),
23
+ regexp: Regexp.new(ENV.fetch("REGEXP")),
24
+ team: ENV.fetch("TEAM"),
25
+ github_token: ENV.fetch("GITHUB_TOKEN")
26
+ }
27
+
28
+ repositories = RepositoryLister.new(params)
29
+ .repository_names
30
+ .inject([]) do |arr, repo_name|
31
+ report = RepositoryReport.new(params.merge(repo_name: repo_name)).report
32
+ arr << report
33
+ end
34
+
35
+ puts({
36
+ repositories: repositories,
37
+ updated_at: Time.now
38
+ }.to_json)
data/env.example ADDED
@@ -0,0 +1,4 @@
1
+ export GITHUB_TOKEN="your github personal access token with public_repos scope"
2
+ export ORGANIZATION="ministryofjustice"
3
+ export TEAM="WebOps"
4
+ export REGEXP="^cloud-platform-*"
@@ -0,0 +1,24 @@
1
+ class GithubGraphQlClient
2
+ attr_reader :github_token
3
+
4
+ GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
5
+
6
+ def initialize(params)
7
+ @github_token = params.fetch(:github_token)
8
+ end
9
+
10
+ private
11
+
12
+ def run_query(params)
13
+ body = params.fetch(:body)
14
+ token = params.fetch(:token)
15
+
16
+ json = {query: body}.to_json
17
+ headers = {"Authorization" => "bearer #{token}"}
18
+
19
+ uri = URI.parse(GITHUB_GRAPHQL_URL)
20
+ resp = Net::HTTP.post(uri, json, headers)
21
+
22
+ resp.body
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ class RepositoryLister < GithubGraphQlClient
2
+ attr_reader :organization, :regexp
3
+
4
+ PAGE_SIZE = 100
5
+
6
+ def initialize(params)
7
+ @organization = params.fetch(:organization)
8
+ @regexp = params.fetch(:regexp)
9
+ super(params)
10
+ end
11
+
12
+ # Returns a list of repository names which match `regexp`
13
+ def repository_names
14
+ list_repos
15
+ .filter { |repo| repo["name"] =~ regexp }
16
+ .map { |repo| repo["name"] }
17
+ end
18
+
19
+ private
20
+
21
+ # TODO:
22
+ # * figure out a way to only fetch cloud-platform-* repos
23
+ # * de-duplicate the code
24
+ # * filter out archived repos
25
+ # * filter out disabled repos
26
+ #
27
+ def list_repos
28
+ repos = []
29
+ end_cursor = nil
30
+
31
+ data = get_repos(end_cursor)
32
+ repos = repos + data.fetch("nodes")
33
+ next_page = data.dig("pageInfo", "hasNextPage")
34
+ end_cursor = data.dig("pageInfo", "endCursor")
35
+
36
+ while next_page do
37
+ data = get_repos(end_cursor)
38
+ repos = repos + data.fetch("nodes")
39
+ next_page = data.dig("pageInfo", "hasNextPage")
40
+ end_cursor = data.dig("pageInfo", "endCursor")
41
+ end
42
+
43
+ repos.reject { |r| r.dig("isArchived") || r.dig("isDisabled") }
44
+ end
45
+
46
+ def get_repos(end_cursor = nil)
47
+ json = run_query(
48
+ body: repositories_query(end_cursor),
49
+ token: github_token
50
+ )
51
+
52
+ JSON.parse(json).dig("data", "organization", "repositories")
53
+ end
54
+
55
+ # TODO: it should be possible to exclude disabled/archived repos in this
56
+ # query, but I don't know how to do that yet, so I'm just fetching everything
57
+ # and throwing away the disabled/archived repos later. We should also be able
58
+ # to only fetch repos whose names match the pattern we're interested in, at
59
+ # this stage.
60
+ def repositories_query(end_cursor)
61
+ after = end_cursor.nil? ? "" : %[, after: "#{end_cursor}"]
62
+ %[
63
+ {
64
+ organization(login: "#{organization}") {
65
+ repositories(first: #{PAGE_SIZE} #{after}) {
66
+ nodes {
67
+ id
68
+ name
69
+ isLocked
70
+ isArchived
71
+ isDisabled
72
+ }
73
+ pageInfo {
74
+ hasNextPage
75
+ endCursor
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ]
81
+ end
82
+ end
@@ -0,0 +1,140 @@
1
+ class RepositoryReport < GithubGraphQlClient
2
+ attr_reader :organization, :repo_name, :team
3
+
4
+ MASTER = "master"
5
+ ADMIN = "admin"
6
+ PASS = "PASS"
7
+ FAIL = "FAIL"
8
+
9
+ def initialize(params)
10
+ @organization = params.fetch(:organization)
11
+ @repo_name = params.fetch(:repo_name)
12
+ @team = params.fetch(:team)
13
+ super(params)
14
+ end
15
+
16
+ # TODO: additional checks
17
+ # * has issues enabled
18
+ # * deleteBranchOnMerge
19
+ # * mergeCommitAllowed (do we want this on or off?)
20
+ # * squashMergeAllowed (do we want this on or off?)
21
+
22
+ def report
23
+ {
24
+ organization: organization,
25
+ name: repo_name,
26
+ url: repo_url,
27
+ status: status,
28
+ report: all_checks_result
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ def repo_data
35
+ @repo_data ||= fetch_repo_data
36
+ end
37
+
38
+ def repo_url
39
+ @url ||= repo_data.dig("data", "repository", "url")
40
+ end
41
+
42
+ def status
43
+ all_checks_result.values.all? ? PASS : FAIL
44
+ end
45
+
46
+ def all_checks_result
47
+ @all_checks_result ||= {
48
+ has_master_branch_protection: has_master_branch_protection?,
49
+ requires_approving_reviews: has_branch_protection_property?("requiresApprovingReviews"),
50
+ requires_code_owner_reviews: has_branch_protection_property?("requiresCodeOwnerReviews"),
51
+ administrators_require_review: has_branch_protection_property?("isAdminEnforced"),
52
+ dismisses_stale_reviews: has_branch_protection_property?("dismissesStaleReviews"),
53
+ requires_strict_status_checks: has_branch_protection_property?("requiresStrictStatusChecks"),
54
+ team_is_admin: is_team_admin?,
55
+ }
56
+ end
57
+
58
+ def fetch_repo_data
59
+ body = repo_settings_query(
60
+ organization: organization,
61
+ repo_name: repo_name,
62
+ )
63
+
64
+ json = run_query(
65
+ body: body,
66
+ token: github_token
67
+ )
68
+
69
+ JSON.parse(json)
70
+ end
71
+
72
+ def repo_settings_query(params)
73
+ owner = params.fetch(:organization)
74
+ repo_name = params.fetch(:repo_name)
75
+
76
+ %[
77
+ {
78
+ repository(owner: "#{owner}", name: "#{repo_name}") {
79
+ name
80
+ url
81
+ owner {
82
+ login
83
+ }
84
+ branchProtectionRules(first: 50) {
85
+ edges {
86
+ node {
87
+ pattern
88
+ requiresApprovingReviews
89
+ requiresCodeOwnerReviews
90
+ isAdminEnforced
91
+ dismissesStaleReviews
92
+ requiresStrictStatusChecks
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ]
99
+ end
100
+
101
+ def is_team_admin?
102
+ client = Octokit::Client.new(access_token: github_token)
103
+
104
+ client.repo_teams([organization, repo_name].join("/")).filter do |team|
105
+ team[:name] == team && team[:permission] == ADMIN
106
+ end.any?
107
+ rescue Octokit::NotFound
108
+ # This happens if our token does not have permission to view repo settings
109
+ false
110
+ end
111
+
112
+ def branch_protection_rules
113
+ @rules ||= repo_data.dig("data", "repository", "branchProtectionRules", "edges")
114
+ end
115
+
116
+ def has_master_branch_protection?
117
+ requiring_branch_protection_rules do |rules|
118
+
119
+ rules
120
+ .filter { |edge| edge.dig("node", "pattern") == MASTER }
121
+ .any?
122
+ end
123
+ end
124
+
125
+ def has_branch_protection_property?(property)
126
+ requiring_branch_protection_rules do |rules|
127
+ rules
128
+ .map { |edge| edge.dig("node", property) }
129
+ .all?
130
+ end
131
+ end
132
+
133
+ def requiring_branch_protection_rules
134
+ rules = branch_protection_rules
135
+ return false unless rules.any?
136
+
137
+ yield rules
138
+ end
139
+
140
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud-platform-repository-checker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - David Salgado
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: platforms@digital.justice.gov.uk
15
+ executables:
16
+ - cloud-platform-repository-checker
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - README.md
20
+ files:
21
+ - Gemfile
22
+ - Gemfile.lock
23
+ - LICENSE
24
+ - README.md
25
+ - bin/cloud-platform-repository-checker
26
+ - env.example
27
+ - lib/github_graph_ql_client.rb
28
+ - lib/repository_lister.rb
29
+ - lib/repository_report.rb
30
+ homepage: https://github.com/ministryofjustice/cloud-platform
31
+ licenses: []
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options:
35
+ - "--main"
36
+ - README.md
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.0.3
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: What this thing does
54
+ test_files: []