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 228 229 230
|
# frozen_string_literal: true
module Tooling
class FeatureFlagDangerfile
SEE_DOC = "See the [feature flag documentation](https://docs.gitlab.com/ee/development/feature_flags#feature-flag-definition-and-validation)."
FEATURE_FLAG_LABEL = "feature flag"
FEATURE_FLAG_EXISTS_LABEL = "#{FEATURE_FLAG_LABEL}::exists".freeze
FEATURE_FLAG_SKIPPED_LABEL = "#{FEATURE_FLAG_LABEL}::skipped".freeze
DEVOPS_LABELS_REQUIRING_FEATURE_FLAG_REVIEW = ["devops::verify"].freeze
SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT.freeze
```suggestion
group: "%<group>s"
```
#{SEE_DOC}
SUGGEST_COMMENT
FEATURE_FLAG_ENFORCEMENT_WARNING = <<~WARNING_MESSAGE.freeze
There were no new or modified feature flag YAML files detected in this MR.
If the changes here are already controlled under an existing feature flag, please add
the ~"#{FEATURE_FLAG_EXISTS_LABEL}". Otherwise, if you think the changes here don't need
to be under a feature flag, please add the label ~"#{FEATURE_FLAG_SKIPPED_LABEL}", and
add a short comment about why we skipped the feature flag.
For guidance on when to use a feature flag, please see the [documentation](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags).
WARNING_MESSAGE
def initialize(context:, added_files:, modified_files:, helper:)
@context = context
@added_files = added_files
@modified_files = modified_files
@helper = helper
end
def check_added_feature_flag_files
added_files.each do |feature_flag|
check_feature_flag_yaml(feature_flag)
end
end
def check_modified_feature_flag_files
modified_files.each do |feature_flag|
check_default_enabled(feature_flag)
end
end
def feature_flag_file_added?
added_files.any?
end
def feature_flag_file_touched?
touched_feature_flag_files.any?
end
def mr_has_backend_or_frontend_changes?
changes = helper.changes_by_category
changes.has_key?(:backend) || changes.has_key?(:frontend)
end
def stage_requires_feature_flag_review?
DEVOPS_LABELS_REQUIRING_FEATURE_FLAG_REVIEW.include?(helper.stage_label)
end
def mr_missing_feature_flag_status_label?
([FEATURE_FLAG_EXISTS_LABEL, FEATURE_FLAG_SKIPPED_LABEL] & helper.mr_labels).none?
end
private
attr_reader :context, :added_files, :modified_files, :helper
def check_feature_flag_yaml(feature_flag)
unless feature_flag.valid?
context.failure("#{helper.html_link(feature_flag.path)} isn't valid YAML! #{SEE_DOC}")
return
end
check_group(feature_flag)
check_feature_issue_url(feature_flag)
# Note: we don't check introduced_by_url as it's already done by danger/config_files/Dangerfile
check_rollout_issue_url(feature_flag)
check_milestone(feature_flag)
check_default_enabled(feature_flag)
end
def touched_feature_flag_files
added_files + modified_files
end
def check_group(feature_flag)
mr_group_label = helper.group_label
if feature_flag.missing_group?
message_for_feature_flag_missing_group!(feature_flag: feature_flag, mr_group_label: mr_group_label)
else
message_for_feature_flag_with_group!(feature_flag: feature_flag, mr_group_label: mr_group_label)
end
end
def message_for_feature_flag_missing_group!(feature_flag:, mr_group_label:)
if mr_group_label.nil?
context.failure("Please specify a valid `group` label in #{helper.html_link(feature_flag.path)}. #{SEE_DOC}")
return
end
add_message_on_line(
feature_flag: feature_flag,
needle: "group:",
note: format(SUGGEST_MR_COMMENT, group: mr_group_label),
fallback_note: %(Please add `group: "#{mr_group_label}"` in #{helper.html_link(feature_flag.path)}. #{SEE_DOC}),
message_method: :failure
)
end
def message_for_feature_flag_with_group!(feature_flag:, mr_group_label:)
return if feature_flag.group_match_mr_label?(mr_group_label)
if mr_group_label.nil?
helper.labels_to_add << feature_flag.group
else
note = <<~FAILURE_MESSAGE
`group` is set to ~"#{feature_flag.group}" in #{helper.html_link(feature_flag.path)},
which does not match ~"#{mr_group_label}" set on the MR!
FAILURE_MESSAGE
add_message_on_line(
feature_flag: feature_flag,
needle: "group:",
note: note,
message_method: :failure
)
end
end
def check_feature_issue_url(feature_flag)
return unless feature_flag.missing_feature_issue_url?
add_message_on_line(
feature_flag: feature_flag,
needle: "feature_issue_url:",
note: "Consider filling `feature_issue_url:`"
)
end
def add_message_on_line(feature_flag:, needle:, note:, fallback_note: note, message_method: :message)
mr_line = feature_flag.find_line_index(needle)
# rubocop:disable GitlabSecurity/PublicSend -- we allow calling context.message, context.warning & context.failure
if mr_line
context.public_send(message_method, note, file: feature_flag.path, line: mr_line.succ)
else
context.public_send(message_method, "#{feature_flag.path}: #{fallback_note}")
end
# rubocop:enable GitlabSecurity/PublicSend
end
def check_rollout_issue_url(feature_flag)
return unless ::Feature::Shared::TYPES.dig(feature_flag.name.to_sym, :rollout_issue)
return unless feature_flag.missing_rollout_issue_url?
missing_field_error(feature_flag: feature_flag, field: :rollout_issue_url)
end
def check_milestone(feature_flag)
return unless feature_flag.missing_milestone?
missing_field_error(feature_flag: feature_flag, field: :milestone)
end
def check_default_enabled(feature_flag)
return unless feature_flag.default_enabled?
if ::Feature::Shared.can_be_default_enabled?(feature_flag.type)
note = <<~SUGGEST_COMMENT
You're about to [release the feature with the feature flag](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md#optional-release-the-feature-with-the-feature-flag).
This process can only be done **after** the [global rollout on production](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md#global-rollout-on-production).
Please make sure in [the rollout issue](#{feature_flag.rollout_issue_url}) that the preliminary steps have already been done. Otherwise, changing the YAML definition might not have the desired effect.
SUGGEST_COMMENT
mr_line = feature_flag.find_line_index("default_enabled: true")
context.markdown(note, file: feature_flag.path, line: mr_line.succ) if mr_line
else
context.failure(
"[Feature flag with the `#{feature_flag.type}` type must not be enabled by default](https://docs.gitlab.com/ee/development/feature_flags/##{feature_flag.type}-type). " \
"Consider changing the feature flag type if it's ready to be enabled by default."
)
end
end
def missing_field_error(feature_flag:, field:)
note = <<~MISSING_FIELD_ERROR
[Feature flag with the `#{feature_flag.type}` type must have `:#{field}` set](https://docs.gitlab.com/ee/development/feature_flags/##{feature_flag.type}-type).
MISSING_FIELD_ERROR
mr_line = feature_flag.find_line_index("#{field}:")
if mr_line
context.message(note, file: feature_flag.path, line: mr_line.succ)
else
context.message(note)
end
end
end
end
feature_flag_dangerfile = Tooling::FeatureFlagDangerfile.new(
context: self,
added_files: feature_flag.feature_flag_files(danger_helper: helper, change_type: :added),
modified_files: feature_flag.feature_flag_files(danger_helper: helper, change_type: :modified),
helper: helper
)
feature_flag_dangerfile.check_added_feature_flag_files
feature_flag_dangerfile.check_modified_feature_flag_files
if helper.security_mr? && feature_flag_dangerfile.feature_flag_file_added?
failure("Feature flags are discouraged from security merge requests. Read the [security documentation](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/feature_flags.md) for details.")
end
if !helper.security_mr? && feature_flag_dangerfile.mr_has_backend_or_frontend_changes? && feature_flag_dangerfile.stage_requires_feature_flag_review?
if feature_flag_dangerfile.feature_flag_file_touched? && !helper.mr_has_labels?(Tooling::FeatureFlagDangerfile::FEATURE_FLAG_EXISTS_LABEL)
# Feature flag config file touched in this MR, so let's add the label to avoid the warning.
helper.labels_to_add << Tooling::FeatureFlagDangerfile::FEATURE_FLAG_EXISTS_LABEL
end
if feature_flag_dangerfile.mr_missing_feature_flag_status_label?
warn(Tooling::FeatureFlagDangerfile::FEATURE_FLAG_ENFORCEMENT_WARNING)
end
end
|