gitlab-dangerfiles 0.1.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.gitlab-ci.yml +43 -0
  4. data/.gitlab/merge_request_templates/Release.md +35 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Guardfile +70 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +43 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/fixtures/emojis/aliases.json +542 -0
  14. data/fixtures/emojis/digests.json +12553 -0
  15. data/gitlab-dangerfiles.gemspec +38 -0
  16. data/lib/danger/changelog.rb +39 -0
  17. data/lib/danger/helper.rb +260 -0
  18. data/lib/danger/roulette.rb +135 -0
  19. data/lib/danger/sidekiq_queues.rb +37 -0
  20. data/lib/gitlab-dangerfiles.rb +1 -0
  21. data/lib/gitlab/Dangerfile +1 -0
  22. data/lib/gitlab/dangerfiles.rb +40 -0
  23. data/lib/gitlab/dangerfiles/bundle_size/Dangerfile +38 -0
  24. data/lib/gitlab/dangerfiles/ce_ee_vue_templates/Dangerfile +56 -0
  25. data/lib/gitlab/dangerfiles/changelog/Dangerfile +90 -0
  26. data/lib/gitlab/dangerfiles/changes_size/Dangerfile +17 -0
  27. data/lib/gitlab/dangerfiles/commit_linter.rb +226 -0
  28. data/lib/gitlab/dangerfiles/commit_messages/Dangerfile +135 -0
  29. data/lib/gitlab/dangerfiles/database/Dangerfile +67 -0
  30. data/lib/gitlab/dangerfiles/documentation/Dangerfile +29 -0
  31. data/lib/gitlab/dangerfiles/duplicate_yarn_dependencies/Dangerfile +29 -0
  32. data/lib/gitlab/dangerfiles/emoji_checker.rb +45 -0
  33. data/lib/gitlab/dangerfiles/eslint/Dangerfile +31 -0
  34. data/lib/gitlab/dangerfiles/frozen_string/Dangerfile +28 -0
  35. data/lib/gitlab/dangerfiles/karma/Dangerfile +51 -0
  36. data/lib/gitlab/dangerfiles/metadata/Dangerfile +50 -0
  37. data/lib/gitlab/dangerfiles/popen.rb +55 -0
  38. data/lib/gitlab/dangerfiles/prettier/Dangerfile +41 -0
  39. data/lib/gitlab/dangerfiles/roulette/Dangerfile +97 -0
  40. data/lib/gitlab/dangerfiles/sidekiq_queues/Dangerfile +27 -0
  41. data/lib/gitlab/dangerfiles/specs/Dangerfile +42 -0
  42. data/lib/gitlab/dangerfiles/tasks.rb +19 -0
  43. data/lib/gitlab/dangerfiles/teammate.rb +106 -0
  44. data/lib/gitlab/dangerfiles/telemetry/Dangerfile +32 -0
  45. data/lib/gitlab/dangerfiles/utility_css/Dangerfile +51 -0
  46. data/lib/gitlab/dangerfiles/version.rb +5 -0
  47. metadata +191 -0
@@ -0,0 +1,38 @@
1
+ require_relative "lib/gitlab/dangerfiles/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "gitlab-dangerfiles"
5
+ spec.version = Gitlab::Dangerfiles::VERSION
6
+ spec.authors = ["Rémy Coutable"]
7
+ spec.email = ["remy@rymai.me"]
8
+
9
+ spec.summary = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
10
+ spec.description = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
11
+ spec.homepage = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
19
+ spec.metadata["changelog_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "danger"
31
+
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "rspec-parameterized"
34
+ spec.add_development_dependency "timecop"
35
+ spec.add_development_dependency "webmock"
36
+ spec.add_development_dependency "climate_control"
37
+ spec.add_development_dependency "rufo"
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # Common helper functions for our danger scripts. See Danger::Helper
5
+ # for more details
6
+ class Changelog < Danger::Plugin
7
+ NO_CHANGELOG_LABELS = [
8
+ "backstage", # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360.
9
+ "tooling",
10
+ "tooling::pipelines",
11
+ "tooling::workflow",
12
+ "ci-build",
13
+ "meta",
14
+ ].freeze
15
+ NO_CHANGELOG_CATEGORIES = %i[docs none].freeze
16
+
17
+ def needed?
18
+ categories_need_changelog? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
19
+ end
20
+
21
+ def found
22
+ @found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
23
+ end
24
+
25
+ def sanitized_mr_title
26
+ gitlab.mr_json["title"].gsub(/^WIP: */, "").gsub(/`/, '\\\`')
27
+ end
28
+
29
+ def ee_changelog?
30
+ found.start_with?("ee/")
31
+ end
32
+
33
+ private
34
+
35
+ def categories_need_changelog?
36
+ (helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require_relative "../gitlab/dangerfiles/teammate"
6
+
7
+ module Danger
8
+ # Common helper functions for our danger scripts.
9
+ class Helper < Danger::Plugin
10
+ RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
11
+ CATEGORY_LABELS = {
12
+ docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
13
+ none: "",
14
+ qa: "~QA",
15
+ test: "~test ~Quality for `spec/features/*`",
16
+ engineering_productivity: '~"Engineering Productivity" for CI, Danger',
17
+ }.freeze
18
+ # First-match win, so be sure to put more specific regex at the top...
19
+ CATEGORIES = {
20
+ %r{\Adoc/} => :docs,
21
+ %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
22
+
23
+ %r{\A(ee/)?app/(assets|views)/} => :frontend,
24
+ %r{\A(ee/)?public/} => :frontend,
25
+ %r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend,
26
+ %r{\A(ee/)?vendor/assets/} => :frontend,
27
+ %r{\A(ee/)?scripts/frontend/} => :frontend,
28
+ %r{(\A|/)(
29
+ \.babelrc |
30
+ \.eslintignore |
31
+ \.eslintrc(\.yml)? |
32
+ \.nvmrc |
33
+ \.prettierignore |
34
+ \.prettierrc |
35
+ \.scss-lint.yml |
36
+ \.stylelintrc |
37
+ \.haml-lint.yml |
38
+ \.haml-lint_todo.yml |
39
+ babel\.config\.js |
40
+ jest\.config\.js |
41
+ package\.json |
42
+ yarn\.lock |
43
+ config/.+\.js
44
+ )\z}x => :frontend,
45
+
46
+ %r{(\A|/)(
47
+ \.gitlab/ci/frontend\.gitlab-ci\.yml
48
+ )\z}x => %i[frontend engineering_productivity],
49
+
50
+ %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database,
51
+ %r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database,
52
+ %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
53
+ %r{\A(ee/)?app/finders/} => :database,
54
+ %r{\Arubocop/cop/migration(/|\.rb)} => :database,
55
+
56
+ %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
57
+ %r{\A\.codeclimate\.yml\z} => :engineering_productivity,
58
+ %r{\A\.overcommit\.yml\.example\z} => :engineering_productivity,
59
+ %r{\A\.editorconfig\z} => :engineering_productivity,
60
+ %r{Dangerfile\z} => :engineering_productivity,
61
+ %r{\A(ee/)?(danger/|lib/gitlab/danger/)} => :engineering_productivity,
62
+ %r{\A(ee/)?scripts/} => :engineering_productivity,
63
+ %r{\Atooling/} => :engineering_productivity,
64
+
65
+ %r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend,
66
+ %r{\A(ee/)?(bin|config|generator_templates|lib|rubocop)/} => :backend,
67
+ %r{\A(ee/)?spec/features/} => :test,
68
+ %r{\A(ee/)?spec/} => :backend,
69
+ %r{\A(ee/)?vendor/} => :backend,
70
+ %r{\A(Gemfile|Gemfile.lock|Rakefile)\z} => :backend,
71
+ %r{\A[A-Z_]+_VERSION\z} => :backend,
72
+ %r{\A\.rubocop(_todo)?\.yml\z} => :backend,
73
+ %r{\Afile_hooks/} => :backend,
74
+
75
+ %r{\A(ee/)?qa/} => :qa,
76
+
77
+ # Files that don't fit into any category are marked with :none
78
+ %r{\A(ee/)?changelogs/} => :none,
79
+ %r{\Alocale/gitlab\.pot\z} => :none,
80
+
81
+ # Fallbacks in case the above patterns miss anything
82
+ %r{\.rb\z} => :backend,
83
+ %r{(
84
+ \.(md|txt)\z |
85
+ \.markdownlint\.json
86
+ )}x => :none, # To reinstate roulette for documentation, set to `:docs`.
87
+ %r{\.js\z} => :frontend,
88
+ }.freeze
89
+
90
+ HTTPError = Class.new(StandardError)
91
+
92
+ def gitlab_helper
93
+ # Unfortunately the following does not work:
94
+ # - respond_to?(:gitlab)
95
+ # - respond_to?(:gitlab, true)
96
+ gitlab
97
+ rescue NoMethodError
98
+ nil
99
+ end
100
+
101
+ def rule_names
102
+ ci? ? Gitlab::Dangerfiles::LOCAL_RULES | Gitlab::Dangerfiles::CI_ONLY_RULES : Gitlab::Dangerfiles::LOCAL_RULES
103
+ end
104
+
105
+ def html_link(str)
106
+ ci? ? gitlab_helper.html_link(str) : str
107
+ end
108
+
109
+ def ci?
110
+ !gitlab_helper.nil?
111
+ end
112
+
113
+ # @param [String] url
114
+ def http_get_json(url)
115
+ rsp = Net::HTTP.get_response(URI.parse(url))
116
+
117
+ unless rsp.is_a?(Net::HTTPOK)
118
+ raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
119
+ end
120
+
121
+ JSON.parse(rsp.body)
122
+ end
123
+
124
+ # Returns a list of all files that have been added, modified or renamed.
125
+ # `git.modified_files` might contain paths that already have been renamed,
126
+ # so we need to remove them from the list.
127
+ #
128
+ # Considering these changes:
129
+ #
130
+ # - A new_file.rb
131
+ # - D deleted_file.rb
132
+ # - M modified_file.rb
133
+ # - R renamed_file_before.rb -> renamed_file_after.rb
134
+ #
135
+ # it will return
136
+ # ```
137
+ # [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ]
138
+ # ```
139
+ #
140
+ # @return [Array<String>]
141
+ def all_changed_files
142
+ Set.new
143
+ .merge(git.added_files.to_a)
144
+ .merge(git.modified_files.to_a)
145
+ .merge(git.renamed_files.map { |x| x[:after] })
146
+ .subtract(git.renamed_files.map { |x| x[:before] })
147
+ .to_a
148
+ .sort
149
+ end
150
+
151
+ def all_ee_changes
152
+ all_changed_files.grep(%r{\Aee/})
153
+ end
154
+
155
+ def ee?
156
+ # Support former project name for `dev` and support local Danger run
157
+ %w[gitlab gitlab-ee].include?(ENV["CI_PROJECT_NAME"]) || Dir.exist?("../../ee")
158
+ end
159
+
160
+ def release_automation?
161
+ gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
162
+ end
163
+
164
+ def project_name
165
+ ee? ? "gitlab" : "gitlab-foss"
166
+ end
167
+
168
+ def markdown_list(items)
169
+ list = items.map { |item| "* `#{item}`" }.join("\n")
170
+
171
+ if items.size > 10
172
+ "\n<details>\n\n#{list}\n\n</details>\n"
173
+ else
174
+ list
175
+ end
176
+ end
177
+
178
+ # @return [Hash<String,Array<String>>]
179
+ def changes_by_category
180
+ all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
181
+ categories_for_file(file).each { |category| hash[category] << file }
182
+ end
183
+ end
184
+
185
+ # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`.
186
+ # @return Array<Symbol>
187
+ def categories_for_file(file)
188
+ _, categories = CATEGORIES.find { |regexp, _| regexp.match?(file) }
189
+
190
+ Array(categories || :unknown)
191
+ end
192
+
193
+ # Returns the GFM for a category label, making its best guess if it's not
194
+ # a category we know about.
195
+ #
196
+ # @return[String]
197
+ def label_for_category(category)
198
+ CATEGORY_LABELS.fetch(category, "~#{category}")
199
+ end
200
+
201
+ def new_teammates(usernames)
202
+ usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
203
+ end
204
+
205
+ def missing_database_labels(current_mr_labels)
206
+ labels = if has_database_scoped_labels?(current_mr_labels)
207
+ ["database"]
208
+ else
209
+ ["database", "database::review pending"]
210
+ end
211
+
212
+ labels - current_mr_labels
213
+ end
214
+
215
+ def sanitize_mr_title(title)
216
+ title.gsub(/^WIP: */, "").gsub(/`/, '\\\`')
217
+ end
218
+
219
+ def security_mr?
220
+ return false unless gitlab_helper
221
+
222
+ gitlab_helper.mr_json["web_url"].include?("/gitlab-org/security/")
223
+ end
224
+
225
+ def cherry_pick_mr?
226
+ return false unless gitlab_helper
227
+
228
+ /cherry[\s-]*pick/i.match?(gitlab_helper.mr_json["title"])
229
+ end
230
+
231
+ def stable_branch?
232
+ return false unless gitlab_helper
233
+
234
+ /\A\d+-\d+-stable-ee/i.match?(gitlab_helper.mr_json["target_branch"])
235
+ end
236
+
237
+ def mr_has_labels?(*labels)
238
+ return false unless gitlab_helper
239
+
240
+ labels = labels.flatten.uniq
241
+ (labels & gitlab_helper.mr_labels) == labels
242
+ end
243
+
244
+ def labels_list(labels, sep: ", ")
245
+ labels.map { |label| %Q{~"#{label}"} }.join(sep)
246
+ end
247
+
248
+ def prepare_labels_for_mr(labels)
249
+ return "" unless labels.any?
250
+
251
+ "/label #{labels_list(labels, sep: " ")}"
252
+ end
253
+
254
+ private
255
+
256
+ def has_database_scoped_labels?(current_mr_labels)
257
+ current_mr_labels.any? { |label| label.start_with?("database::") }
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../gitlab/dangerfiles/teammate"
4
+
5
+ module Danger
6
+ # Common helper functions for our danger scripts. See Danger::Helper
7
+ # for more details
8
+ class Roulette < Danger::Plugin
9
+ ROULETTE_DATA_URL = "https://gitlab-org.gitlab.io/gitlab-roulette/roulette.json"
10
+ HOURS_WHEN_PERSON_CAN_BE_PICKED = (6..14).freeze
11
+
12
+ Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role)
13
+
14
+ # Assigns GitLab team members to be reviewer and maintainer
15
+ # for each change category that a Merge Request contains.
16
+ #
17
+ # @return [Array<Spin>]
18
+ def spin(project, categories, branch_name, timezone_experiment: false)
19
+ team = begin
20
+ project_team(project)
21
+ rescue => err
22
+ warn("Reviewer roulette failed to load team data: #{err.message}")
23
+ []
24
+ end
25
+
26
+ canonical_branch_name = canonical_branch_name(branch_name)
27
+
28
+ spin_per_category = categories.each_with_object({}) do |category, memo|
29
+ memo[category] = spin_for_category(team, project, category, canonical_branch_name, timezone_experiment: timezone_experiment)
30
+ end
31
+
32
+ spin_per_category.map do |category, spin|
33
+ case category
34
+ when :test
35
+ if spin.reviewer.nil?
36
+ # Fetch an already picked backend reviewer, or pick one otherwise
37
+ spin.reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer
38
+ end
39
+ when :engineering_productivity
40
+ if spin.maintainer.nil?
41
+ # Fetch an already picked backend maintainer, or pick one otherwise
42
+ spin.maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer
43
+ end
44
+ end
45
+
46
+ spin
47
+ end
48
+ end
49
+
50
+ # Looks up the current list of GitLab team members and parses it into a
51
+ # useful form
52
+ #
53
+ # @return [Array<Teammate>]
54
+ def team
55
+ @team ||= begin
56
+ data = helper.http_get_json(ROULETTE_DATA_URL)
57
+ data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
58
+ rescue JSON::ParserError
59
+ raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
60
+ end
61
+ end
62
+
63
+ # Like +team+, but only returns teammates in the current project, based on
64
+ # project_name.
65
+ #
66
+ # @return [Array<Teammate>]
67
+ def project_team(project_name)
68
+ team.select { |member| member.in_project?(project_name) }
69
+ end
70
+
71
+ def canonical_branch_name(branch_name)
72
+ branch_name.gsub(/^[ce]e-|-[ce]e$/, "")
73
+ end
74
+
75
+ def new_random(seed)
76
+ Random.new(Digest::MD5.hexdigest(seed).to_i(16))
77
+ end
78
+
79
+ # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
80
+ # selection will change on next spin
81
+ # @param [Array<Teammate>] people
82
+ def spin_for_person(people, random:, timezone_experiment: false)
83
+ shuffled_people = people.shuffle(random: random)
84
+
85
+ if timezone_experiment
86
+ shuffled_people.find(&method(:valid_person_with_timezone?))
87
+ else
88
+ shuffled_people.find(&method(:valid_person?))
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ # @param [Teammate] person
95
+ # @return [Boolean]
96
+ def valid_person?(person)
97
+ !mr_author?(person) && person.available
98
+ end
99
+
100
+ # @param [Teammate] person
101
+ # @return [Boolean]
102
+ def valid_person_with_timezone?(person)
103
+ valid_person?(person) && HOURS_WHEN_PERSON_CAN_BE_PICKED.cover?(person.local_hour)
104
+ end
105
+
106
+ # @param [Teammate] person
107
+ # @return [Boolean]
108
+ def mr_author?(person)
109
+ person.username == gitlab.mr_author
110
+ end
111
+
112
+ def spin_role_for_category(team, role, project, category)
113
+ team.select do |member|
114
+ member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend
115
+ end
116
+ end
117
+
118
+ def spin_for_category(team, project, category, branch_name, timezone_experiment: false)
119
+ reviewers, traintainers, maintainers =
120
+ %i[reviewer traintainer maintainer].map do |role|
121
+ spin_role_for_category(team, role, project, category)
122
+ end
123
+
124
+ # TODO: take CODEOWNERS into account?
125
+ # https://gitlab.com/gitlab-org/gitlab/issues/26723
126
+
127
+ # Make traintainers have triple the chance to be picked as a reviewer
128
+ random = new_random(branch_name)
129
+ reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
130
+ maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
131
+
132
+ Spin.new(category, reviewer, maintainer)
133
+ end
134
+ end
135
+ end