1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
|
# frozen_string_literal: true
require 'gitlab/dangerfiles/title_linting'
module Tooling
module Danger
module Changelog
NO_CHANGELOG_LABELS = [
'tooling',
'tooling::pipelines',
'tooling::workflow',
'ci-build',
'meta'
].freeze
NO_CHANGELOG_CATEGORIES = %i[docs none].freeze
CHANGELOG_TRAILER_REGEX = /^Changelog:\s*(?<category>.+)$/.freeze
CHANGELOG_EE_TRAILER_REGEX = /^EE: true$/.freeze
CHANGELOG_MODIFIED_URL_TEXT = "**CHANGELOG.md was edited.** Please remove the additions and follow the [changelog guidelines](https://docs.gitlab.com/ee/development/changelog.html).\n\n"
CHANGELOG_MISSING_URL_TEXT = "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**:\n\n"
OPTIONAL_CHANGELOG_MESSAGE = {
local: "If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.",
ci: <<~MSG
If you want to create a changelog entry for GitLab FOSS, add the `Changelog` trailer to the commit message you want to add to the changelog.
If you want to create a changelog entry for GitLab EE, also add the trailer `EE: true` to your commit message.
If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
MSG
}.freeze
SEE_DOC = "See the [changelog documentation](https://docs.gitlab.com/ee/development/changelog.html)."
REQUIRED_CHANGELOG_REASONS = {
db_changes: 'introduces a database migration',
feature_flag_removed: 'removes a feature flag'
}.freeze
REQUIRED_CHANGELOG_MESSAGE = {
local: "This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).",
ci: <<~MSG
To create a changelog entry, add the `Changelog` trailer to one of your Git commit messages.
This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).
MSG
}.freeze
CATEGORIES = YAML
.load_file(File.expand_path('../../.gitlab/changelog_config.yml', __dir__))
.fetch('categories')
.keys
.freeze
class ChangelogCheckResult
attr_reader :errors, :warnings, :markdowns, :messages
def initialize(errors: [], warnings: [], markdowns: [], messages: [])
@errors = errors
@warnings = warnings
@markdowns = markdowns
@messages = messages
end
private_class_method :new
def self.empty
new
end
def self.error(error)
new(errors: [error])
end
def self.warning(warning)
new(warnings: [warning])
end
def error(error)
errors << error
end
def warning(warning)
warnings << warning
end
def markdown(markdown)
markdowns << markdown
end
def message(message)
messages << message
end
end
# rubocop:disable Style/SignalException
def check!
if git.modified_files.include?("CHANGELOG.md")
fail modified_text
end
if present?
add_danger_messages(check_changelog_path)
elsif required?
required_texts.each { |_, text| fail(text) } # rubocop:disable Lint/UnreachableLoop
elsif optional?
message optional_text
end
check_changelog_commit_categories
end
# rubocop:enable Style/SignalException
# rubocop:disable Style/SignalException
def add_danger_messages(check_result)
check_result.errors.each { |error| fail(error) } # rubocop:disable Lint/UnreachableLoop
check_result.warnings.each { |warning| warn(warning) }
check_result.markdowns.each { |markdown_hash| markdown(**markdown_hash) }
check_result.messages.each { |text| message(text) }
end
# rubocop:enable Style/SignalException
def check_changelog_commit_categories
changelog_commits.each do |commit|
add_danger_messages(check_changelog_trailer(commit))
end
end
def check_changelog_trailer(commit)
trailer = commit.message.match(CHANGELOG_TRAILER_REGEX)
category = trailer[:category]
return ChangelogCheckResult.empty if CATEGORIES.include?(category)
ChangelogCheckResult.error("Commit #{commit.sha} uses an invalid changelog category: #{category}")
end
def check_changelog_path
check_result = ChangelogCheckResult.empty
return check_result unless present?
ee_changes = project_helper.all_ee_changes.dup
if ee_changes.any? && !ee_changelog? && !required?
check_result.warning("This MR changes code in `ee/`, but is missing a Changelog commit. Consider adding the Changelog trailer to at least one commit.")
end
if ee_changes.empty? && ee_changelog?
check_result.warning("This MR has a Changelog commit for EE, but no code changes in `ee/`. Consider removing the use of the `EE: true` trailer from your commits.")
end
if ee_changes.any? && ee_changelog? && required_reasons.include?(:db_changes)
check_result.warning("This MR has a Changelog commit with the `EE: true` trailer, but there are database changes which [requires](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry) the Changelog commiot to not have the `EE: true` trailer. Consider removing the `EE: true` trailer.")
end
check_result
end
def required_reasons
[].tap do |reasons|
reasons << :db_changes if project_helper.changes.added.has_category?(:migration)
reasons << :feature_flag_removed if project_helper.changes.deleted.has_category?(:feature_flag)
end
end
def required?
required_reasons.any?
end
def optional?
categories_need_changelog? && mr_without_no_changelog_label?
end
def present?
valid_changelog_commits.any?
end
def changelog_commits
git.commits.select do |commit|
commit.message.match?(CHANGELOG_TRAILER_REGEX)
end
end
def valid_changelog_commits
changelog_commits.select do |commit|
trailer = commit.message.match(CHANGELOG_TRAILER_REGEX)
CATEGORIES.include?(trailer[:category])
end
end
def ee_changelog?
changelog_commits.any? do |commit|
commit.message.match?(CHANGELOG_EE_TRAILER_REGEX)
end
end
def modified_text
CHANGELOG_MODIFIED_URL_TEXT +
(helper.ci? ? format(OPTIONAL_CHANGELOG_MESSAGE[:ci]) : OPTIONAL_CHANGELOG_MESSAGE[:local])
end
def required_texts
required_reasons.each_with_object({}) do |required_reason, memo|
memo[required_reason] =
CHANGELOG_MISSING_URL_TEXT +
(helper.ci? ? format(REQUIRED_CHANGELOG_MESSAGE[:ci], reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason)) : REQUIRED_CHANGELOG_MESSAGE[:local])
end
end
def optional_text
CHANGELOG_MISSING_URL_TEXT +
(helper.ci? ? format(OPTIONAL_CHANGELOG_MESSAGE[:ci]) : OPTIONAL_CHANGELOG_MESSAGE[:local])
end
private
def read_file(path)
File.read(path)
end
def categories_need_changelog?
(project_helper.changes.categories - NO_CHANGELOG_CATEGORIES).any?
end
def mr_without_no_changelog_label?
(helper.mr_labels & NO_CHANGELOG_LABELS).empty?
end
end
end
end
|