danger-dangermattic 1.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 +7 -0
- data/.buildkite/gem-push.sh +15 -0
- data/.buildkite/pipeline.yml +69 -0
- data/.bundle/config +2 -0
- data/.github/workflows/reusable-check-labels-on-issues.yml +91 -0
- data/.github/workflows/reusable-run-danger.yml +54 -0
- data/.gitignore +30 -0
- data/.rubocop.yml +67 -0
- data/.ruby-version +1 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +191 -0
- data/Guardfile +21 -0
- data/LICENSE +373 -0
- data/README.md +68 -0
- data/Rakefile +24 -0
- data/danger-dangermattic.gemspec +58 -0
- data/lib/danger_dangermattic.rb +3 -0
- data/lib/danger_plugin.rb +4 -0
- data/lib/dangermattic/gem_version.rb +5 -0
- data/lib/dangermattic/plugins/android_release_checker.rb +50 -0
- data/lib/dangermattic/plugins/android_strings_checker.rb +31 -0
- data/lib/dangermattic/plugins/android_unit_test_checker.rb +187 -0
- data/lib/dangermattic/plugins/common/common_release_checker.rb +113 -0
- data/lib/dangermattic/plugins/common/git_utils.rb +166 -0
- data/lib/dangermattic/plugins/common/github_utils.rb +68 -0
- data/lib/dangermattic/plugins/common/reporter.rb +38 -0
- data/lib/dangermattic/plugins/ios_release_checker.rb +106 -0
- data/lib/dangermattic/plugins/labels_checker.rb +74 -0
- data/lib/dangermattic/plugins/manifest_pr_checker.rb +106 -0
- data/lib/dangermattic/plugins/milestone_checker.rb +98 -0
- data/lib/dangermattic/plugins/podfile_checker.rb +122 -0
- data/lib/dangermattic/plugins/pr_size_checker.rb +125 -0
- data/lib/dangermattic/plugins/tracks_checker.rb +72 -0
- data/lib/dangermattic/plugins/view_changes_checker.rb +46 -0
- data/spec/android_release_checker_spec.rb +93 -0
- data/spec/android_strings_checker_spec.rb +185 -0
- data/spec/android_unit_test_checker_spec.rb +343 -0
- data/spec/common_release_checker_spec.rb +70 -0
- data/spec/fixtures/android_unit_test_checker/Abc.java +7 -0
- data/spec/fixtures/android_unit_test_checker/AbcFeatureConfig.java +7 -0
- data/spec/fixtures/android_unit_test_checker/Abcdef.kt +5 -0
- data/spec/fixtures/android_unit_test_checker/AbcdefgViewHelper.java +7 -0
- data/spec/fixtures/android_unit_test_checker/AnotherViewHelper.kt +7 -0
- data/spec/fixtures/android_unit_test_checker/MyNewClass.java +7 -0
- data/spec/fixtures/android_unit_test_checker/Polygon.kt +3 -0
- data/spec/fixtures/android_unit_test_checker/Shape.kt +10 -0
- data/spec/fixtures/android_unit_test_checker/TestsINeedThem.java +5 -0
- data/spec/fixtures/android_unit_test_checker/TestsINeedThem.kt +7 -0
- data/spec/fixtures/android_unit_test_checker/TestsINeedThem2.kt +12 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/activities/MyActivity.kt +7 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/activities/MyJavaActivity.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/fragments/MyFragment.kt +6 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/fragments/MyNewJavaFragment.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/module/MyModule.java +13 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/view/ActionCardViewHolder.kt +22 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/view/MyRecyclerView.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/AbcTests.java +5 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/AnotherTestClass.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/PolygonTest.kt +4 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/TestMyNewClass.java +9 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/ToolTest.kt +5 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/org/wordpress/android/widgets/NestedWebView.kt +32 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/org/wordpress/util/config/BloggingPromptsFeatureConfig.kt +23 -0
- data/spec/fixtures/android_unit_test_checker/src/test/java/org/test/TestsINeedThem.java +9 -0
- data/spec/github_utils_spec.rb +110 -0
- data/spec/ios_release_checker_spec.rb +191 -0
- data/spec/labels_checker_spec.rb +169 -0
- data/spec/manifest_pr_checker_spec.rb +140 -0
- data/spec/milestone_checker_spec.rb +222 -0
- data/spec/podfile_checker_spec.rb +595 -0
- data/spec/pr_size_checker_spec.rb +250 -0
- data/spec/spec_helper.rb +115 -0
- data/spec/tracks_checker_spec.rb +156 -0
- data/spec/view_changes_checker_spec.rb +168 -0
- 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
|