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,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