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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "open3"
5
+
6
+ module Gitlab
7
+ module Dangerfiles
8
+ class Popen
9
+ Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
10
+
11
+ # Returns [stdout + stderr, status]
12
+ def popen(cmd, path = nil, vars = {}, &block)
13
+ result = popen_with_detail(cmd, path, vars, &block)
14
+
15
+ ["#{result.stdout}#{result.stderr}", result.status&.exitstatus]
16
+ end
17
+
18
+ # Returns Result
19
+ def popen_with_detail(cmd, path = nil, vars = {})
20
+ unless cmd.is_a?(Array)
21
+ raise "System commands must be given as an array of strings"
22
+ end
23
+
24
+ path ||= Dir.pwd
25
+ vars["PWD"] = path
26
+ options = { chdir: path }
27
+
28
+ unless File.directory?(path)
29
+ FileUtils.mkdir_p(path)
30
+ end
31
+
32
+ cmd_stdout = ""
33
+ cmd_stderr = ""
34
+ cmd_status = nil
35
+ start = Time.now
36
+
37
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
38
+ # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
39
+ # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
40
+ out_reader = Thread.new { stdout.read }
41
+ err_reader = Thread.new { stderr.read }
42
+
43
+ yield(stdin) if block_given?
44
+ stdin.close
45
+
46
+ cmd_stdout = out_reader.value
47
+ cmd_stderr = err_reader.value
48
+ cmd_status = wait_thr.value
49
+ end
50
+
51
+ Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ def get_prettier_files(files)
4
+ files.select do |file|
5
+ file.end_with?('.js', '.scss', '.vue')
6
+ end
7
+ end
8
+
9
+ prettier_candidates = get_prettier_files(helper.all_changed_files)
10
+
11
+ return if prettier_candidates.empty?
12
+
13
+ unpretty = `node_modules/prettier/bin-prettier.js --list-different #{prettier_candidates.join(" ")}`
14
+ .split(/$/)
15
+ .map(&:strip)
16
+ .reject(&:empty?)
17
+
18
+ return if unpretty.empty?
19
+
20
+ warn 'This merge request changed frontend files without pretty printing them.'
21
+
22
+ if helper.ci?
23
+ markdown(<<~MARKDOWN)
24
+ ## Pretty print Frontend files
25
+
26
+ The following files should have been pretty printed with `prettier`:
27
+
28
+ * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")}
29
+
30
+ Please run
31
+
32
+ ```
33
+ node_modules/.bin/prettier --write \\
34
+ #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")}
35
+ ```
36
+
37
+ Also consider auto-formatting [on-save].
38
+
39
+ [on-save]: https://docs.gitlab.com/ee/development/fe_guide/tooling.html#formatting-with-prettier
40
+ MARKDOWN
41
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/md5'
4
+
5
+ MESSAGE = <<MARKDOWN
6
+ ## Reviewer roulette
7
+
8
+ Changes that require review have been detected! A merge request is normally
9
+ reviewed by both a reviewer and a maintainer in its primary category (e.g.
10
+ ~frontend or ~backend), and by a maintainer in all other categories.
11
+ MARKDOWN
12
+
13
+ CATEGORY_TABLE_HEADER = <<MARKDOWN
14
+
15
+ To spread load more evenly across eligible reviewers, Danger has picked a candidate for each
16
+ review slot, based on their timezone. Feel free to
17
+ [override these selections](https://about.gitlab.com/handbook/engineering/projects/#gitlab)
18
+ if you think someone else would be better-suited, or the chosen person is unavailable.
19
+
20
+ To read more on how to use the reviewer roulette, please take a look at the
21
+ [Engineering workflow](https://about.gitlab.com/handbook/engineering/workflow/#basics)
22
+ and [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html).
23
+ Please consider assigning a reviewer or maintainer who is a
24
+ [domain expert](https://about.gitlab.com/handbook/engineering/projects/#gitlab) in the area of the merge request.
25
+
26
+ Once you've decided who will review this merge request, mention them as you
27
+ normally would! Danger does not automatically notify them for you.
28
+
29
+ | Category | Reviewer | Maintainer |
30
+ | -------- | -------- | ---------- |
31
+ MARKDOWN
32
+
33
+ UNKNOWN_FILES_MESSAGE = <<MARKDOWN
34
+
35
+ These files couldn't be categorised, so Danger was unable to suggest a reviewer.
36
+ Please consider creating a merge request to
37
+ [add support](https://gitlab.com/gitlab-org/gitlab-dangerfiles/blob/master/lib/danger/helper.rb)
38
+ for them.
39
+ MARKDOWN
40
+
41
+ OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze
42
+ NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.freeze
43
+ TIMEZONE_EXPERIMENT = true
44
+
45
+ def mr_author
46
+ roulette.team.find { |person| person.username == gitlab.mr_author }
47
+ end
48
+
49
+ def note_for_category_role(spin, role)
50
+ if spin.optional_role == role
51
+ return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) }
52
+ end
53
+
54
+ spin.public_send(role)&.markdown_name(timezone_experiment: TIMEZONE_EXPERIMENT, author: mr_author) || NOT_AVAILABLE_TEMPLATE % { role: role } # rubocop:disable GitlabSecurity/PublicSend
55
+ end
56
+
57
+ def markdown_row_for_spin(spin)
58
+ reviewer_note = note_for_category_role(spin, :reviewer)
59
+ maintainer_note = note_for_category_role(spin, :maintainer)
60
+
61
+ "| #{helper.label_for_category(spin.category)} | #{reviewer_note} | #{maintainer_note} |"
62
+ end
63
+
64
+ changes = helper.changes_by_category
65
+
66
+ # Ignore any files that are known but uncategorized. Prompt for any unknown files
67
+ changes.delete(:none)
68
+ # To reinstate roulette for documentation, remove this line.
69
+ changes.delete(:docs)
70
+ categories = changes.keys - [:unknown]
71
+
72
+ # Ensure to spin for database reviewer/maintainer when ~database is applied (e.g. to review SQL queries)
73
+ categories << :database if gitlab.mr_labels.include?('database') && !categories.include?(:database)
74
+
75
+ if changes.any?
76
+ project = helper.project_name
77
+ branch_name = gitlab.mr_json['source_branch']
78
+
79
+ markdown(MESSAGE)
80
+
81
+ roulette_spins = roulette.spin(project, categories, branch_name, timezone_experiment: TIMEZONE_EXPERIMENT)
82
+ rows = roulette_spins.map do |spin|
83
+ # MR includes QA changes, but also other changes, and author isn't an SET
84
+ if spin.category == :qa && categories.size > 1 && !mr_author.reviewer?(project, spin.category, [])
85
+ spin.optional_role = :maintainer
86
+ end
87
+
88
+ spin.optional_role = :maintainer if spin.category == :test
89
+
90
+ markdown_row_for_spin(spin)
91
+ end
92
+
93
+ markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
94
+
95
+ unknown = changes.fetch(:unknown, [])
96
+ markdown(UNKNOWN_FILES_MESSAGE + helper.markdown_list(unknown)) unless unknown.empty?
97
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ SCALABILITY_REVIEW_MESSAGE = <<~MSG
4
+ ## Sidekiq queue changes
5
+
6
+ This merge request contains changes to Sidekiq queues. Please follow the [documentation on changing a queue's urgency](https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#changing-a-queues-urgency).
7
+ MSG
8
+
9
+ ADDED_QUEUES_MESSAGE = <<~MSG
10
+ These queues were added:
11
+ MSG
12
+
13
+ CHANGED_QUEUES_MESSAGE = <<~MSG
14
+ These queues had their attributes changed:
15
+ MSG
16
+
17
+ if sidekiq_queues.added_queue_names.any? || sidekiq_queues.changed_queue_names.any?
18
+ markdown(SCALABILITY_REVIEW_MESSAGE)
19
+
20
+ if sidekiq_queues.added_queue_names.any?
21
+ markdown(ADDED_QUEUES_MESSAGE + helper.markdown_list(sidekiq_queues.added_queue_names))
22
+ end
23
+
24
+ if sidekiq_queues.changed_queue_names.any?
25
+ markdown(CHANGED_QUEUES_MESSAGE + helper.markdown_list(sidekiq_queues.changed_queue_names))
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ NO_SPECS_LABELS = [
4
+ 'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360.
5
+ 'tooling',
6
+ 'tooling::pipelines',
7
+ 'tooling::workflow',
8
+ 'documentation',
9
+ 'QA'
10
+ ].freeze
11
+ NO_NEW_SPEC_MESSAGE = <<~MSG
12
+ You've made some app changes, but didn't add any tests.
13
+ That's OK as long as you're refactoring existing code,
14
+ but please consider adding any of the %<labels>s labels.
15
+ MSG
16
+ EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE = <<~MSG
17
+ You've made some EE-specific changes, but only made changes to FOSS tests.
18
+ This could be a sign that you're testing an EE-specific behavior in a FOSS test.
19
+
20
+ Please make sure the spec files pass in AS-IF-FOSS mode either:
21
+
22
+ 1. Locally with `FOSS_ONLY=1 bin/rspec -- %<spec_files>s`.
23
+ 1. In the MR pipeline by verifying that the `rspec foss-impact` job has passed.
24
+ 1. In the MR pipelines by including `RUN AS-IF-FOSS` in the MR title (you can do it with the ``/title %<mr_title>s [RUN AS-IF-FOSS]`` quick action) and start a new MR pipeline.
25
+
26
+ MSG
27
+
28
+ has_app_changes = helper.all_changed_files.grep(%r{\A(app|lib|db/(geo/)?(post_)?migrate)/}).any?
29
+ has_ee_app_changes = helper.all_changed_files.grep(%r{\Aee/(app|lib|db/(geo/)?(post_)?migrate)/}).any?
30
+ spec_changes = helper.all_changed_files.grep(%r{\Aspec/})
31
+ has_spec_changes = spec_changes.any?
32
+ has_ee_spec_changes = helper.all_changed_files.grep(%r{\Aee/spec/}).any?
33
+ new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty?
34
+
35
+ if (has_app_changes || has_ee_app_changes) && !(has_spec_changes || has_ee_spec_changes) && new_specs_needed
36
+ warn format(NO_NEW_SPEC_MESSAGE, labels: helper.labels_list(NO_SPECS_LABELS)), sticky: false
37
+ end
38
+
39
+ # The only changes outside `ee/` are in `spec/`
40
+ if has_ee_app_changes && has_spec_changes && !(has_app_changes || has_ee_spec_changes)
41
+ warn format(EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE, spec_files: spec_changes.join(" "), mr_title: gitlab.mr_json['title']), sticky: false
42
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Run local Danger rules"
4
+ task :danger_local do
5
+ require "gitlab/dangerfiles"
6
+ require "popen/danger/popen"
7
+
8
+ puts("#{Gitlab::Dangerfiles.local_warning_message}\n")
9
+
10
+ # _status will _always_ be 0, regardless of failure or success :(
11
+ output, _status = Gitlab::Dangerfiles::Popen.popen(%w{danger dry_run})
12
+
13
+ if output.empty?
14
+ puts(Gitlab::Dangerfiles.success_message)
15
+ else
16
+ puts(output)
17
+ exit(1)
18
+ end
19
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ class Teammate
6
+ attr_reader :username, :name, :role, :projects, :available, :tz_offset_hours
7
+
8
+ # The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
9
+ def initialize(options = {})
10
+ @username = options["username"]
11
+ @name = options["name"]
12
+ @markdown_name = options["markdown_name"]
13
+ @role = options["role"]
14
+ @projects = options["projects"]
15
+ @available = options["available"]
16
+ @tz_offset_hours = options["tz_offset_hours"]
17
+ end
18
+
19
+ def in_project?(name)
20
+ projects&.has_key?(name)
21
+ end
22
+
23
+ # Traintainers also count as reviewers
24
+ def reviewer?(project, category, labels)
25
+ has_capability?(project, category, :reviewer, labels) ||
26
+ traintainer?(project, category, labels)
27
+ end
28
+
29
+ def traintainer?(project, category, labels)
30
+ has_capability?(project, category, :trainee_maintainer, labels)
31
+ end
32
+
33
+ def maintainer?(project, category, labels)
34
+ has_capability?(project, category, :maintainer, labels)
35
+ end
36
+
37
+ def markdown_name(timezone_experiment: false, author: nil)
38
+ return @markdown_name unless timezone_experiment
39
+
40
+ "#{@markdown_name} (#{utc_offset_text(author)})"
41
+ end
42
+
43
+ def local_hour
44
+ (Time.now.utc + tz_offset_hours * 3600).hour
45
+ end
46
+
47
+ protected
48
+
49
+ def floored_offset_hours
50
+ floored_offset = tz_offset_hours.floor(0)
51
+
52
+ floored_offset == tz_offset_hours ? floored_offset : tz_offset_hours
53
+ end
54
+
55
+ private
56
+
57
+ def utc_offset_text(author = nil)
58
+ offset_text = if floored_offset_hours >= 0
59
+ "UTC+#{floored_offset_hours}"
60
+ else
61
+ "UTC#{floored_offset_hours}"
62
+ end
63
+
64
+ return offset_text unless author
65
+
66
+ "#{offset_text}, #{offset_diff_compared_to_author(author)}"
67
+ end
68
+
69
+ def offset_diff_compared_to_author(author)
70
+ diff = floored_offset_hours - author.floored_offset_hours
71
+ return "same timezone as `@#{author.username}`" if diff.zero?
72
+
73
+ ahead_or_behind = diff < 0 ? "behind" : "ahead"
74
+ pluralized_hours = pluralize(diff.abs, "hour", "hours")
75
+
76
+ "#{pluralized_hours} #{ahead_or_behind} `@#{author.username}`"
77
+ end
78
+
79
+ def has_capability?(project, category, kind, labels)
80
+ case category
81
+ when :test
82
+ area = role[/Software Engineer in Test(?:.*?, (\w+))/, 1]
83
+
84
+ area && labels.any?("devops::#{area.downcase}") if kind == :reviewer
85
+ when :engineering_productivity
86
+ return false unless role[/Engineering Productivity/]
87
+ return true if kind == :reviewer
88
+
89
+ capabilities(project).include?("#{kind} backend")
90
+ else
91
+ capabilities(project).include?("#{kind} #{category}")
92
+ end
93
+ end
94
+
95
+ def capabilities(project)
96
+ Array(projects.fetch(project, []))
97
+ end
98
+
99
+ def pluralize(count, singular, plural)
100
+ word = count == 1 || count.to_s =~ /^1(\.0+)?$/ ? singular : plural
101
+
102
+ "#{count || 0} #{word}"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ TELEMETRY_CHANGED_FILES_MESSAGE = <<~MSG
4
+ For the following files, a review from the [Data team and Telemetry team](https://gitlab.com/groups/gitlab-org/growth/telemetry/engineers/-/group_members?with_inherited_permissions=exclude) is recommended
5
+ Please check the ~telemetry [guide](https://docs.gitlab.com/ee/development/telemetry/usage_ping.html) and reach out to @gitlab-org/growth/telemetry/engineers group for a review.
6
+
7
+ %<changed_files>s
8
+
9
+ MSG
10
+
11
+ tracking_files = [
12
+ 'lib/gitlab/tracking.rb',
13
+ 'spec/lib/gitlab/tracking_spec.rb',
14
+ 'app/helpers/tracking_helper.rb',
15
+ 'spec/helpers/tracking_helper_spec.rb',
16
+ 'app/assets/javascripts/tracking.js',
17
+ 'spec/frontend/tracking_spec.js'
18
+ ]
19
+
20
+ usage_data_changed_files = git.modified_files.grep(%r{usage_data})
21
+ snowplow_events_changed_files = git.modified_files & tracking_files
22
+
23
+ changed_files = (usage_data_changed_files + snowplow_events_changed_files)
24
+
25
+ if changed_files.any?
26
+ warn format(TELEMETRY_CHANGED_FILES_MESSAGE, changed_files: helper.markdown_list(changed_files))
27
+
28
+ telemetry_labels = ['telemetry']
29
+ telemetry_labels << 'telemetry::review pending' unless helper.mr_has_labels?('telemetry::reviewed')
30
+
31
+ markdown(helper.prepare_labels_for_mr(telemetry_labels))
32
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ common = 'app/assets/stylesheets/framework/common.scss'
4
+ utilities = 'app/assets/stylesheets/utilities.scss'
5
+
6
+ def get_css_files(files, common_filepath, utilities_filepath)
7
+ files.select do |file|
8
+ file.include?(common_filepath) ||
9
+ file.include?(utilities_filepath)
10
+ end
11
+ end
12
+
13
+ changed_util_files = get_css_files(helper.all_changed_files.to_a, common, utilities)
14
+
15
+ unless changed_util_files.empty?
16
+
17
+ markdown(<<~MARKDOWN)
18
+ ## Changes to utility SCSS files
19
+ MARKDOWN
20
+
21
+ if changed_util_files.include?(common)
22
+
23
+ markdown(<<~MARKDOWN)
24
+ ### Addition to `#{common}`
25
+
26
+ You have added a new rule to `#{common}`. Are you sure you need this rule?
27
+
28
+ If it is a component class shared across items, could it be added to the component as a utility class or to the component's stylesheet? If not, you can ignore this warning.
29
+
30
+ If it is a new utility class, is there another class that shares the same values in either this file or in `#{utilities}`? If not, please add it to `#{utilities}`, following the [Gitlab UI naming style](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss).
31
+
32
+ MARKDOWN
33
+
34
+ end
35
+
36
+ if changed_util_files.include?(utilities)
37
+ markdown(<<~MARKDOWN)
38
+ ### Addition to `#{utilities}`
39
+
40
+ You have added a new rule to `#{utilities}`. Are you sure you need this rule?
41
+
42
+ If it is a component class shared across items, could it be added to the component as a utility class or to the component's stylesheet? If not, consider adding it to `#{common}`
43
+
44
+ If it is a new utility class, is there another class that shares the same values in either this file or in `#{utilities}`? If not, please be sure this addition follows the [Gitlab UI naming style](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss) so it may be removed when these rules are included. See [Include gitlab-ui utility-class library](https://gitlab.com/gitlab-org/gitlab/issues/36857) for more about this project.
45
+
46
+ MARKDOWN
47
+ end
48
+
49
+ warn "This merge request adds a new rule to #{common} or #{utilities}."
50
+
51
+ end