gitlab-dangerfiles 0.8.1 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8132453b5b4614eaa87813509a18230d9c94d00cdec5b9e6857b032800092b0
4
- data.tar.gz: 2e8dfe3056a7da31abbde54e9a8d1cbb96e9c9105d7fde5058903cc2617dbcfe
3
+ metadata.gz: 0d3407bad03ff3220a55329e625c1fd5a962b4ffe29b4ab1da68a7e998761024
4
+ data.tar.gz: a9f31b18452500fa6e437a847b1d726cbbe2c47a1912c5e40fd18196ca3716fa
5
5
  SHA512:
6
- metadata.gz: 8d1020a4fe7e5bcce74d8c03127373ae1a97ecf7ff57a981167cb6b8b733a17d5aef4b4c40fb31b88e996f8fe5830ecd779c49dc89609c3180f8ccf558a99028
7
- data.tar.gz: 0d13715735e7bdded2a526575e276f21e11c11fb57f2fdcecdceb9051143f1995e2527cb4a57d9427aa3d8d2ced85d13775d96819d80610ae555f3108b5e508d
6
+ metadata.gz: 3390ab204b7572654e748198fb1d03b70056ab26b32a8ea29cc2c624e307e59c5cfab5306afb8900c71df62671b984ccdbb21e7c1e9adb16b2ab43a9299774c0
7
+ data.tar.gz: '06879db4e54bff63e540793f8c94f3c9412d6481641144bfa80934bb36a82872c76e9693c86aa9b381d78528da06a27795f620ff9127829e4904938807958ede'
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --protected
2
+ --no-private
3
+ -
4
+ CODE_OF_CONDUCT.md
5
+ LICENSE.txt
data/Gemfile CHANGED
@@ -5,3 +5,4 @@ gemspec
5
5
 
6
6
  gem "rake", "~> 12.0"
7
7
  gem "guard-rspec"
8
+ gem "yard"
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020 Rémy Coutable
3
+ Copyright (c) 2020-2021 GitLab
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Gitlab::Dangerfiles
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/gitlab/dangerfiles`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ The goal of this gem is to centralize Danger plugins and rules that to be used by multiple GitLab projects.
6
4
 
7
5
  ## Installation
8
6
 
@@ -14,15 +12,57 @@ gem 'gitlab-dangerfiles'
14
12
 
15
13
  And then execute:
16
14
 
17
- $ bundle install
15
+ ```sh
16
+ $ bundle install
17
+ ```
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install gitlab-dangerfiles
21
+ ```sh
22
+ $ gem install gitlab-dangerfiles
23
+ ```
22
24
 
23
25
  ## Usage
24
26
 
25
- TODO: Write usage instructions here
27
+ ### Importing plugins and rules
28
+
29
+ In your project's `Dangerfile`, add the following two line to import the plugins and rules from this gem:
30
+
31
+ ```ruby
32
+ # Get an instance of Gitlab::Dangerfiles
33
+ gitlab_dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
34
+
35
+ # Import all plugins from the gem
36
+ gitlab_dangerfiles.import_plugins
37
+
38
+ # Import all rules from the gem
39
+ gitlab_dangerfiles.import_dangerfiles
40
+
41
+ # Or import a subset of rules from the gem
42
+ gitlab_dangerfiles.import_dangerfiles(rules: [:changes_size])
43
+ ```
44
+
45
+ ### Plugins
46
+
47
+ Danger plugins are located under `lib/danger/plugins`.
48
+
49
+ - `Danger::Helper` available in `Dangerfile`s as `helper`
50
+ - `Danger::Roulette` available in `Dangerfile`s as `roulette`
51
+
52
+ For the full documentation about the plugins, please see https://www.rubydoc.info/gems/gitlab-dangerfiles.
53
+
54
+ ### Rules
55
+
56
+
57
+ Danger rules are located under `lib/danger/rules`.
58
+
59
+ #### `changes_size`
60
+
61
+ ##### Available configurations
62
+
63
+ - `code_size_thresholds`: A hash of the form `{ high: 42, medium: 12 }` where
64
+ `:high` is the lines changed threshold which triggers an error, and
65
+ `:medium` is the lines changed threshold which triggers a warning.
26
66
 
27
67
  ## Development
28
68
 
@@ -3,8 +3,8 @@ require_relative "lib/gitlab/dangerfiles/version"
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "gitlab-dangerfiles"
5
5
  spec.version = Gitlab::Dangerfiles::VERSION
6
- spec.authors = ["Rémy Coutable"]
7
- spec.email = ["remy@rymai.me"]
6
+ spec.authors = ["GitLab"]
7
+ spec.email = ["gitlab_rubygems@gitlab.com"]
8
8
 
9
9
  spec.summary = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
10
10
  spec.description = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
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"
19
+ spec.metadata["changelog_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles/-/releases"
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "danger"
30
+ spec.add_dependency "danger-gitlab"
31
31
 
32
32
  spec.add_development_dependency "rspec", "~> 3.0"
33
33
  spec.add_development_dependency "rspec-parameterized"
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ require_relative "../../gitlab/dangerfiles/changes"
7
+ require_relative "../../gitlab/dangerfiles/config"
8
+ require_relative "../../gitlab/dangerfiles/teammate"
9
+ require_relative "../../gitlab/dangerfiles/title_linting"
10
+
11
+ module Danger
12
+ # Common helper functions for our danger scripts.
13
+ class Helper < Danger::Plugin
14
+ RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
15
+ CATEGORY_LABELS = {
16
+ docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
17
+ none: "",
18
+ qa: "~QA",
19
+ test: "~test ~Quality for `spec/features/*`",
20
+ engineering_productivity: '~"Engineering Productivity" for CI, Danger',
21
+ ci_template: '~"ci::templates"',
22
+ product_intelligence: '~"product intelligence"',
23
+ }.freeze
24
+
25
+ # Allows to set specific rule's configuration by passing a block.
26
+ #
27
+ # @yield [c] Yield a Gitlab::Dangerfiles::Config object
28
+ #
29
+ # @yieldparam [Gitlab::Dangerfiles::Config] The Gitlab::Dangerfiles::Config object
30
+ # @yieldreturn [Gitlab::Dangerfiles::Config] The Gitlab::Dangerfiles::Config object
31
+ #
32
+ # @example
33
+ # helper.config do |config|
34
+ # config.code_size_thresholds = { high: 42, medium: 12 }
35
+ # end
36
+ #
37
+ # @return [Gitlab::Dangerfiles::Config]
38
+ def config
39
+ (@config ||= Gitlab::Dangerfiles::Config.new).tap do |c|
40
+ yield c if block_given?
41
+ end
42
+ end
43
+
44
+ # @example
45
+ # <a href='https://gitlab.com/artsy/eigen/blob/561827e46167077b5e53515b4b7349b8ae04610b/file.txt'>file.txt</a>
46
+ #
47
+ # @param [String, Array<String>] paths
48
+ # A list of strings to convert to gitlab anchors
49
+ # @param [Boolean] full_path
50
+ # Shows the full path as the link's text, defaults to +true+.
51
+ #
52
+ # @see https://danger.systems/reference.html Danger reference where #html_link is described
53
+ # @see https://github.com/danger/danger/blob/eca19719d3e585fe1cc46bc5377f9aa955ebf609/lib/danger/danger_core/plugins/dangerfile_gitlab_plugin.rb#L216 Danger reference where #html_link is implemented
54
+ #
55
+ # @return [String] a list of HTML anchors for a file, or multiple files
56
+ def html_link(paths, full_path: true)
57
+ ci? ? gitlab_helper.html_link(paths, full_path: full_path) : paths
58
+ end
59
+
60
+ # @return [Boolean] whether we're in the CI context or not.
61
+ def ci?
62
+ !gitlab_helper.nil?
63
+ end
64
+
65
+ # @return [Array<String>] a list of filenames added in this MR.
66
+ def added_files
67
+ @added_files ||= if changes_from_api
68
+ changes_from_api.select { |file| file["new_file"] }.map { |file| file["new_path"] }
69
+ else
70
+ git.added_files.to_a
71
+ end
72
+ end
73
+
74
+ # @return [Array<String>] a list of filenames modifier in this MR.
75
+ def modified_files
76
+ @modified_files ||= if changes_from_api
77
+ changes_from_api.select { |file| !file["new_file"] && !file["deleted_file"] && !file["renamed_file"] }.map { |file| file["new_path"] }
78
+ else
79
+ git.modified_files.to_a
80
+ end
81
+ end
82
+
83
+ # @return [Array<String>] a list of filenames renamed in this MR.
84
+ def renamed_files
85
+ @renamed_files ||= if changes_from_api
86
+ changes_from_api.select { |file| file["renamed_file"] }.each_with_object([]) do |file, memo|
87
+ memo << { before: file["old_path"], after: file["new_path"] }
88
+ end
89
+ else
90
+ git.renamed_files.to_a
91
+ end
92
+ end
93
+
94
+ # @return [Array<String>] a list of filenames deleted in this MR.
95
+ def deleted_files
96
+ @deleted_files ||= if changes_from_api
97
+ changes_from_api.select { |file| file["deleted_file"] }.map { |file| file["new_path"] }
98
+ else
99
+ git.deleted_files.to_a
100
+ end
101
+ end
102
+
103
+ # @example
104
+ # # Considering these changes:
105
+ # # - A new_file.rb
106
+ # # - D deleted_file.rb
107
+ # # - M modified_file.rb
108
+ # # - R renamed_file_before.rb -> renamed_file_after.rb
109
+ # # it will return:
110
+ #
111
+ # #=> ['new_file.rb', 'modified_file.rb', 'renamed_file_after.rb']
112
+ #
113
+ #
114
+ # @return [Array<String>] a list of all files that have been added, modified or renamed.
115
+ # +modified_files+ might contain paths that already have been renamed,
116
+ # so we need to remove them from the list.
117
+ def all_changed_files
118
+ Set.new
119
+ .merge(added_files)
120
+ .merge(modified_files)
121
+ .merge(renamed_files.map { |x| x[:after] })
122
+ .subtract(renamed_files.map { |x| x[:before] })
123
+ .to_a
124
+ .sort
125
+ end
126
+
127
+ # @param filename [String] A file name for which we want the diff.
128
+ #
129
+ # @example
130
+ # # Considering changing a line in lib/gitlab/usage_data.rb, it will return:
131
+ #
132
+ # ["--- a/lib/gitlab/usage_data.rb",
133
+ # "+++ b/lib/gitlab/usage_data.rb",
134
+ # "+ # Test change",
135
+ # "- # Old change"]
136
+ #
137
+ # @return [Array<String>] an array of changed lines in Git diff format.
138
+ def changed_lines(filename)
139
+ diff = diff_for_file(filename)
140
+ return [] unless diff
141
+
142
+ diff.split("\n").select { |line| %r{^[+-]}.match?(line) }
143
+ end
144
+
145
+ def release_automation?
146
+ gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
147
+ end
148
+
149
+ # @param items [Array<String>] An array of items to transform into a bullet list.
150
+ #
151
+ # @example
152
+ # markdown_list(%w[foo bar])
153
+ # # => * foo
154
+ # * bar
155
+ #
156
+ # @return [String] a bullet list for the given +items+. If there are more than 10 items, wrap the list in a +<details></details>+ block.
157
+ def markdown_list(items)
158
+ list = items.map { |item| "* `#{item}`" }.join("\n")
159
+
160
+ if items.size > 10
161
+ "\n<details>\n\n#{list}\n\n</details>\n"
162
+ else
163
+ list
164
+ end
165
+ end
166
+
167
+ # @param categories [{Regexp => Array<Symbol>}, {Array<Regexp> => Array<Symbol>}] A hash of the form +{ filename_regex => categories, [filename_regex, changes_regex] => categories }+.
168
+ # +filename_regex+ is the regex pattern to match file names. +changes_regex+ is the regex pattern to
169
+ # match changed lines in files that match +filename_regex+
170
+ #
171
+ # @return [{Symbol => Array<String>}] a hash of the type +{ category1: ["file1", "file2"], category2: ["file3", "file4"] }+
172
+ # using filename regex (+filename_regex+) and specific change regex (+changes_regex+) from the given +categories+ hash.
173
+ def changes_by_category(categories)
174
+ all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
175
+ categories_for_file(file, categories).each { |category| hash[category] << file }
176
+ end
177
+ end
178
+
179
+ # @param categories [{Regexp => Array<Symbol>}, {Array<Regexp> => Array<Symbol>}] A hash of the form +{ filename_regex => categories, [filename_regex, changes_regex] => categories }+.
180
+ # +filename_regex+ is the regex pattern to match file names. +changes_regex+ is the regex pattern to
181
+ # match changed lines in files that match +filename_regex+
182
+ #
183
+ # @return [Gitlab::Dangerfiles::Changes] a +Gitlab::Dangerfiles::Changes+ object that represents the changes of an MR
184
+ # using filename regex (+filename_regex+) and specific change regex (+changes_regex+) from the given +categories+ hash.
185
+ def changes(categories)
186
+ Gitlab::Dangerfiles::Changes.new([]).tap do |changes|
187
+ added_files.each do |file|
188
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :added, category) }
189
+ end
190
+
191
+ modified_files.each do |file|
192
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :modified, category) }
193
+ end
194
+
195
+ deleted_files.each do |file|
196
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :deleted, category) }
197
+ end
198
+
199
+ renamed_files.map { |x| x[:before] }.each do |file|
200
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_before, category) }
201
+ end
202
+
203
+ renamed_files.map { |x| x[:after] }.each do |file|
204
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_after, category) }
205
+ end
206
+ end
207
+ end
208
+
209
+ # @param filename [String] A file name.
210
+ # @param categories [{Regexp => Array<Symbol>}, {Array<Regexp> => Array<Symbol>}] A hash of the form +{ filename_regex => categories, [filename_regex, changes_regex] => categories }+.
211
+ # +filename_regex+ is the regex pattern to match file names. +changes_regex+ is the regex pattern to
212
+ # match changed lines in files that match +filename_regex+
213
+ #
214
+ # @return [Array<Symbol>] the categories a file is in, e.g., +[:frontend]+, +[:backend]+, or +%i[frontend engineering_productivity]+
215
+ # using filename regex (+filename_regex+) and specific change regex (+changes_regex+) from the given +categories+ hash.
216
+ def categories_for_file(filename, categories)
217
+ _, categories = categories.find do |key, _|
218
+ filename_regex, changes_regex = Array(key)
219
+
220
+ found = filename_regex.match?(filename)
221
+ found &&= changed_lines(filename).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex
222
+
223
+ found
224
+ end
225
+
226
+ Array(categories || :unknown)
227
+ end
228
+
229
+ # @param category [Symbol] A category.
230
+ #
231
+ # @return [String] the GFM for a category label, making its best guess if it's not
232
+ # a category we know about.
233
+ def label_for_category(category)
234
+ CATEGORY_LABELS.fetch(category, "~#{category}")
235
+ end
236
+
237
+ # @return [String] +""+ when not in the CI context, and the MR IID as a string otherwise.
238
+ def mr_iid
239
+ return "" unless ci?
240
+
241
+ gitlab_helper.mr_json["iid"].to_s
242
+ end
243
+
244
+ # @return [String] +`whoami`+ when not in the CI context, and the MR author username otherwise.
245
+ def mr_author
246
+ return `whoami`.strip unless ci?
247
+
248
+ gitlab_helper.mr_author
249
+ end
250
+
251
+ # @return [String] +""+ when not in the CI context, and the MR title otherwise.
252
+ def mr_title
253
+ return "" unless ci?
254
+
255
+ gitlab_helper.mr_json["title"]
256
+ end
257
+
258
+ # @return [String] +""+ when not in the CI context, and the MR URL otherwise.
259
+ def mr_web_url
260
+ return "" unless ci?
261
+
262
+ gitlab_helper.mr_json["web_url"]
263
+ end
264
+
265
+ # @return [Array<String>] +[]+ when not in the CI context, and the MR labels otherwise.
266
+ def mr_labels
267
+ return [] unless ci?
268
+
269
+ gitlab_helper.mr_labels
270
+ end
271
+
272
+ # @return [String] +`git rev-parse --abbrev-ref HEAD`+ when not in the CI context, and the MR source branch otherwise.
273
+ def mr_source_branch
274
+ return `git rev-parse --abbrev-ref HEAD`.strip unless ci?
275
+
276
+ gitlab_helper.mr_json["source_branch"]
277
+ end
278
+
279
+ # @return [String] +""+ when not in the CI context, and the MR target branch otherwise.
280
+ def mr_target_branch
281
+ return "" unless ci?
282
+
283
+ gitlab_helper.mr_json["target_branch"]
284
+ end
285
+
286
+ # @return [Boolean] whether a MR is a Draft or not.
287
+ def draft_mr?
288
+ Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
289
+ end
290
+
291
+ # @return [Boolean] whether a MR is opened in the security mirror or not.
292
+ def security_mr?
293
+ mr_web_url.include?("/gitlab-org/security/")
294
+ end
295
+
296
+ # @return [Boolean] whether a MR title includes "cherry-pick" or not.
297
+ def cherry_pick_mr?
298
+ Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
299
+ end
300
+
301
+ # @return [Boolean] whether a MR title includes "RUN ALL RSPEC" or not.
302
+ def run_all_rspec_mr?
303
+ Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
304
+ end
305
+
306
+ # @return [Boolean] whether a MR title includes "RUN AS-IF-FOSS" or not.
307
+ def run_as_if_foss_mr?
308
+ Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
309
+ end
310
+
311
+ # @return [Boolean] whether a MR targets a stable branch or not.
312
+ def stable_branch?
313
+ /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
314
+ end
315
+
316
+ # Whether a MR has any database-scoped labels (i.e. +"database::*"+) set or not.
317
+ #
318
+ # @return [Boolean]
319
+ def has_database_scoped_labels?
320
+ mr_labels.any? { |label| label.start_with?("database::") }
321
+ end
322
+
323
+ # @return [Boolean] whether a MR has any CI-related changes (i.e. +".gitlab-ci.yml"+ or +".gitlab/ci/*"+) or not.
324
+ def has_ci_changes?
325
+ changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
326
+ end
327
+
328
+ # @param labels [Array<String>] An array of labels.
329
+ #
330
+ # @return [Boolean] whether a MR has the given +labels+ set or not.
331
+ def mr_has_labels?(*labels)
332
+ labels = labels.flatten.uniq
333
+
334
+ (labels & mr_labels) == labels
335
+ end
336
+
337
+ # @param labels [Array<String>] An array of labels.
338
+ # @param sep [String] A separator.
339
+ #
340
+ # @example
341
+ # labels_list(["foo", "bar baz"], sep: "; ")
342
+ # # => '~"foo"; ~"bar baz"'
343
+ #
344
+ # @return [String] the list of +labels+ ready for being used in a Markdown comment, separated by +sep+.
345
+ def labels_list(labels, sep: ", ")
346
+ labels.map { |label| %Q{~"#{label}"} }.join(sep)
347
+ end
348
+
349
+ # @deprecated Use {#quick_action_label} instead.
350
+ def prepare_labels_for_mr(labels)
351
+ quick_action_label(labels)
352
+ end
353
+
354
+ # @param labels [Array<String>] An array of labels.
355
+ #
356
+ # @example
357
+ # quick_action_label(["foo", "bar baz"])
358
+ # # => '/label ~"foo" ~"bar baz"'
359
+ #
360
+ # @return [String] a quick action to set the +given+ labels. Returns +""+ if +labels+ is empty.
361
+ def quick_action_label(labels)
362
+ return "" unless labels.any?
363
+
364
+ "/label #{labels_list(labels, sep: " ")}"
365
+ end
366
+
367
+ # @param regex [Regexp] A Regexp to match against.
368
+ #
369
+ # @return [Array<String>] changed files matching the given +regex+.
370
+ def changed_files(regex)
371
+ all_changed_files.grep(regex)
372
+ end
373
+
374
+ # @return [Array<String>] the group labels (i.e. +"group::*"+) set on the MR.
375
+ def group_label
376
+ mr_labels.find { |label| label.start_with?("group::") }
377
+ end
378
+
379
+ private
380
+
381
+ # @return [Danger::RequestSources::GitLab, nil] the +gitlab+ helper, or +nil+ when it's not available.
382
+ def gitlab_helper
383
+ # Unfortunately the following does not work:
384
+ # - respond_to?(:gitlab)
385
+ # - respond_to?(:gitlab, true)
386
+ gitlab
387
+ rescue NoMethodError
388
+ nil
389
+ end
390
+
391
+ # @param filename [String] A filename for which we want the diff.
392
+ #
393
+ # @return [String] the raw diff as a string for the given +filename+.
394
+ def diff_for_file(filename)
395
+ if changes_from_api
396
+ changes_hash = changes_from_api.find { |file| file["new_path"] == filename }
397
+ changes_hash["diff"] if changes_hash
398
+ else
399
+ git.diff_for_file(filename)&.patch
400
+ end
401
+ end
402
+
403
+ # Fetches MR changes from the API instead of Git (default).
404
+ #
405
+ # @return [Array<Hash>, nil]
406
+ def changes_from_api
407
+ return nil unless ci?
408
+ return nil if defined?(@force_changes_from_git)
409
+
410
+ @changes_from_api ||= gitlab_helper.mr_changes.to_h["changes"]
411
+ rescue
412
+ # Fallback to the Git strategy in any case
413
+ @force_changes_from_git = true
414
+ nil
415
+ end
416
+ end
417
+ end