gitlab-dangerfiles 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,37 @@
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 SidekiqQueues < Danger::Plugin
7
+ def changed_queue_files
8
+ @changed_queue_files ||= git.modified_files.grep(%r{\A(ee/)?app/workers/all_queues\.yml})
9
+ end
10
+
11
+ def added_queue_names
12
+ @added_queue_names ||= new_queues.keys - old_queues.keys
13
+ end
14
+
15
+ def changed_queue_names
16
+ @changed_queue_names ||=
17
+ (new_queues.values_at(*old_queues.keys) - old_queues.values)
18
+ .compact.map { |queue| queue[:name] }
19
+ end
20
+
21
+ private
22
+
23
+ def old_queues
24
+ @old_queues ||= queues_for(gitlab.base_commit)
25
+ end
26
+
27
+ def new_queues
28
+ @new_queues ||= queues_for(gitlab.head_commit)
29
+ end
30
+
31
+ def queues_for(branch)
32
+ changed_queue_files
33
+ .flat_map { |file| YAML.safe_load(`git show #{branch}:#{file}`, permitted_classes: [Symbol]) }
34
+ .to_h { |queue| [queue[:name], queue] }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ require_relative "gitlab/dangerfiles"
@@ -0,0 +1 @@
1
+ danger.import_plugin(File.expand_path("../danger/*.rb", __dir__))
@@ -0,0 +1,40 @@
1
+ require "gitlab/dangerfiles/version"
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ LOCAL_RULES ||= %w[
6
+ changes_size
7
+ commit_messages
8
+ database
9
+ documentation
10
+ duplicate_yarn_dependencies
11
+ eslint
12
+ frozen_string
13
+ karma
14
+ prettier
15
+ telemetry
16
+ utility_css
17
+ ].freeze
18
+ CI_ONLY_RULES ||= %w[
19
+ ce_ee_vue_templates
20
+ changelog
21
+ metadata
22
+ roulette
23
+ sidekiq_queues
24
+ specs
25
+ ].freeze
26
+ MESSAGE_PREFIX = "==>".freeze
27
+
28
+ def self.load_tasks
29
+ require "gitlab/dangerfiles/tasks"
30
+ end
31
+
32
+ def self.local_warning_message
33
+ "#{MESSAGE_PREFIX} Only the following Danger rules can be run locally: #{LOCAL_RULES.join(", ")}"
34
+ end
35
+
36
+ def self.success_message
37
+ "#{MESSAGE_PREFIX} No Danger rule violations!"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ analysis_result = "./bundle-size-review/analysis.json"
4
+ markdown_result = "./bundle-size-review/comparison.md"
5
+
6
+ # Executing the webpack-entry-point-analyser
7
+ # We would like to do that in the CI file directly,
8
+ # but unfortunately the head_commit SHA is not available
9
+ # as a CI variable due to our merge into master simulation
10
+ analyze_cmd = [
11
+ "webpack-entry-point-analyser",
12
+ "--from-file ./webpack-report/stats.json",
13
+ "--json #{analysis_result}",
14
+ " --sha #{gitlab&.head_commit}"
15
+ ].join(" ")
16
+
17
+ # execute analysis
18
+ `#{analyze_cmd}`
19
+
20
+ # We are executing the comparison by comparing the start_sha
21
+ # to the current pipeline result. The start_sha is the commit
22
+ # from master that was merged into for the merged pipeline.
23
+ comparison_cmd = [
24
+ "webpack-compare-reports",
25
+ "--job #{ENV["CI_JOB_ID"]}",
26
+ "--to-file #{analysis_result}",
27
+ "--html ./bundle-size-review/comparison.html",
28
+ "--markdown #{markdown_result}"
29
+ ].join(" ")
30
+
31
+ # execute comparison
32
+ `#{comparison_cmd}`
33
+
34
+ comment = `cat #{markdown_result}`
35
+
36
+ markdown(<<~MARKDOWN)
37
+ #{comment}
38
+ MARKDOWN
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ require 'cgi'
3
+
4
+ def get_vue_files_with_ce_and_ee_versions(files)
5
+ files.select do |file|
6
+ if file.end_with?('.vue')
7
+ counterpart_path = if file.start_with?('ee/')
8
+ file.delete_prefix('ee/')
9
+ else
10
+ "ee/#{file}"
11
+ end
12
+
13
+ escaped_path = CGI.escape(counterpart_path)
14
+ api_endpoint = "https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab-ee/repository/files/#{escaped_path}?ref=master"
15
+ response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty
16
+ response.code != 404
17
+ else
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ vue_candidates = get_vue_files_with_ce_and_ee_versions(helper.all_changed_files)
24
+
25
+ return if vue_candidates.empty?
26
+
27
+ message 'This merge request includes changes to Vue files that have both CE and EE versions.'
28
+
29
+ markdown(<<~MARKDOWN)
30
+ ## Vue `<template>` in CE and EE
31
+
32
+ Some Vue files in CE have a counterpart in EE.
33
+ (For example, `path/to/file.vue` and `ee/path/to/file.vue`.)
34
+
35
+ When run in the context of CE, the `<template>` of the CE Vue file is used.
36
+ When run in the context of EE, the `<template>` of the EE Vue file is used.
37
+
38
+ It's easy to accidentally make a change to a CE `<template>` that _should_
39
+ appear in both CE and EE without making the change in both places.
40
+ When this happens, the change only takes effect in CE.
41
+
42
+ The following Vue files were changed as part of this merge request that
43
+ include both a CE and EE version of the file:
44
+
45
+ * #{vue_candidates.map { |path| "`#{path}`" }.join("\n* ")}
46
+
47
+ If you made a change to the `<template>` of any of these Vue files that
48
+ should be visible in both CE and EE, please ensure you have made your
49
+ change to both versions of the file.
50
+
51
+ ### A better alternative
52
+
53
+ An even _better_ alternative is to refactor this component to only use
54
+ a single template for both CE and EE. More info on this approach here:
55
+ https://docs.gitlab.com/ee/development/ee_features.html#template-tag
56
+ MARKDOWN
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable Style/SignalException
3
+
4
+ require 'yaml'
5
+
6
+ SEE_DOC = "See the [changelog documentation](https://docs.gitlab.com/ee/development/changelog.html)."
7
+ CREATE_CHANGELOG_MESSAGE = <<~MSG
8
+ If you want to create a changelog entry for GitLab FOSS, run the following:
9
+
10
+ ```
11
+ bin/changelog -m %<mr_iid>s "%<mr_title>s"
12
+ ```
13
+
14
+ If you want to create a changelog entry for GitLab EE, run the following instead:
15
+
16
+ ```
17
+ bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"
18
+ ```
19
+
20
+ If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
21
+ MSG
22
+
23
+ SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT
24
+ ```suggestion
25
+ merge_request: %<mr_iid>s
26
+ ```
27
+
28
+ #{SEE_DOC}
29
+ SUGGEST_COMMENT
30
+
31
+ def check_changelog_yaml(path)
32
+ raw_file = File.read(path)
33
+ yaml = YAML.safe_load(raw_file)
34
+
35
+ fail "`title` should be set, in #{helper.html_link(path)}! #{SEE_DOC}" if yaml["title"].nil?
36
+ fail "`type` should be set, in #{helper.html_link(path)}! #{SEE_DOC}" if yaml["type"].nil?
37
+
38
+ return if helper.security_mr?
39
+
40
+ cherry_pick_against_stable_branch = helper.cherry_pick_mr? && helper.stable_branch?
41
+
42
+ if yaml["merge_request"].nil?
43
+ mr_line = raw_file.lines.find_index("merge_request:\n")
44
+
45
+ if mr_line
46
+ markdown(format(SUGGEST_MR_COMMENT, mr_iid: gitlab.mr_json["iid"]), file: path, line: mr_line.succ)
47
+ else
48
+ message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{helper.html_link(path)}. #{SEE_DOC}"
49
+ end
50
+ elsif yaml["merge_request"] != gitlab.mr_json["iid"] && !cherry_pick_against_stable_branch
51
+ fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
52
+ end
53
+ rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
54
+ # YAML could not be parsed, fail the build.
55
+ fail "#{helper.html_link(path)} isn't valid YAML! #{SEE_DOC}"
56
+ rescue StandardError => e
57
+ warn "There was a problem trying to check the Changelog. Exception: #{e.class.name} - #{e.message}"
58
+ end
59
+
60
+ def check_changelog_path(path)
61
+ ee_changes = helper.all_ee_changes.dup
62
+ ee_changes.delete(path)
63
+
64
+ if ee_changes.any? && !changelog.ee_changelog?
65
+ warn "This MR has a Changelog file outside `ee/`, but code changes in `ee/`. Consider moving the Changelog file into `ee/`."
66
+ end
67
+
68
+ if ee_changes.empty? && changelog.ee_changelog?
69
+ warn "This MR has a Changelog file in `ee/`, but no code changes in `ee/`. Consider moving the Changelog file outside `ee/`."
70
+ end
71
+ end
72
+
73
+ def sanitized_mr_title
74
+ helper.sanitize_mr_title(gitlab.mr_json["title"])
75
+ end
76
+
77
+ if git.modified_files.include?("CHANGELOG.md")
78
+ fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" +
79
+ format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title)
80
+ end
81
+
82
+ changelog_found = changelog.found
83
+
84
+ if changelog_found
85
+ check_changelog_yaml(changelog_found)
86
+ check_changelog_path(changelog_found)
87
+ elsif changelog.needed?
88
+ message "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**:\n\n" +
89
+ format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title)
90
+ end
@@ -0,0 +1,17 @@
1
+ # FIXME: git.info_for_file raises the following error
2
+ # /usr/local/bundle/gems/git-1.4.0/lib/git/lib.rb:956:in `command': (Danger::DSLError)
3
+ # [!] Invalid `Dangerfile` file:
4
+ # [!] Invalid `Dangerfile` file: git '--git-dir=/builds/gitlab-org/gitlab/.git' '--work-tree=/builds/gitlab-org/gitlab' cat-file '-t' '' 2>&1:fatal: Not a valid object name
5
+ # This seems to be the same as https://github.com/danger/danger/issues/535.
6
+
7
+ # locale_files_updated = git.modified_files.select { |path| path.start_with?('locale') }
8
+ # locale_files_updated.each do |locale_file_updated|
9
+ # git_stats = git.info_for_file(locale_file_updated)
10
+ # message "Git stats for #{locale_file_updated}: #{git_stats[:insertions]} insertions, #{git_stats[:deletions]} insertions"
11
+ # end
12
+
13
+ if git.lines_of_code > 2_000
14
+ warn "This merge request is definitely too big (more than #{git.lines_of_code} lines changed), please split it into multiple merge requests."
15
+ elsif git.lines_of_code > 500
16
+ warn "This merge request is quite big (more than #{git.lines_of_code} lines changed), please consider splitting it into multiple merge requests."
17
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ emoji_checker_path = File.expand_path("emoji_checker", __dir__)
4
+ defined?(Rails) ? require_dependency(emoji_checker_path) : require_relative(emoji_checker_path)
5
+
6
+ module Gitlab
7
+ module Dangerfiles
8
+ class CommitLinter
9
+ MIN_SUBJECT_WORDS_COUNT = 3
10
+ MAX_LINE_LENGTH = 72
11
+ MAX_CHANGED_FILES_IN_COMMIT = 3
12
+ MAX_CHANGED_LINES_IN_COMMIT = 30
13
+ SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(#|!|&|%)\d+\b}.freeze
14
+ DEFAULT_SUBJECT_DESCRIPTION = "commit subject"
15
+ WIP_PREFIX = "WIP: "
16
+ PROBLEMS = {
17
+ subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
18
+ subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
19
+ subject_starts_with_lowercase: "The %s must start with a capital letter",
20
+ subject_ends_with_a_period: "The %s must not end with a period",
21
+ separator_missing: "The commit subject and body must be separated by a blank line",
22
+ details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
23
+ "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
24
+ details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
25
+ message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
26
+ "to the commit message, and are displayed as plain text outside of GitLab",
27
+ message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
28
+ "message, and may not be displayed properly everywhere",
29
+ message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
30
+ "`!123`), as short references are displayed as plain text outside of GitLab",
31
+ }.freeze
32
+
33
+ attr_reader :commit, :problems
34
+
35
+ def initialize(commit)
36
+ @commit = commit
37
+ @problems = {}
38
+ @linted = false
39
+ end
40
+
41
+ def fixup?
42
+ commit.message.start_with?("fixup!", "squash!")
43
+ end
44
+
45
+ def suggestion?
46
+ commit.message.start_with?("Apply suggestion to")
47
+ end
48
+
49
+ def merge?
50
+ commit.message.start_with?("Merge branch")
51
+ end
52
+
53
+ def revert?
54
+ commit.message.start_with?('Revert "')
55
+ end
56
+
57
+ def multi_line?
58
+ !details.nil? && !details.empty?
59
+ end
60
+
61
+ def failed?
62
+ problems.any?
63
+ end
64
+
65
+ def add_problem(problem_key, *args)
66
+ @problems[problem_key] = sprintf(PROBLEMS[problem_key], *args)
67
+ end
68
+
69
+ def lint(subject_description = "commit subject")
70
+ return self if @linted
71
+
72
+ @linted = true
73
+ lint_subject(subject_description)
74
+ lint_separator
75
+ lint_details
76
+ lint_message
77
+
78
+ self
79
+ end
80
+
81
+ def lint_subject(subject_description)
82
+ if subject_too_short?
83
+ add_problem(:subject_too_short, subject_description)
84
+ end
85
+
86
+ if subject_too_long?
87
+ add_problem(:subject_too_long, subject_description)
88
+ end
89
+
90
+ if subject_starts_with_lowercase?
91
+ add_problem(:subject_starts_with_lowercase, subject_description)
92
+ end
93
+
94
+ if subject_ends_with_a_period?
95
+ add_problem(:subject_ends_with_a_period, subject_description)
96
+ end
97
+
98
+ self
99
+ end
100
+
101
+ private
102
+
103
+ def lint_separator
104
+ return self unless separator && !separator.empty?
105
+
106
+ add_problem(:separator_missing)
107
+
108
+ self
109
+ end
110
+
111
+ def lint_details
112
+ if !multi_line? && many_changes?
113
+ add_problem(:details_too_many_changes)
114
+ end
115
+
116
+ details&.each_line do |line|
117
+ line = line.strip
118
+
119
+ next unless line_too_long?(line)
120
+
121
+ url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length } # rubocop:disable CodeReuse/ActiveRecord
122
+
123
+ # If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
124
+ # only if the line _without_ the URL does not exceed this limit.
125
+ next unless line_too_long?(line.length - url_size)
126
+
127
+ add_problem(:details_line_too_long)
128
+ break
129
+ end
130
+
131
+ self
132
+ end
133
+
134
+ def lint_message
135
+ if message_contains_text_emoji?
136
+ add_problem(:message_contains_text_emoji)
137
+ end
138
+
139
+ if message_contains_unicode_emoji?
140
+ add_problem(:message_contains_unicode_emoji)
141
+ end
142
+
143
+ if message_contains_short_reference?
144
+ add_problem(:message_contains_short_reference)
145
+ end
146
+
147
+ self
148
+ end
149
+
150
+ def files_changed
151
+ commit.diff_parent.stats[:total][:files]
152
+ end
153
+
154
+ def lines_changed
155
+ commit.diff_parent.stats[:total][:lines]
156
+ end
157
+
158
+ def many_changes?
159
+ files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT
160
+ end
161
+
162
+ def subject
163
+ message_parts[0].delete_prefix(WIP_PREFIX)
164
+ end
165
+
166
+ def separator
167
+ message_parts[1]
168
+ end
169
+
170
+ def details
171
+ message_parts[2]&.gsub(/^Signed-off-by.*$/, "")
172
+ end
173
+
174
+ def line_too_long?(line)
175
+ case line
176
+ when String
177
+ line.length > MAX_LINE_LENGTH
178
+ when Integer
179
+ line > MAX_LINE_LENGTH
180
+ else
181
+ raise ArgumentError, "The line argument (#{line}) should be a String or an Integer! #{line.class} given."
182
+ end
183
+ end
184
+
185
+ def subject_too_short?
186
+ subject.split(" ").length < MIN_SUBJECT_WORDS_COUNT
187
+ end
188
+
189
+ def subject_too_long?
190
+ line_too_long?(subject)
191
+ end
192
+
193
+ def subject_starts_with_lowercase?
194
+ first_char = subject.sub(/\A\[.+\]\s/, "")[0]
195
+ first_char_downcased = first_char.downcase
196
+ return true unless ("a".."z").cover?(first_char_downcased)
197
+
198
+ first_char.downcase == first_char
199
+ end
200
+
201
+ def subject_ends_with_a_period?
202
+ subject.end_with?(".")
203
+ end
204
+
205
+ def message_contains_text_emoji?
206
+ emoji_checker.includes_text_emoji?(commit.message)
207
+ end
208
+
209
+ def message_contains_unicode_emoji?
210
+ emoji_checker.includes_unicode_emoji?(commit.message)
211
+ end
212
+
213
+ def message_contains_short_reference?
214
+ commit.message.match?(SHORT_REFERENCE_REGEX)
215
+ end
216
+
217
+ def emoji_checker
218
+ @emoji_checker ||= Gitlab::Dangerfiles::EmojiChecker.new
219
+ end
220
+
221
+ def message_parts
222
+ @message_parts ||= commit.message.split("\n", 3)
223
+ end
224
+ end
225
+ end
226
+ end