summaryrefslogtreecommitdiff
path: root/tooling/danger/stable_branch.rb
blob: 83e8b0a7a9d0066f7681e63897b5f4f85e070aa8 (plain)
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
# frozen_string_literal: true

module Tooling
  module Danger
    module StableBranch
      VersionApiError = Class.new(StandardError)

      STABLE_BRANCH_REGEX = %r{\A(?<version>\d+-\d+)-stable-ee\z}.freeze

      # rubocop:disable Lint/MixedRegexpCaptureTypes
      VERSION_REGEX = %r{
        \A(?<major>\d+)
        \.(?<minor>\d+)
        (\.(?<patch>\d+))?
        (-(?<rc>rc(?<rc_number>\d*)))?
        (-\h+\.\h+)?
        (-ee|\.ee\.\d+)?\z
      }x.freeze
      # rubocop:enable Lint/MixedRegexpCaptureTypes

      MAINTENANCE_POLICY_URL = 'https://docs.gitlab.com/ee/policy/maintenance.html'

      MAINTENANCE_POLICY_MESSAGE = <<~MSG
      See the [release and maintenance policy](#{MAINTENANCE_POLICY_URL}) for more information.
      MSG

      FEATURE_ERROR_MESSAGE = <<~MSG
      This MR includes the `type::feature` label. Features do not qualify for patch releases. #{MAINTENANCE_POLICY_MESSAGE}
      MSG

      BUG_ERROR_MESSAGE = <<~MSG
      This branch is meant for backporting bug fixes. If this MR qualifies please add the `type::bug` label. #{MAINTENANCE_POLICY_MESSAGE}
      MSG

      VERSION_WARNING_MESSAGE = <<~MSG
      Backporting to older releases requires an [exception request process](https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases)
      MSG

      FAILED_VERSION_REQUEST_MESSAGE = <<~MSG
      There was a problem checking if this is a qualified version for backporting. Re-running this job may fix the problem.
      MSG

      # rubocop:disable Style/SignalException
      def check!
        return unless stable_target_branch && !helper.security_mr?

        fail FEATURE_ERROR_MESSAGE if has_feature_label?
        fail BUG_ERROR_MESSAGE unless has_bug_label?

        warn VERSION_WARNING_MESSAGE unless targeting_patchable_version?
      end
      # rubocop:enable Style/SignalException

      private

      def stable_target_branch
        helper.mr_target_branch.match(STABLE_BRANCH_REGEX)
      end

      def has_feature_label?
        helper.mr_has_labels?('type::feature')
      end

      def has_bug_label?
        helper.mr_has_labels?('type::bug')
      end

      def targeting_patchable_version?
        raise VersionApiError if last_three_minor_versions.empty?

        last_three_minor_versions.include?(targeted_version)
      rescue VersionApiError
        warn FAILED_VERSION_REQUEST_MESSAGE
        true
      end

      def last_three_minor_versions
        return [] unless versions

        current_version = versions.first.match(VERSION_REGEX)
        version_1 = previous_minor_version(current_version)
        version_2 = previous_minor_version(version_1)

        [
          version_to_minor_string(current_version),
          version_to_minor_string(version_1),
          version_to_minor_string(version_2)
        ]
      end

      def targeted_version
        stable_target_branch[1].tr('-', '.')
      end

      def versions(page = 1)
        version_api_endpoint = "https://version.gitlab.com/api/v1/versions?per_page=50&page=#{page}"
        response = HTTParty.get(version_api_endpoint) # rubocop:disable Gitlab/HTTParty

        raise VersionApiError unless response.success?

        version_list = response.parsed_response.map { |v| v['version'] } # rubocop:disable Rails/Pluck

        version_list.sort_by { |v| Gem::Version.new(v) }.reverse
      end

      def previous_minor_version(version)
        previous_minor = version[:minor].to_i - 1

        return "#{version[:major]}.#{previous_minor}".match(VERSION_REGEX) if previous_minor >= 0

        fetch_last_minor_version_for_major(version[:major].to_i - 1)
      end

      def fetch_last_minor_version_for_major(major)
        page = 1
        last_minor_version = nil

        while last_minor_version.nil?
          last_minor_version = versions(page).find do |version|
            version.split('.').first.to_i == major
          end

          break if page > 10

          page += 1
        end

        raise VersionApiError if last_minor_version.nil?

        last_minor_version.match(VERSION_REGEX)
      end

      def version_to_minor_string(version)
        "#{version[:major]}.#{version[:minor]}"
      end
    end
  end
end