File: Dangerfile

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (230 lines) | stat: -rw-r--r-- 9,146 bytes parent folder | download
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