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