diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-12-17 09:56:50 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-12-17 11:01:09 +0300 |
commit | 0fa3cb8bdb7b17a6986494ad633c1b5ba2e5c6a7 (patch) | |
tree | 5baf37054ef693a10cd546b2cb796ce43261def3 /danger/rules | |
parent | 61471fff6a262bc2c9c5270016f8a677e48638bb (diff) |
danger: Move rules into subdirectory
It's best practice for Danger rules to live in a separate "rules/"
directory. Let's move them there, which also allows us to use globs to
import all rules.
Diffstat (limited to 'danger/rules')
-rw-r--r-- | danger/rules/changelog/Dangerfile | 91 | ||||
-rw-r--r-- | danger/rules/labels/Dangerfile | 48 | ||||
-rw-r--r-- | danger/rules/merge_request/Dangerfile | 13 | ||||
-rw-r--r-- | danger/rules/milestones/Dangerfile | 23 | ||||
-rw-r--r-- | danger/rules/roulette/Dangerfile | 74 |
5 files changed, 249 insertions, 0 deletions
diff --git a/danger/rules/changelog/Dangerfile b/danger/rules/changelog/Dangerfile new file mode 100644 index 000000000..e1079b78a --- /dev/null +++ b/danger/rules/changelog/Dangerfile @@ -0,0 +1,91 @@ +require 'yaml' + +def lint_commit(commit) + trailer = commit.message.match(/^(?<name>Changelog):\s*(?<category>.+)$/i) + + return :missing if trailer.nil? || trailer[:category].nil? + + name = trailer[:name] + + unless name == 'Changelog' + self.fail( + "The changelog trailer for commit #{commit.sha} must be `Changelog` (starting with a capital C), not `#{name}`" + ) + + return :invalid + end + + category = trailer[:category] + + return :valid if CATEGORIES.include?(category) + + self.fail( + "Commit #{commit.sha} uses an invalid changelog category: #{category}" + ) + + :invalid +end + +def presented_no_changelog_labels + NO_CHANGELOG_LABELS.map { |label| %(~"#{label}") }.join(', ') +end + +NO_CHANGELOG_LABELS = [ + 'documentation', + 'type::tooling', + 'tooling::pipelines', + 'tooling::workflow', + 'ci-build', + 'meta' +].freeze + +CATEGORIES = YAML + .load_file(File.expand_path('../../../.gitlab/changelog_config.yml', __dir__)) + .fetch('categories') + .keys + .freeze + +SEE_DOC = "See [the documentation](https://docs.gitlab.com/ee/development/changelog.html).".freeze + +CHANGELOG_MISSING = <<~MSG.freeze +**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html).** + +To ceate a changelog, annotate one or more commits with the `Changelog` Git +trailer. If you want to annotate the latest commit, you can do so using `git +commit --amend`. If you want to annotate older or multiple commits, you need to +do so using `git rebase -i`. + +When adding the trailer, you can use the following values: + +- #{CATEGORIES.join("\n- ")} + +For example: + +``` +This is the subject of your commit. + +This would be the body of your commit containing some extra details. + +Changelog: added +``` + +If your merge request doesn't warrant a CHANGELOG entry, consider adding any of +the #{presented_no_changelog_labels} labels. + +#{SEE_DOC} +MSG + +changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? + +if changelog_needed + checked = 0 + + git.commits.each do |commit| + case lint_commit(commit) + when :valid, :invalid + checked += 1 + end + end + + warn(CHANGELOG_MISSING) if checked.zero? +end diff --git a/danger/rules/labels/Dangerfile b/danger/rules/labels/Dangerfile new file mode 100644 index 000000000..6bfee7178 --- /dev/null +++ b/danger/rules/labels/Dangerfile @@ -0,0 +1,48 @@ +def changelog_entry + @changelog_entry ||= + begin + file = git.added_files.find { |path| path =~ %r{\Achangelogs/unreleased/} } + + YAML.safe_load(File.read(file)) + rescue + # The change log danger file will handle this + {} + end +end + +required_labels = %w[devops::create] +required_labels << "group::gitaly" if GITALY_TEAM.include?(gitlab.mr_author) + +TYPE_TO_LABEL = { + 'added' => %w[type::feature feature::addition], + 'fixed' => ['type::bug'], + 'changed' => ['type::maintenance'], + 'deprecated' => ['type::maintenance'], + 'security' => ['security'], + 'removed' => ['type::maintenance'], + 'performance' => %w[type::maintenance performance], + 'other' => ['type::tooling'], + nil => [] +} + +INHERITABLE_LABELS = TYPE_TO_LABEL.values.flatten + %w[Deliverable] + +def inherited_labels + gitlab.api + .merge_request_closes_issues( gitlab.mr_json['project_id'], gitlab.mr_json['iid']) + .flat_map { |i| i.labels } + .compact + .uniq + .select { |label| INHERITABLE_LABELS.include?(label) } +end + +required_labels.concat(TYPE_TO_LABEL[changelog_entry["type"]]) + +mr_labels = gitlab.mr_labels | required_labels | inherited_labels +gitlab.api.update_merge_request( + gitlab.mr_json['project_id'], + gitlab.mr_json['iid'], + labels: mr_labels.join(",") +) + +# vim: ft=ruby diff --git a/danger/rules/merge_request/Dangerfile b/danger/rules/merge_request/Dangerfile new file mode 100644 index 000000000..b9c81d2af --- /dev/null +++ b/danger/rules/merge_request/Dangerfile @@ -0,0 +1,13 @@ +unless /^([[:alnum:]][[:lower:]]+: )?[[:upper:]]/ =~ gitlab.mr_title + warn("Please capitalize the merge request title") +end + +if gitlab.mr_body.empty? + fail("Please provide a merge request description") +end + +if gitlab.mr_title.length > 72 + warn "The title of this merge requests it too long" +end + +# vim: ft=ruby diff --git a/danger/rules/milestones/Dangerfile b/danger/rules/milestones/Dangerfile new file mode 100644 index 000000000..ce74bbb87 --- /dev/null +++ b/danger/rules/milestones/Dangerfile @@ -0,0 +1,23 @@ +unless gitlab.mr_json["milestone"] + issues = gitlab.api.merge_request_closes_issues(gitlab.mr_json['project_id'], gitlab.mr_json['iid']) + + milestone_id = issues + .map { |i| i.milestone } + .compact + .reject { |m| m.state == "closed" || m.due_date.nil? } + .sort_by { |m| m.due_date } + .first + &.id + + if milestone_id + gitlab.api.update_merge_request( + gitlab.mr_json['project_id'], + gitlab.mr_json['iid'], + milestone_id: milestone_id + ) + else + warn "No milestone was set, nor could it be detected from the issues this merge request closes." + end +end + +# vim: ft=ruby diff --git a/danger/rules/roulette/Dangerfile b/danger/rules/roulette/Dangerfile new file mode 100644 index 000000000..80ee666df --- /dev/null +++ b/danger/rules/roulette/Dangerfile @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +MESSAGE = <<MARKDOWN +## Reviewer roulette + +Changes that require review have been detected! A merge request is normally +reviewed by both a reviewer and a maintainer in its primary category and by a +maintainer in all other categories. +MARKDOWN + +CATEGORY_TABLE_HEADER = <<MARKDOWN + +To spread load more evenly across eligible reviewers, Danger has picked a candidate for each +review slot. Feel free to +[override these selections](https://about.gitlab.com/handbook/engineering/projects/#gitaly) +if you think someone else would be better-suited +or use the [GitLab Review Workload Dashboard](https://gitlab-org.gitlab.io/gitlab-roulette/) to find other available reviewers. + +To read more on how to use the reviewer roulette, please take a look at the +[Engineering workflow](https://about.gitlab.com/handbook/engineering/workflow/#basics) +and [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html). +Please consider assigning a reviewer or maintainer who is a +[domain expert](https://about.gitlab.com/handbook/engineering/projects/#gitaly) in the area of the merge request. + +Once you've decided who will review this merge request, mention them as you +normally would! Danger does not automatically notify them for you. + +| Category | Reviewer | Maintainer | +| -------- | -------- | ---------- | +MARKDOWN + +OPTIONAL_REVIEW_TEMPLATE = '%{role} review is optional for %{category}' +NOT_AVAILABLE_TEMPLATE = 'No %{role} available' + +def note_for_spins_role(spins, role) + spins.each do |spin| + note = note_for_spin_role(spin, role) + + return note if note + end + + NOT_AVAILABLE_TEMPLATE % { role: role } +end + +def note_for_spin_role(spin, role) + if spin.optional_role == role + return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) } + end + + spin.public_send(role)&.markdown_name(author: roulette.team_mr_author) # rubocop:disable GitlabSecurity/PublicSend +end + +def markdown_row_for_spins(category, spins_array) + reviewer_note = note_for_spins_role(spins_array, :reviewer) + maintainer_note = note_for_spins_role(spins_array, :maintainer) + + "| #{helper.label_for_category(category)} | #{reviewer_note} | #{maintainer_note} |" +end + +changes = project_helper.changes_by_category + +if changes.any? + categories = changes.keys + project = project_helper.project_name + + random_roulette_spins = roulette.spin(project, [nil], timezone_experiment: false) + + rows = random_roulette_spins.map do |spin| + markdown_row_for_spins(spin.category, [spin]) + end + + markdown(MESSAGE) + markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? +end |