Class: Danger::Changelog

Inherits:
Plugin
  • Object
show all
Defined in:
lib/danger/plugins/changelog.rb

Overview

Contains method to check the presense and validity of changelogs.

Defined Under Namespace

Classes: ChangelogCheckResult, CommitWrapper

Constant Summary collapse

NO_CHANGELOG_LABELS =
[
  "maintenance::refactor",
  "maintenance::pipelines",
  "maintenance::workflow",
  "ci-build",
  "meta",
].freeze
NO_CHANGELOG_CATEGORIES =
%i[docs none].freeze
CHANGELOG_TRAILER_REGEX =
/^(?<name>Changelog):\s*(?<category>.+)$/i.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"
IF_REVERT_MR_TEXT =
<<~MARKDOWN
  In a revert merge request? Use the revert merge request template to add labels [that skip changelog checks](https://docs.gitlab.com/ee/development/pipelines#revert-mrs).

  Reverting something in the current milestone? A changelog isn't required. Skip changelog checks by adding `~"regression:*"` label, then re-run the danger job (there is a link at the bottom of this comment).
MARKDOWN
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 this merge request needs a changelog entry, add the `Changelog` trailer to the commit message you want to add to the changelog.

    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
DEFAULT_CHANGELOG_CATEGORIES =
%w[
  added
  fixed
  changed
  deprecated
  removed
  security
  performance
  other
].freeze

Instance Method Summary collapse

Instance Method Details

#add_danger_messages(check_result) ⇒ Object

rubocop:disable Style/SignalException



162
163
164
165
166
167
# File 'lib/danger/plugins/changelog.rb', line 162

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

#categoriesObject



112
113
114
# File 'lib/danger/plugins/changelog.rb', line 112

def categories
  valid_changelog_commits.map(&:category)
end

#changelog_categories_checksObject



157
158
159
# File 'lib/danger/plugins/changelog.rb', line 157

def changelog_categories_checks
  check_changelog_commit_categories
end

#changelog_commitsObject



227
228
229
230
231
232
233
# File 'lib/danger/plugins/changelog.rb', line 227

def changelog_commits
  git.commits.each_with_object([]) do |commit, memo|
    trailer = commit.message.match(CHANGELOG_TRAILER_REGEX)

    memo << CommitWrapper.new(commit, trailer[:name], trailer[:category]) if trailer
  end
end

#check!Object



116
117
118
119
120
121
122
# File 'lib/danger/plugins/changelog.rb', line 116

def check!
  return if revert_in_current_milestone?

  critical_checks
  regular_checks
  changelog_categories_checks
end

#check_changelog_commit_categoriesObject

rubocop:enable Style/SignalException



171
172
173
174
175
# File 'lib/danger/plugins/changelog.rb', line 171

def check_changelog_commit_categories
  changelog_commits.each do |commit|
    add_danger_messages(check_changelog_trailer(commit))
  end
end

#check_changelog_pathObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/danger/plugins/changelog.rb', line 187

def check_changelog_path
  check_result = ChangelogCheckResult.empty
  return check_result unless exist?

  ee_changes = helper.changed_files(%r{\Aee/})

  if ee_changes.any? && !ee_changelog? && !required?
    check_result.warning("This MR changes code in `ee/`, but its Changelog commit is missing the [`EE: true` trailer](https://docs.gitlab.com/ee/development/changelog.html#gitlab-enterprise-changes). Consider adding it to your Changelog commits.")
  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 `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 commit to not have the `EE: true` trailer. Consider removing the `EE: true` trailer from your commits.")
  end

  check_result
end

#check_changelog_trailer(commit) ⇒ Object



177
178
179
180
181
182
183
184
185
# File 'lib/danger/plugins/changelog.rb', line 177

def check_changelog_trailer(commit)
  unless commit.trailer_key == "Changelog"
    return ChangelogCheckResult.error("The changelog trailer for commit #{commit.sha} must be `Changelog` (starting with a capital C), not `#{commit.trailer_key}`")
  end

  return ChangelogCheckResult.empty if valid_categories.include?(commit.category)

  ChangelogCheckResult.error("Commit #{commit.sha} uses an invalid changelog category: #{commit.category}")
end

#critical_checksObject



136
137
138
139
140
141
142
143
144
145
# File 'lib/danger/plugins/changelog.rb', line 136

def critical_checks
  check_result = ChangelogCheckResult.empty

  check_result.warning(modified_text) if git.modified_files.include?("CHANGELOG.md")

  # Help the user to apply the correct labels to skip this danger check in case it's a revert MR
  check_result.warning(IF_REVERT_MR_TEXT) if helper.revert_mr? && !helper.stable_branch?

  add_danger_messages(check_result)
end

#ee_changelog?Boolean

Returns:

  • (Boolean)


241
242
243
244
245
# File 'lib/danger/plugins/changelog.rb', line 241

def ee_changelog?
  changelog_commits.any? do |commit|
    commit.message.match?(CHANGELOG_EE_TRAILER_REGEX)
  end
end

#exist?Boolean

Returns:

  • (Boolean)


223
224
225
# File 'lib/danger/plugins/changelog.rb', line 223

def exist?
  valid_changelog_commits.any?
end

#modified_textObject



247
248
249
250
# File 'lib/danger/plugins/changelog.rb', line 247

def modified_text
  CHANGELOG_MODIFIED_URL_TEXT +
    (helper.ci? ? format(OPTIONAL_CHANGELOG_MESSAGE[:ci]) : OPTIONAL_CHANGELOG_MESSAGE[:local])
end

#optional?Boolean

Returns:

  • (Boolean)


219
220
221
# File 'lib/danger/plugins/changelog.rb', line 219

def optional?
  categories_need_changelog? && mr_without_no_changelog_label?
end

#optional_textObject



260
261
262
263
# File 'lib/danger/plugins/changelog.rb', line 260

def optional_text
  CHANGELOG_MISSING_URL_TEXT +
    (helper.ci? ? format(OPTIONAL_CHANGELOG_MESSAGE[:ci]) : OPTIONAL_CHANGELOG_MESSAGE[:local])
end

#regular_checksObject



147
148
149
150
151
152
153
154
155
# File 'lib/danger/plugins/changelog.rb', line 147

def regular_checks
  if exist?
    add_danger_messages(check_changelog_path)
  elsif required?
    required_texts.each { |_, text| fail(text) } # rubocop:disable Lint/UnreachableLoop, Style/SignalException
  elsif optional?
    message optional_text
  end
end

#required?Boolean

Returns:

  • (Boolean)


215
216
217
# File 'lib/danger/plugins/changelog.rb', line 215

def required?
  required_reasons.any?
end

#required_reasonsObject



208
209
210
211
212
213
# File 'lib/danger/plugins/changelog.rb', line 208

def required_reasons
  [].tap do |reasons|
    reasons << :db_changes if helper.changes.added.has_category?(:migration)
    reasons << :feature_flag_removed if helper.changes.deleted.has_category?(:feature_flag)
  end
end

#required_textsObject



252
253
254
255
256
257
258
# File 'lib/danger/plugins/changelog.rb', line 252

def required_texts
  required_reasons.each_with_object({}) do |required_reason, memo|
    memo[required_reason] =
      CHANGELOG_MISSING_URL_TEXT +
      format(REQUIRED_CHANGELOG_MESSAGE[helper.ci? ? :ci : :local], reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason))
  end
end

#revert_in_current_milestone?Boolean

Returns:

  • (Boolean)


124
125
126
127
128
129
130
131
132
133
134
# File 'lib/danger/plugins/changelog.rb', line 124

def revert_in_current_milestone?
  return false unless helper.revert_mr?
  # In dry-run mode, without the API token, we are able to fetch the current milestone nor the labels.
  # We simply assume that we are reverting in the current milestone.
  return true unless helper.ci?
  return false unless helper.current_milestone

  current_regression_label = "regression:#{helper.current_milestone.title}"

  helper.mr_labels.any?(current_regression_label)
end

#valid_changelog_commitsObject



235
236
237
238
239
# File 'lib/danger/plugins/changelog.rb', line 235

def valid_changelog_commits
  changelog_commits.select do |commit|
    valid_categories.include?(commit.message.match(CHANGELOG_TRAILER_REGEX)[:category])
  end
end