From 20083ff6bb0b57b42e77f32fe933dd30626bfee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 4 Feb 2021 18:04:48 +0100 Subject: [PATCH] danger: Make sure a feature flag removal comes with a changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This would address the use-case where a feature flag is removed with no changelog, creating potential confusion for users. Signed-off-by: Rémy Coutable <remy@rymai.me> --- danger/changelog/Dangerfile | 8 +- doc/development/changelog.md | 3 +- spec/tooling/danger/changelog_spec.rb | 120 ++++++++++++++++++++++---- tooling/danger/changelog.rb | 27 ++++-- 4 files changed, 130 insertions(+), 28 deletions(-) diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index c8474157fa56a..32c0277480cb7 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -46,7 +46,7 @@ def check_changelog_path(path) ee_changes = helper.all_ee_changes.dup ee_changes.delete(path) - if ee_changes.any? && !changelog.ee_changelog? && !changelog.db_changes? + if ee_changes.any? && !changelog.ee_changelog? && !changelog.required? warn "This MR has a Changelog file outside `ee/`, but code changes in `ee/`. Consider moving the Changelog file into `ee/`." end @@ -54,7 +54,7 @@ def check_changelog_path(path) warn "This MR has a Changelog file in `ee/`, but no code changes in `ee/`. Consider moving the Changelog file outside `ee/`." end - if ee_changes.any? && changelog.ee_changelog? && changelog.db_changes? + if ee_changes.any? && changelog.ee_changelog? && changelog.required_reasons(feature_flag_helper: feature_flag).include?(:db_changes) warn "This MR has a Changelog file inside `ee/`, but there are database changes which [requires](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry) the Changelog placement to be outside of `ee/`. Consider moving the Changelog file outside `ee/`." end end @@ -68,8 +68,8 @@ changelog_found = changelog.found if changelog_found check_changelog_yaml(changelog_found) check_changelog_path(changelog_found) -elsif changelog.required? - fail changelog.required_text +elsif changelog.required?(feature_flag_helper: feature_flag) + changelog.required_texts(feature_flag_helper: feature_flag).each { |_, text| fail(text) } elsif changelog.optional? message changelog.optional_text end diff --git a/doc/development/changelog.md b/doc/development/changelog.md index f2c8aa4db62fa..44be6e9548b11 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -57,8 +57,7 @@ the `author` field. GitLab team members **should not**. - Any change behind an enabled feature flag **should** have a changelog entry. - Any change that adds new usage data metrics and changes that needs to be documented in Product Intelligence [Event Dictionary](https://about.gitlab.com/handbook/product/product-intelligence-guide/#event-dictionary) **should** have a changelog entry. - A change that adds snowplow events **should** have a changelog entry - -- A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry - - only if the feature flag did not default to true already. +- A change that [removes a feature flag](feature_flags/development.md) **must** have a changelog entry. - A fix for a regression introduced and then fixed in the same release (i.e., fixing a bug introduced during a monthly release candidate) **should not** have a changelog entry. diff --git a/spec/tooling/danger/changelog_spec.rb b/spec/tooling/danger/changelog_spec.rb index c0eca67ce92da..4b7ef9cf79e88 100644 --- a/spec/tooling/danger/changelog_spec.rb +++ b/spec/tooling/danger/changelog_spec.rb @@ -7,7 +7,8 @@ RSpec.describe Tooling::Danger::Changelog do include DangerSpecHelper - let(:added_files) { nil } + let(:added_files) { [] } + let(:removed_feature_flag_files) { [] } let(:fake_git) { double('fake-git', added_files: added_files) } let(:mr_labels) { nil } @@ -19,10 +20,62 @@ let(:ee?) { false } let(:fake_helper) { double('fake-helper', changes_by_category: changes_by_category, sanitize_mr_title: sanitize_mr_title, ee?: ee?) } + let(:fake_feature_flag) { double('feature-flag', feature_flag_files: removed_feature_flag_files) } let(:fake_danger) { new_fake_danger.include(described_class) } subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) } + describe '#required_reasons' do + subject { changelog.required_reasons } + + [ + 'db/migrate/20200000000000_new_migration.rb', + 'db/post_migrate/20200000000000_new_migration.rb' + ].each do |file_path| + context "added files contain a migration (#{file_path})" do + let(:added_files) { [file_path] } + + it { is_expected.to include(:db_changes) } + end + end + + [ + 'config/feature_flags/foo.yml', + 'ee/config/feature_flags/foo.yml' + ].each do |file_path| + context "removed files contains a feature flag (#{file_path})" do + let(:removed_feature_flag_files) { [file_path] } + + context 'when no feature_flag_helper is given' do + it { is_expected.to be_empty } + end + + context 'when a feature_flag_helper is given' do + subject { changelog.required_reasons(feature_flag_helper: fake_feature_flag) } + + it { is_expected.to include(:feature_flag_removed) } + end + end + end + + [ + 'app/models/model.rb', + 'app/assets/javascripts/file.js' + ].each do |file_path| + context "added files do not contain a migration (#{file_path})" do + let(:added_files) { [file_path] } + + it { is_expected.to be_empty } + end + end + + context "removed files do not contain a feature flag" do + let(:removed_feature_flag_files) { [] } + + it { is_expected.to be_empty } + end + end + describe '#required?' do subject { changelog.required? } @@ -37,6 +90,25 @@ end end + [ + 'config/feature_flags/foo.yml', + 'ee/config/feature_flags/foo.yml' + ].each do |file_path| + context "removed files contains a feature flag (#{file_path})" do + let(:removed_feature_flag_files) { [file_path] } + + context 'when no feature_flag_helper is given' do + it { is_expected.to be_falsey } + end + + context 'when a feature_flag_helper is given' do + subject { changelog.required?(feature_flag_helper: fake_feature_flag) } + + it { is_expected.to be_truthy } + end + end + end + context 'added files do not contain a migration' do [ 'app/models/model.rb', @@ -174,28 +246,46 @@ end end - describe '#required_text' do + describe '#required_texts' do + let(:sanitize_mr_title) { 'Fake Title' } let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } - subject { changelog.required_text } - - context "when title is not changed from sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'Fake Title' } + subject { changelog.required_texts } + shared_examples 'changelog required text' do |key| specify do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).not_to include('--ee') + expect(subject).to have_key(key) + expect(subject[key]).to include('CHANGELOG missing') + expect(subject[key]).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject[key]).not_to include('--ee') end end - context "when title needs sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'DRAFT: Fake Title' } + context 'with a new migration file' do + let(:added_files) { ['db/migrate/20200000000000_new_migration.rb'] } - specify do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).not_to include('--ee') + context "when title is not changed from sanitization", :aggregate_failures do + it_behaves_like 'changelog required text', :db_changes + end + + context "when title needs sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'DRAFT: Fake Title' } + + it_behaves_like 'changelog required text', :db_changes + end + end + + context 'with a removed feature flag file' do + let(:removed_feature_flag_files) { ['config/feature_flags/foo.yml'] } + + context 'when no feature_flag_helper is given' do + it { is_expected.to be_empty } + end + + context 'when a feature_flag_helper is given' do + subject { changelog.required_texts(feature_flag_helper: fake_feature_flag) } + + it_behaves_like 'changelog required text', :feature_flag_removed end end end diff --git a/tooling/danger/changelog.rb b/tooling/danger/changelog.rb index f7f505f51a639..30bdfd9f9ae1d 100644 --- a/tooling/danger/changelog.rb +++ b/tooling/danger/changelog.rb @@ -30,18 +30,28 @@ module 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 + REQUIRED_CHANGELOG_REASONS = { + db_changes: 'introduces a database migration', + feature_flag_removed: 'removes a feature flag' + }.freeze REQUIRED_CHANGELOG_MESSAGE = <<~MSG To create a changelog entry, run the following: #{CREATE_CHANGELOG_COMMAND} - This merge request requires a changelog entry because it [introduces a database migration](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry). + 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 - def required? - git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} } + def required_reasons(feature_flag_helper: nil) + [].tap do |reasons| + reasons << :db_changes if git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} } + reasons << :feature_flag_removed if feature_flag_helper&.respond_to?(:feature_flag_files) && feature_flag_helper.feature_flag_files(change_type: :deleted).any? + end + end + + def required?(feature_flag_helper: nil) + required_reasons(feature_flag_helper: feature_flag_helper).any? end - alias_method :db_changes?, :required? def optional? categories_need_changelog? && without_no_changelog_label? @@ -60,9 +70,12 @@ def modified_text format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) end - def required_text - CHANGELOG_MISSING_URL_TEXT + - format(REQUIRED_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + def required_texts(feature_flag_helper: nil) + required_reasons(feature_flag_helper: feature_flag_helper).each_with_object({}) do |required_reason, memo| + memo[required_reason] = + CHANGELOG_MISSING_URL_TEXT + + format(REQUIRED_CHANGELOG_MESSAGE, reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason), mr_iid: mr_iid, mr_title: sanitized_mr_title) + end end def optional_text -- GitLab