Skip to content
代码片段 群组 项目
delete_old_feature_flags.rb 7.0 KB
更新 更旧
# frozen_string_literal: true

require 'fileutils'
require 'cgi'

require_relative '../config/environment'
require_relative 'helpers/groups'
require_relative 'helpers/milestones'
require_relative 'helpers/git_diff_parser'
  # This is an implementation of a ::Gitlab::Housekeeper::Keep. This keep will locate any feature flag definition file
  # that were added at least `<CUTOFF_MILESTONE_OLD> milestones` ago and remove the definition file.
  #
  # You can run it individually with:
  #
  # ```
  # bundle exec gitlab-housekeeper -d \
  #   -k Keeps::DeleteOldFeatureFlags
  # ```
  class DeleteOldFeatureFlags < ::Gitlab::Housekeeper::Keep
    CUTOFF_MILESTONE_OLD = 12
    GREP_IGNORE = [
      'locale/',
      'db/structure.sql'
    ].freeze
    ROLLOUT_ISSUE_URL_REGEX = %r{\Ahttps://gitlab\.com/(?<project_path>.*)/-/issues/(?<issue_iid>\d+)\z}
    API_ISSUE_URL = "https://gitlab.com/api/v4/projects/%<project_path>s/issues/%<issue_iid>s"
    FEATURE_FLAG_LOG_ISSUES_URL = "https://gitlab.com/gitlab-com/gl-infra/feature-flag-log/-/issues/?search=%<feature_flag_name>s&sort=created_date&state=all&label_name%%5B%%5D=host%%3A%%3Agitlab.com"
      each_feature_flag do |feature_flag|
        change = prepare_change(feature_flag)
    def prepare_change(feature_flag)
      if feature_flag.milestone.nil?
        @logger.puts "#{feature_flag.name} has no milestone set!"
      return unless milestones_helper.before_cuttoff?(
        milestone: feature_flag.milestone,
        milestones_ago: CUTOFF_MILESTONE_OLD)
      change = ::Gitlab::Housekeeper::Change.new
      change.changelog_type = 'removed'
      change.title = "Delete the `#{feature_flag.name}` feature flag"
      change.identifiers = [self.class.name.demodulize, feature_flag.name]
      FileUtils.rm(feature_flag.path)

      change.changed_files = [feature_flag.path]

      apply_patch_and_cleanup(feature_flag, change)

      # rubocop:disable Gitlab/DocUrl -- Not running inside rails application
      change.description = <<~MARKDOWN
      This feature flag was introduced in #{feature_flag.milestone}, which is more than #{CUTOFF_MILESTONE_OLD} milestones ago.
      As part of our process we want to ensure [feature flags don't stay too long in the codebase](https://docs.gitlab.com/ee/development/feature_flags/#types-of-feature-flags).
      Rollout issue: #{feature_flag_rollout_issue_url(feature_flag)}
      #{feature_flag_default_enabled_note(feature_flag.default_enabled)}
      <details><summary>Remaining mentions of the feature flag (click to expand)</summary>
      ```
      #{feature_flag_grep(feature_flag.name)}
      ```
      It is possible that this MR will still need some changes to remove references to the feature flag in the code.
      At the moment the `gitlab-housekeeper` is not always capable of removing all references so you must check the diff and pipeline failures to confirm if there are any issues.
      It is the responsibility of ~"#{feature_flag.group}" to push those changes to this branch.
      If they are already removing this feature flag in another merge request then they can just close this merge request.
      You can also see the status of the rollout by checking #{feature_flag_rollout_issue_url(feature_flag)} and #{format(FEATURE_FLAG_LOG_ISSUES_URL, feature_flag_name: feature_flag.name)}.
      MARKDOWN
      # rubocop:enable Gitlab/DocUrl
      change.labels = [
        'maintenance::removal',
        'feature flag',
        feature_flag.group
      ]
      change.reviewers = assignees(feature_flag.rollout_issue_url)
      if change.reviewers.empty?
        group_data = groups_helper.group_for_group_label(feature_flag.group)

        change.reviewers = groups_helper.pick_reviewer(group_data, change.identifiers) if group_data
    def feature_flag_default_enabled_note(feature_flag_default_enabled)
      if feature_flag_default_enabled
        <<~NOTE
        The feature flag is enabled by default. Unless it's disabled on GitLab.com, you should keep the feature-flag
        code branch, otherwise, keep the other branch.
        NOTE
      else
        <<~NOTE
        The feature flag isn't enabled by default. If it's enabled on GitLab.com, you should keep the feature-flag
        code branch, otherwise, keep the other branch.
        NOTE
      end
    end

    def feature_flag_grep(feature_flag_name)
      Gitlab::Housekeeper::Shell.execute(
        'git',
        'grep',
        '--heading',
        '--line-number',
        '--break',
        feature_flag_name,
        '--',
        *(GREP_IGNORE.map { |path| ":^#{path}" })
      )
    rescue ::Gitlab::Housekeeper::Shell::Error
      # git grep returns error status if nothing is found
    end

    def apply_patch_and_cleanup(feature_flag, change)
      return unless patch_exists?(feature_flag)

      change.changed_files << patch_path(feature_flag)
      change.changed_files += extract_changed_files_from_patch(feature_flag)

      apply_patch(feature_flag)
      FileUtils.rm(patch_path(feature_flag))
    end

    def patch_exists?(feature_flag)
      File.file?(patch_path(feature_flag))
    end

    def apply_patch(feature_flag)
      Gitlab::Housekeeper::Shell.execute('git', 'apply', patch_path(feature_flag))
    end

    def patch_path(feature_flag)
      feature_flag.path.sub(/.yml$/, '.patch')
    end

    def extract_changed_files_from_patch(feature_flag)
      git_diff_parser_helper.all_changed_files(File.read(patch_path(feature_flag)))
    def feature_flag_rollout_issue_url(feature_flag)
      feature_flag.rollout_issue_url || '(missing URL)'
    end

    def assignees(rollout_issue_url)
      rollout_issue = get_rollout_issue(rollout_issue_url)

      return unless rollout_issue && rollout_issue[:assignees]
    end

    def get_rollout_issue(rollout_issue_url)
      matches = ROLLOUT_ISSUE_URL_REGEX.match(rollout_issue_url)
      return unless matches

      response = Gitlab::HTTP_V2.try_get(
        format(API_ISSUE_URL, project_path: CGI.escape(matches[:project_path]), issue_iid: matches[:issue_iid])
      )

      unless (200..299).cover?(response.code)
        raise Error,
          "Failed with response code: #{response.code} and body:\n#{response.body}"
      end

      Gitlab::Json.parse(response.body, symbolize_names: true)
    end

    def each_feature_flag
      all_feature_flag_files.map do |f|
          Feature::Definition.new(f, YAML.safe_load_file(f, permitted_classes: [Symbol], symbolize_names: true))
      end
    end

    def all_feature_flag_files
      Dir.glob("{,ee/}config/feature_flags/{development,gitlab_com_derisk}/*.yml")
    end

    def groups_helper
      @groups_helper ||= ::Keeps::Helpers::Groups.new
    end

    def milestones_helper
      @milestones_helper ||= ::Keeps::Helpers::Milestones.new
    end

    def git_diff_parser_helper
      @git_diff_parser_helper ||= ::Keeps::Helpers::GitDiffParser.new
    end