danger-dangermattic 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.buildkite/gem-push.sh +15 -0
  3. data/.buildkite/pipeline.yml +69 -0
  4. data/.bundle/config +2 -0
  5. data/.github/workflows/reusable-check-labels-on-issues.yml +91 -0
  6. data/.github/workflows/reusable-run-danger.yml +54 -0
  7. data/.gitignore +30 -0
  8. data/.rubocop.yml +67 -0
  9. data/.ruby-version +1 -0
  10. data/.yardopts +7 -0
  11. data/CHANGELOG.md +7 -0
  12. data/Gemfile +5 -0
  13. data/Gemfile.lock +191 -0
  14. data/Guardfile +21 -0
  15. data/LICENSE +373 -0
  16. data/README.md +68 -0
  17. data/Rakefile +24 -0
  18. data/danger-dangermattic.gemspec +58 -0
  19. data/lib/danger_dangermattic.rb +3 -0
  20. data/lib/danger_plugin.rb +4 -0
  21. data/lib/dangermattic/gem_version.rb +5 -0
  22. data/lib/dangermattic/plugins/android_release_checker.rb +50 -0
  23. data/lib/dangermattic/plugins/android_strings_checker.rb +31 -0
  24. data/lib/dangermattic/plugins/android_unit_test_checker.rb +187 -0
  25. data/lib/dangermattic/plugins/common/common_release_checker.rb +113 -0
  26. data/lib/dangermattic/plugins/common/git_utils.rb +166 -0
  27. data/lib/dangermattic/plugins/common/github_utils.rb +68 -0
  28. data/lib/dangermattic/plugins/common/reporter.rb +38 -0
  29. data/lib/dangermattic/plugins/ios_release_checker.rb +106 -0
  30. data/lib/dangermattic/plugins/labels_checker.rb +74 -0
  31. data/lib/dangermattic/plugins/manifest_pr_checker.rb +106 -0
  32. data/lib/dangermattic/plugins/milestone_checker.rb +98 -0
  33. data/lib/dangermattic/plugins/podfile_checker.rb +122 -0
  34. data/lib/dangermattic/plugins/pr_size_checker.rb +125 -0
  35. data/lib/dangermattic/plugins/tracks_checker.rb +72 -0
  36. data/lib/dangermattic/plugins/view_changes_checker.rb +46 -0
  37. data/spec/android_release_checker_spec.rb +93 -0
  38. data/spec/android_strings_checker_spec.rb +185 -0
  39. data/spec/android_unit_test_checker_spec.rb +343 -0
  40. data/spec/common_release_checker_spec.rb +70 -0
  41. data/spec/fixtures/android_unit_test_checker/Abc.java +7 -0
  42. data/spec/fixtures/android_unit_test_checker/AbcFeatureConfig.java +7 -0
  43. data/spec/fixtures/android_unit_test_checker/Abcdef.kt +5 -0
  44. data/spec/fixtures/android_unit_test_checker/AbcdefgViewHelper.java +7 -0
  45. data/spec/fixtures/android_unit_test_checker/AnotherViewHelper.kt +7 -0
  46. data/spec/fixtures/android_unit_test_checker/MyNewClass.java +7 -0
  47. data/spec/fixtures/android_unit_test_checker/Polygon.kt +3 -0
  48. data/spec/fixtures/android_unit_test_checker/Shape.kt +10 -0
  49. data/spec/fixtures/android_unit_test_checker/TestsINeedThem.java +5 -0
  50. data/spec/fixtures/android_unit_test_checker/TestsINeedThem.kt +7 -0
  51. data/spec/fixtures/android_unit_test_checker/TestsINeedThem2.kt +12 -0
  52. data/spec/fixtures/android_unit_test_checker/src/android/java/org/activities/MyActivity.kt +7 -0
  53. data/spec/fixtures/android_unit_test_checker/src/android/java/org/activities/MyJavaActivity.java +7 -0
  54. data/spec/fixtures/android_unit_test_checker/src/android/java/org/fragments/MyFragment.kt +6 -0
  55. data/spec/fixtures/android_unit_test_checker/src/android/java/org/fragments/MyNewJavaFragment.java +7 -0
  56. data/spec/fixtures/android_unit_test_checker/src/android/java/org/module/MyModule.java +13 -0
  57. data/spec/fixtures/android_unit_test_checker/src/android/java/org/view/ActionCardViewHolder.kt +22 -0
  58. data/spec/fixtures/android_unit_test_checker/src/android/java/org/view/MyRecyclerView.java +7 -0
  59. data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/AbcTests.java +5 -0
  60. data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/AnotherTestClass.java +7 -0
  61. data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/PolygonTest.kt +4 -0
  62. data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/TestMyNewClass.java +9 -0
  63. data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/ToolTest.kt +5 -0
  64. data/spec/fixtures/android_unit_test_checker/src/main/java/org/wordpress/android/widgets/NestedWebView.kt +32 -0
  65. data/spec/fixtures/android_unit_test_checker/src/main/java/org/wordpress/util/config/BloggingPromptsFeatureConfig.kt +23 -0
  66. data/spec/fixtures/android_unit_test_checker/src/test/java/org/test/TestsINeedThem.java +9 -0
  67. data/spec/github_utils_spec.rb +110 -0
  68. data/spec/ios_release_checker_spec.rb +191 -0
  69. data/spec/labels_checker_spec.rb +169 -0
  70. data/spec/manifest_pr_checker_spec.rb +140 -0
  71. data/spec/milestone_checker_spec.rb +222 -0
  72. data/spec/podfile_checker_spec.rb +595 -0
  73. data/spec/pr_size_checker_spec.rb +250 -0
  74. data/spec/spec_helper.rb +115 -0
  75. data/spec/tracks_checker_spec.rb +156 -0
  76. data/spec/view_changes_checker_spec.rb +168 -0
  77. metadata +341 -0
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # This plugin provides methods to check for the presence of unit tests for newly added classes in a pull request.
5
+ #
6
+ # @example Check missing unit tests using the default parameters:
7
+ # android_unit_test_checker.check_missing_tests
8
+ #
9
+ # @example Check missing unit tests while excluding certain classes, subclasses, and paths:
10
+ # android_unit_test_checker.check_missing_tests(
11
+ # classes_exceptions: [/ViewHolder$/],
12
+ # subclasses_exceptions: [/RecyclerView/],
13
+ # path_exceptions: ['*.java', 'org/app/ui/**']
14
+ # )
15
+ #
16
+ # @example Check missing unit tests with a custom bypass label:
17
+ # android_unit_test_checker.check_missing_tests(bypass_label: 'BypassTestCheck')
18
+ #
19
+ # @example Check missing unit tests excluding certain classes, subclasses and paths:
20
+ # android_unit_test_checker.check_missing_tests(classes_exceptions: [/ViewHolder$/], subclasses_exceptions: [/RecyclerView/], path_exceptions: ['*.java', 'org/app/ui/**'])
21
+ #
22
+ # @see Automattic/dangermattic
23
+ # @tags android, unit test, github, pull request
24
+ #
25
+ class AndroidUnitTestChecker < Plugin
26
+ ANY_CLASS_DETECTOR = /class\s+([A-Z]\w+)\s*(.*?)\s*{/m
27
+ NON_PRIVATE_CLASS_DETECTOR = /(?:\s|public|internal|protected|final|abstract|static)*class\s+([A-Z]\w+)\s*(.*?)\s*{/m
28
+ DEFAULT_CLASSES_EXCEPTIONS = [
29
+ /ViewHolder$/,
30
+ /Module$/,
31
+ /Button$/
32
+ ].freeze
33
+
34
+ DEFAULT_SUBCLASSES_EXCEPTIONS = [
35
+ /(Fragment|Activity)\b/,
36
+ /RecyclerView/,
37
+ /^BroadcastReceiver$/,
38
+ /^ContentProvider$/,
39
+ /Service$/,
40
+ /View$/,
41
+ /ViewGroup$/,
42
+ /Layout$/
43
+ ].freeze
44
+
45
+ DEFAULT_UNIT_TESTS_BYPASS_PR_LABEL = 'unit-tests-exemption'
46
+
47
+ # Check and warns about missing unit tests for a Git diff, with optional classes/subclasses to ignore and an
48
+ # optional PR label to bypass the checks.
49
+ #
50
+ # @param classes_exceptions [Array<String>] Optional list of regexes matching class names to exclude from the
51
+ # check.
52
+ # Defaults to DEFAULT_CLASSES_EXCEPTIONS.
53
+ # @param subclasses_exceptions [Array<String>] Optional list of regexes matching base class names to exclude from
54
+ # the check.
55
+ # Defaults to DEFAULT_SUBCLASSES_EXCEPTIONS.
56
+ # @param path_exceptions [Array<String>] Optional list of file paths to exclude from the check.
57
+ # Defaults to [].
58
+ # @param bypass_label [String] Optional label to indicate we can bypass the check. Defaults to
59
+ # DEFAULT_UNIT_TESTS_BYPASS_PR_LABEL.
60
+ #
61
+ # @return [void]
62
+ def check_missing_tests(classes_exceptions: DEFAULT_CLASSES_EXCEPTIONS,
63
+ subclasses_exceptions: DEFAULT_SUBCLASSES_EXCEPTIONS,
64
+ path_exceptions: [],
65
+ bypass_label: DEFAULT_UNIT_TESTS_BYPASS_PR_LABEL)
66
+ list = find_classes_missing_tests(
67
+ git_diff: git.diff,
68
+ classes_exceptions: classes_exceptions,
69
+ subclasses_exceptions: subclasses_exceptions,
70
+ path_exceptions: path_exceptions
71
+ )
72
+
73
+ return if list.empty?
74
+
75
+ if danger.github.pr_labels.include?(bypass_label)
76
+ list.each do |c|
77
+ warn("Class `#{c.classname}` is missing tests, but `#{bypass_label}` label was set to ignore this.")
78
+ end
79
+ else
80
+ list.each do |c|
81
+ failure("Please add tests for class `#{c.classname}` (or add `#{bypass_label}` label to ignore this).")
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ ClassViolation = Struct.new(:classname, :file)
89
+
90
+ # @param git_diff [Git::Diff] the git diff object
91
+ # @param classes_exceptions [Array<String>] Regexes matching class names to exclude from the check.
92
+ # @param subclasses_exceptions [Array<String>] Regexes matching base class names to exclude from the check
93
+ # @param path_exceptions [Array<String>] Regexes matching base class names to exclude from the check
94
+ #
95
+ # @return [Array<ClassViolation>] An array of `ClassViolation` objects for each added class that is missing a test
96
+ def find_classes_missing_tests(git_diff:, classes_exceptions:, subclasses_exceptions:, path_exceptions:)
97
+ violations = []
98
+ removed_classes = []
99
+ added_test_lines = []
100
+
101
+ # Parse the diff of each file, storing test lines for test files, and added/removed classes for non-test files
102
+ git_diff.each do |file_diff|
103
+ file_path = file_diff.path
104
+
105
+ next if path_exceptions.any? { |exception| File.fnmatch?(exception, file_path) }
106
+
107
+ if test_file?(path: file_path)
108
+ # Store added test lines from test files
109
+ added_test_lines += file_diff.patch.each_line.select do |line|
110
+ git_utils.change_type(diff_line: line) == :added
111
+ end
112
+ else
113
+ # Detect added classes (violations) and removed classes in non-test files
114
+ patch = file_diff.patch
115
+
116
+ violations += find_violations(
117
+ path: file_path,
118
+ diff_patch: patch,
119
+ classes_exceptions: classes_exceptions,
120
+ subclasses_exceptions: subclasses_exceptions
121
+ )
122
+
123
+ removed_classes += find_removed_classes(diff_patch: patch)
124
+ end
125
+ end
126
+
127
+ # We only want newly added classes, not if class signature was modified or line was moved
128
+ violations.reject! { |v| removed_classes.include?(v.classname) }
129
+
130
+ # For each remaining candidate, only keep the ones _not_ used in a new test.
131
+ # The regex will match usages of this class in any test file
132
+ violations.select { |v| added_test_lines.none? { |line| line =~ /\b#{v.classname}\b/ } }
133
+ end
134
+
135
+ # Finds added classes that potentially will require a test (violations) in the given file based on the changes in the diff patch.
136
+ #
137
+ # @param path [String] The file in the diff to check for violations.
138
+ # @param diff_patch [String] The diff patch containing the changes to the file.
139
+ # @param classes_exceptions [Array<String>] An array of class names that are exceptions and should be ignored.
140
+ # @param subclasses_exceptions [Array<String>] An array of class names whose subclasses should be ignored as well.
141
+ #
142
+ # @return [Array<ClassViolation>] An array of ClassViolation objects representing the violations found.
143
+ def find_violations(path:, diff_patch:, classes_exceptions:, subclasses_exceptions:)
144
+ added_lines = git_utils.added_lines(diff_patch: diff_patch)
145
+ matches = added_lines.scan(NON_PRIVATE_CLASS_DETECTOR)
146
+ matches.reject! do |m|
147
+ class_match_is_exception?(
148
+ m,
149
+ path,
150
+ classes_exceptions,
151
+ subclasses_exceptions
152
+ )
153
+ end
154
+
155
+ matches.map { |m| ClassViolation.new(m[0], path) }
156
+ end
157
+
158
+ # Finds the names of removed classes based on the removals the diff patch.
159
+ #
160
+ # @param diff_patch [String] The diff patch containing the changes to the file.
161
+ #
162
+ # @return [Array<String>] An array with the class names of the classes that were removed in the diff.
163
+ def find_removed_classes(diff_patch:)
164
+ removed_lines = git_utils.removed_lines(diff_patch: diff_patch)
165
+ matches = removed_lines.scan(ANY_CLASS_DETECTOR)
166
+ matches.map { |m| m[0] }
167
+ end
168
+
169
+ # @param match [Array<String>] match an array of captured substrings matching our `*_CLASS_DETECTOR` for a given line
170
+ # @param file [String] file the path to the file where that class declaration line was matched
171
+ # @param classes_exceptions [Array<String>] Regexes matching class names to exclude from the check.
172
+ # @param subclasses_exceptions [Array<String>] Regexes matching base class names to exclude from the check
173
+ #
174
+ # @return [void]
175
+ def class_match_is_exception?(match, file, classes_exceptions, subclasses_exceptions)
176
+ return true if classes_exceptions.any? { |re| match[0] =~ re }
177
+
178
+ subclass_regexp = File.extname(file) == '.java' ? /extends\s+([A-Z]\w+)/m : /\s*:\s*([A-Z]\w+)/m
179
+ subclass = match[1].scan(subclass_regexp)&.last&.last
180
+ subclasses_exceptions.any? { |re| subclass =~ re }
181
+ end
182
+
183
+ def test_file?(path:)
184
+ path.match? %r{/(test|androidTest).*\.(java|kt)$}
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # Plugin to perform generic checks related to releases.
5
+ # It can be used directly or via the specialised plugins `AndroidReleaseChecker` and `IosReleaseChecker`.
6
+ #
7
+ # @example Checking if a specific file has changed on a release branch:
8
+ # common_release_checker.check_file_changed(
9
+ # file_comparison: ->(path) { path == 'metadata/full_release_notes.txt' },
10
+ # message: 'Release notes have been modified on a release branch.',
11
+ # on_release_branch: true
12
+ # )
13
+ #
14
+ # @example Checking if release notes and store strings have changed:
15
+ # common_release_checker.check_release_notes_and_store_strings(
16
+ # release_notes_file: 'metadata/release_notes.txt',
17
+ # po_file: 'metadata/PlayStoreStrings.po'
18
+ # )
19
+ #
20
+ # @example Checking for changes in internal release notes:
21
+ # common_release_checker.check_internal_release_notes_changed
22
+ #
23
+ # @see Automattic/dangermattic
24
+ # @tags util, process, release
25
+ #
26
+ class CommonReleaseChecker < Plugin
27
+ DEFAULT_INTERNAL_RELEASE_NOTES = 'RELEASE-NOTES.txt'
28
+
29
+ MESSAGE_STORE_FILE_NOT_CHANGED = 'The `%s` file should be updated if the editorialized release notes file `%s` is being changed.'
30
+ MESSAGE_INTERNAL_RELEASE_NOTES_CHANGED = <<~WARNING
31
+ This PR contains changes to `%s`.
32
+ Note that these changes won't affect the final version of the release notes as this version is in code freeze.
33
+ Please, get in touch with a release manager if you want to update the final release notes.
34
+ WARNING
35
+
36
+ # Check if certain files have been modified, returning a warning or failure message based on the branch type.
37
+ #
38
+ # @param file_comparison [Proc] Function used to compare modified file paths.
39
+ # It should take a single argument, which is the path to a modified file,
40
+ # and return true if the file matches the desired condition.
41
+ # Example: `file_comparison = ->(file_path) { file_path.include?('app/') }`
42
+ #
43
+ # @param message [String] The message to display in the warning or failure output if the condition is met.
44
+ #
45
+ # @param on_release_branch [Boolean] If true, the check will only run on release branches, otherwise on non-release branches.
46
+ #
47
+ # @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
48
+ #
49
+ # @example Check if any modified file is under the 'app/' directory and emit a warning on release branches:
50
+ # check_file_changed(file_comparison: ->(file_path) { file_path.include?('app/') },
51
+ # message: 'Some files in the "app/" directory have been modified. Please review the changes.',
52
+ # on_release_branch: true)
53
+ #
54
+ # @example Check if a specific file has been modified and emit a failure on non-release branches:
55
+ # check_file_changed(file_comparison: ->(file_path) { file_path == 'path/to/file/DoNotChange.java' },
56
+ # message: 'The "DoNotChange.java" file has been modified. This change is not allowed on non-release branches.',
57
+ # on_release_branch: false,
58
+ # report_type: :error)
59
+ #
60
+ # @return [void]
61
+ def check_file_changed(file_comparison:, message:, on_release_branch:, report_type: :warning)
62
+ has_modified_file = git_utils.all_changed_files.any?(&file_comparison)
63
+
64
+ should_be_changed = (on_release_branch == github_utils.release_branch?)
65
+ return unless should_be_changed && has_modified_file
66
+
67
+ reporter.report(message: message, type: report_type)
68
+ end
69
+
70
+ # Check if the release notes and store strings files are correctly updated after a modification to the release notes.
71
+ #
72
+ # @param release_notes_file [String] The name of the release notes file that should be checked for modifications.
73
+ # Example: 'metadata/release_notes.txt'
74
+ #
75
+ # @param po_file [String] The name of the store strings file that should be checked for modifications.
76
+ # Example: 'metadata/PlayStoreStrings.po'
77
+ #
78
+ # @example Check if the release notes file 'release_notes.txt' is modified, and the 'PlayStoreStrings.po' file is not modified, posting a message:
79
+ # check_release_notes_and_store_strings(release_notes_file: 'release_notes.txt', po_file: 'PlayStoreStrings.po')
80
+ #
81
+ # @return [void]
82
+ def check_release_notes_and_store_strings(release_notes_file:, po_file:)
83
+ has_modified_release_notes = danger.git.modified_files.any? { |f| f == release_notes_file }
84
+ has_modified_app_store_strings = danger.git.modified_files.any? { |f| f == po_file }
85
+
86
+ return unless has_modified_release_notes && !has_modified_app_store_strings
87
+
88
+ message(format(MESSAGE_STORE_FILE_NOT_CHANGED, po_file, release_notes_file))
89
+ end
90
+
91
+ # Check if there are changes to the internal release notes file in the release branch and emit a warning if that's the case.
92
+ #
93
+ # @param release_notes_file [String] (optional) The path to the internal release notes file.
94
+ # Defaults to the `DEFAULT_INTERNAL_RELEASE_NOTES` constant if not provided.
95
+ # @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
96
+ #
97
+ # @example Checking for changes in the default internal release notes file:
98
+ # check_internal_release_notes_changed
99
+ #
100
+ # @example Checking for changes in a custom internal release notes file at a specific path:
101
+ # check_internal_release_notes_changed(release_notes_file: '/path/to/internal_release_notes.txt')
102
+ #
103
+ # @return [void]
104
+ def check_internal_release_notes_changed(release_notes_file: DEFAULT_INTERNAL_RELEASE_NOTES, report_type: :warning)
105
+ check_file_changed(
106
+ file_comparison: ->(path) { path == release_notes_file },
107
+ message: format(MESSAGE_INTERNAL_RELEASE_NOTES_CHANGED, release_notes_file),
108
+ on_release_branch: true,
109
+ report_type: report_type
110
+ )
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # Represents a utility plugin for working with Git: to check added and modified lines in Git diffs,
5
+ # to determine the type of change (added, removed, or other) in a diff line, and to retrieve lists of
6
+ # added, modified, and deleted files.
7
+ #
8
+ # @example Check if there is a "TODO" in Ruby files:
9
+ # git_utils.check_added_diff_lines(
10
+ # file_selector: ->(path) { path.end_with?('.rb') },
11
+ # line_matcher: ->(line) { line.include?('TODO') },
12
+ # message: 'Found a TODO in a Ruby file'
13
+ # )
14
+ #
15
+ # @example Get added lines from a diff patch:
16
+ # added_lines = git_utils.added_lines(diff_patch: diff_patch)
17
+ #
18
+ # @example Get removed lines from a diff patch:
19
+ # removed_lines = git_utils.removed_lines(diff_patch: diff_patch)
20
+ #
21
+ # @example Determining the change type of a diff line:
22
+ # git_utils.change_type(diff_line: "+ new line added")
23
+ # #=> :added
24
+ #
25
+ # git_utils.change_type(diff_line: "- line removed")
26
+ # #=> :removed
27
+ #
28
+ # git_utils.change_type(diff_line: " context line")
29
+ # #=> :other
30
+ #
31
+ # @example Select removed lines from a diff patch:
32
+ # removed_lines = git_utils.select_lines(diff_patch: diff_patch, change_type: :removed)
33
+ #
34
+ # @see Automattic/dangermattic
35
+ # @tags tool, util, git
36
+ #
37
+ class GitUtils < Plugin
38
+ # Check added lines in a PR for a specific pattern and issue a warning or failure message when found.
39
+ #
40
+ # @param file_selector [Proc] A block to select the files in the PR.
41
+ # The block should take a file path as input and return true if the file should be checked.
42
+ #
43
+ # @param line_matcher [Proc] A block that will select the diff lines to report.
44
+ # The block should take a line of text as input and return true if the line matches the pattern.
45
+ #
46
+ # @param message [String] The warning or failure message to display when the pattern is found in a line.
47
+ #
48
+ # @param report_type [Symbol] (optional) Type of report (:error, :warning, :message) whenever a line matches the criteria. Default is :warning.
49
+ #
50
+ # @example Checking for added lines containing 'FIXME' and failing the build:
51
+ # check_added_diff_lines(file_selector: ->(path) { File.extname(path) == ('.swift') }, line_matcher: ->(line) { line.include?("FIXME") }, message: "A FIXME was added, failing build.", report_type: :error)
52
+ #
53
+ # @return [void]
54
+ def check_added_diff_lines(file_selector:, line_matcher:, message:, report_type: :warning)
55
+ modified_files = added_and_modified_files.select(&file_selector)
56
+
57
+ matches = matching_lines_in_diff_files(
58
+ files: modified_files,
59
+ line_matcher: line_matcher,
60
+ change_type: :added
61
+ )
62
+
63
+ matches.each do |match|
64
+ match.lines.each do |line|
65
+ final_message = <<~MESSAGE
66
+ #{message}
67
+ File `#{match.file}`:
68
+ ```diff
69
+ #{line.chomp}
70
+ ```
71
+ MESSAGE
72
+
73
+ reporter.report(message: final_message, type: report_type)
74
+ end
75
+ end
76
+ end
77
+
78
+ MatchedData = Struct.new(:file, :lines)
79
+
80
+ # Matches diff lines in the provided files based on the line matcher and change type
81
+ #
82
+ # @param files [Array<String>] List of file names to check
83
+ # @param line_matcher [Proc] A callable that takes a line and returns true if it matches the desired pattern
84
+ # @param change_type [Symbol, nil] Change type to filter lines (e.g., :added, :removed) or nil for no filter
85
+ # @return [Array<MatchedData>] Array of MatchedData objects representing matched lines in files
86
+ def matching_lines_in_diff_files(files:, line_matcher:, change_type: nil)
87
+ matched_data = []
88
+
89
+ files.each do |file|
90
+ matched_lines = []
91
+
92
+ diff = danger.git.diff_for_file(file)
93
+
94
+ diff.patch.each_line do |line|
95
+ matched_lines << line if line_matcher.call(line) && (change_type.nil? || change_type(diff_line: line) == change_type)
96
+ end
97
+
98
+ matched_data << MatchedData.new(file, matched_lines) unless matched_lines.empty?
99
+ end
100
+
101
+ matched_data
102
+ end
103
+
104
+ # Determine the type of change for a given line in a git diff.
105
+ #
106
+ # @param diff_line [String] The line from a git diff that needs to be classified.
107
+ #
108
+ # @return [Symbol] The type of change for the given diff line. Possible values are:
109
+ # - :added for added lines
110
+ # - :removed for removed lines
111
+ # - :other for any other type of lines
112
+ def change_type(diff_line:)
113
+ if diff_line.start_with?('+') && !diff_line.start_with?('+++ ')
114
+ :added
115
+ elsif diff_line.start_with?('-') && !diff_line.start_with?('--- ')
116
+ :removed
117
+ else
118
+ :other
119
+ end
120
+ end
121
+
122
+ # Get the list of added and modified files in the current Pull Request.
123
+ #
124
+ # @return [Array<String>] An array containing the file paths of added and modified files.
125
+ def added_and_modified_files
126
+ danger.git.added_files + danger.git.modified_files
127
+ end
128
+
129
+ # Get the list of all files added, modified and deleted in the current Pull Request.
130
+ #
131
+ # @return [Array<String>] An array containing the file paths of all changed files.
132
+ #
133
+ def all_changed_files
134
+ danger.git.added_files + danger.git.modified_files + danger.git.deleted_files
135
+ end
136
+
137
+ # Returns the lines that were added in the given diff patch.
138
+ #
139
+ # @param diff_patch [String] The diff patch containing the changes.
140
+ #
141
+ # @return [String] A concatenated string of added lines.
142
+ def added_lines(diff_patch:)
143
+ select_lines(diff_patch: diff_patch, change_type: :added)
144
+ end
145
+
146
+ # Returns the lines that were removed in the given diff patch.
147
+ #
148
+ # @param diff_patch [String] The diff patch containing the changes.
149
+ #
150
+ # @return [String] A concatenated string of removed lines.
151
+ def removed_lines(diff_patch:)
152
+ select_lines(diff_patch: diff_patch, change_type: :removed)
153
+ end
154
+
155
+ # Selects lines of a specific change type (added or removed) from the given diff patch.
156
+ #
157
+ # @param diff_patch [String] The diff patch containing the changes.
158
+ # @param change_type [Symbol] The desired change type (:added or :removed).
159
+ #
160
+ # @return [String] A concatenated string of selected lines of the specified change type.
161
+ def select_lines(diff_patch:, change_type:)
162
+ selected_lines = diff_patch.lines.select { |line| change_type(diff_line: line) == change_type }
163
+ selected_lines.map { |line| line[1..] }.join
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # Provides common GitHub utility methods related to Pull Requests in a Danger context.
5
+ #
6
+ # @example Checking if the branch is a release or hotfix branch:
7
+ # github_utils.release_branch? #=> true or false
8
+ #
9
+ # @example Checking if there are active reviewers on the PR:
10
+ # github_utils.active_reviewers? #=> true or false
11
+ #
12
+ # @example Checking if there are requested teams or reviewers on the PR:
13
+ # github_utils.requested_reviewers? #=> true or false
14
+ #
15
+ # @example Checking if the branch is a main branch (trunk, main, master, or develop):
16
+ # github_utils.main_branch? #=> true or false
17
+ #
18
+ # @example Checking if the PR is a work-in-progress (WIP) based on labels or title:
19
+ # github_utils.wip_feature? #=> true or false
20
+ #
21
+ # @see Automattic/dangermattic
22
+ # @tags tool, util
23
+ #
24
+ class GithubUtils < Plugin
25
+ # Checks if there are active reviewers providing feedback and potentially changing the state of the PR
26
+ # (e.g., approved, changes requested).
27
+ #
28
+ # @return [Boolean] True if there are active reviewers, otherwise false.
29
+ def active_reviewers?
30
+ repo_name = github.pr_json['base']['repo']['full_name']
31
+ pr_number = github.pr_json['number']
32
+
33
+ !github.api.pull_request_reviews(repo_name, pr_number).empty?
34
+ end
35
+
36
+ # Checks if there are requested teams or reviewers who haven't reacted yet.
37
+ #
38
+ # @return [Boolean] True if there are requested teams or reviewers, otherwise false.
39
+ def requested_reviewers?
40
+ has_requested_reviews = !github.pr_json['requested_teams'].to_a.empty? || !github.pr_json['requested_reviewers'].to_a.empty?
41
+ has_requested_reviews || active_reviewers?
42
+ end
43
+
44
+ # Checks if the current branch is a main branch (trunk, main, master, or develop).
45
+ #
46
+ # @return [Boolean] True if the branch is a main branch, otherwise false.
47
+ def main_branch?
48
+ %w[trunk main master develop].include?(github.branch_for_base)
49
+ end
50
+
51
+ # Checks if the current branch is a release or hotfix branch.
52
+ #
53
+ # @return [Boolean] True if the branch is a release or hotfix branch, otherwise false.
54
+ def release_branch?
55
+ github.branch_for_base.start_with?('release/') || github.branch_for_base.start_with?('hotfix/')
56
+ end
57
+
58
+ # Checks if the PR is a work-in-progress (WIP) based on labels or title.
59
+ #
60
+ # @return [Boolean] True if the PR is a work-in-progress, otherwise false.
61
+ def wip_feature?
62
+ has_wip_label = github.pr_labels.any? { |label| label.include?('WIP') }
63
+ has_wip_title = github.pr_title.include?('WIP')
64
+
65
+ has_wip_label || has_wip_title
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # Handles reporting messages in the context of Danger, allowing the generation of
5
+ # warnings, errors, and simple messages in a Danger run.
6
+ #
7
+ # @example Reporting a Warning
8
+ # reporter.report(message: "This is a warning message", type: :warning)
9
+ #
10
+ # @example Reporting an Error
11
+ # reporter.report(message: "This is an error message", type: :error)
12
+ #
13
+ # @see Automattic/dangermattic
14
+ # @tags tool, util, danger
15
+ #
16
+ class Reporter < Plugin
17
+ # Report a message to be posted by Danger as an error (failing the build), a warning or a simple message.
18
+ #
19
+ # @param message [String] The message to be reported to Danger.
20
+ # @param type [Symbol] The type of report. Possible values:
21
+ # - :warning (default): Reports a warning.
22
+ # - :error: Reports an error.
23
+ # - :message: Reports a simple message.
24
+ # - Any other value or nil: Takes no action.
25
+ #
26
+ # @return [void]
27
+ def report(message:, type: :warning)
28
+ case type
29
+ when :error
30
+ failure(message)
31
+ when :warning
32
+ warn(message)
33
+ when :message
34
+ danger.message(message)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Danger
4
+ # Plugin for performing iOS / macOS release-related checks in a pull request.
5
+ #
6
+ # @example Checking for changes in Core Data models on a release branch:
7
+ # ios_release_checker.check_core_data_model_changed
8
+ #
9
+ # @example Checking for modified Localizable.strings on a regular branch:
10
+ # ios_release_checker.check_modified_localizable_strings
11
+ #
12
+ # @example Checking for synchronization between release notes and App Store strings:
13
+ # ios_release_checker.check_release_notes_and_app_store_strings
14
+ #
15
+ # @see Automattic/dangermattic
16
+ # @tags ios, macos, process, release
17
+ #
18
+ class IosReleaseChecker < Plugin
19
+ LOCALIZABLE_STRINGS_FILE = 'Localizable.strings'
20
+ BASE_STRINGS_FILE = "en.lproj/#{LOCALIZABLE_STRINGS_FILE}".freeze
21
+
22
+ MESSAGE_STRINGS_FILE_UPDATED = "The `#{LOCALIZABLE_STRINGS_FILE}` files should only be updated on release branches, when the translations are downloaded by our automation.".freeze
23
+ MESSAGE_BASE_STRINGS_FILE_UPDATED = "The `#{BASE_STRINGS_FILE}` file should only be updated before creating a release branch.".freeze
24
+ MESSAGE_TRANSLATION_FILE_UPDATED = "Translation files `*.lproj/#{LOCALIZABLE_STRINGS_FILE}` should only be updated on a release branch.".freeze
25
+ MESSAGE_CORE_DATA_UPDATED = 'Do not edit an existing Core Data model in a release branch unless it hasn\'t been released to testers yet. ' \
26
+ 'Instead create a new model version and merge back to develop soon.'
27
+
28
+ # Checks if an existing Core Data model has been edited in a release branch.
29
+ #
30
+ # @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
31
+ #
32
+ # @return [void]
33
+ def check_core_data_model_changed(report_type: :warning)
34
+ common_release_checker.check_file_changed(
35
+ file_comparison: ->(path) { File.extname(path) == '.xcdatamodeld' },
36
+ message: MESSAGE_CORE_DATA_UPDATED,
37
+ on_release_branch: true,
38
+ report_type: report_type
39
+ )
40
+ end
41
+
42
+ # Checks if any Localizable.strings file has been modified on a release branch, otherwise reporting a warning.
43
+ #
44
+ # @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
45
+ #
46
+ # @return [void]
47
+ def check_modified_localizable_strings_on_release(report_type: :warning)
48
+ common_release_checker.check_file_changed(
49
+ file_comparison: ->(path) { File.basename(path) == LOCALIZABLE_STRINGS_FILE },
50
+ message: MESSAGE_STRINGS_FILE_UPDATED,
51
+ on_release_branch: false,
52
+ report_type: report_type
53
+ )
54
+ end
55
+
56
+ # Checks if the en.lproj/Localizable.strings file has been modified on a regular branch, otherwise reporting a warning.
57
+ #
58
+ # @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
59
+ #
60
+ # @return [void]
61
+ def check_modified_en_strings_on_regular_branch(report_type: :warning)
62
+ common_release_checker.check_file_changed(
63
+ file_comparison: ->(path) { base_strings_file?(path: path) },
64
+ message: MESSAGE_BASE_STRINGS_FILE_UPDATED,
65
+ on_release_branch: true,
66
+ report_type: report_type
67
+ )
68
+ end
69
+
70
+ # Checks if a translation file (*.lproj/Localizable.strings) has been modified on a release branch, otherwise reporting a warning.
71
+ #
72
+ # @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
73
+ #
74
+ # @return [void]
75
+ def check_modified_translations_on_release_branch(report_type: :warning)
76
+ common_release_checker.check_file_changed(
77
+ file_comparison: ->(path) { !base_strings_file?(path: path) && File.basename(path) == LOCALIZABLE_STRINGS_FILE },
78
+ message: MESSAGE_TRANSLATION_FILE_UPDATED,
79
+ on_release_branch: false,
80
+ report_type: report_type
81
+ )
82
+ end
83
+
84
+ # Checks if changes made to the release notes are also followed by changes in the App Store strings file.
85
+ #
86
+ # @return [void]
87
+ def check_release_notes_and_app_store_strings
88
+ common_release_checker.check_release_notes_and_store_strings(
89
+ release_notes_file: 'Resources/release_notes.txt',
90
+ po_file: 'Resources/AppStoreStrings.po'
91
+ )
92
+ end
93
+
94
+ private
95
+
96
+ # Checks if a given path corresponds to the base (English) strings file, en.lproj/Localizable.strings.
97
+ #
98
+ # @return [Boolean] true if path is the base strings file
99
+ def base_strings_file?(path:)
100
+ base_strings_path_components = Pathname.new(BASE_STRINGS_FILE).each_filename.to_a
101
+ path_components = Pathname.new(path).each_filename.to_a
102
+
103
+ base_strings_path_components == path_components.last(base_strings_path_components.length)
104
+ end
105
+ end
106
+ end