summaryrefslogtreecommitdiff
path: root/bin/secpick
blob: 10b3ebae68af0f49ab8ab79e83765bd1fa163bc1 (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
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
#!/usr/bin/env ruby

# frozen_string_literal: true

require 'active_support/core_ext/object/to_query'
require 'optparse'
require 'open3'
require 'rainbow/refinement'
using Rainbow

module Secpick
  BRANCH_PREFIX = 'security'
  STABLE_SUFFIX = 'stable'

  DEFAULT_REMOTE = 'security'

  SECURITY_MR_URL = 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/new'

  class SecurityFix
    def initialize
      @options = self.class.options
    end

    def dry_run?
      @options[:try] == true
    end

    def source_branch
      branch = "#{@options[:branch]}-#{@options[:version]}"
      branch = "#{BRANCH_PREFIX}-#{branch}" unless branch.start_with?("#{BRANCH_PREFIX}-")
      branch
    end

    def stable_branch
      "#{@options[:version]}-#{STABLE_SUFFIX}-ee"
    end

    def git_commands
      [
        fetch_stable_branch,
        create_backport_branch,
        cherry_pick_commit,
        push_to_remote,
        checkout_original_branch
      ]
    end

    def gitlab_params
      {
        issuable_template: 'Security Release',
        merge_request: {
          source_branch: source_branch,
          target_branch: stable_branch
        }
      }
    end

    def new_mr_url
      SECURITY_MR_URL
    end

    def create!
      if dry_run?
        puts "\nGit commands:".blue
        puts git_commands.join("\n")

        if !@options[:merge_request]
          puts "\nMerge request URL:".blue
          puts new_mr_url
        end

        puts "\nMerge request params:".blue
        pp gitlab_params
      else
        cmd = git_commands.join(' && ')
        stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)

        puts stdout.read&.green
        puts stderr.read&.red

        if wait_thr.value.success? && !@options[:merge_request]
          puts "#{new_mr_url}?#{gitlab_params.to_query}".blue
        end

        stdin.close
        stdout.close
        stderr.close
      end
    end

    def self.options
      { version: nil, branch: nil, sha: nil, merge_request: false }.tap do |options|
        parser = OptionParser.new do |opts|
          opts.banner = "Usage: #{$0} [options]"
          opts.on('-v', '--version 10.0', 'Version') do |version|
            options[:version] = version&.tr('.', '-')
          end

          opts.on('-b', '--branch security-fix-branch', 'Original branch name (optional, defaults to current)') do |branch|
            options[:branch] = branch
          end

          opts.on('-s', '--sha abcd', 'SHA or SHA range to cherry pick (optional, defaults to current)') do |sha|
            options[:sha] = sha
          end

          opts.on('-r', '--remote dev', "Git remote name of security repo (optional, defaults to `#{DEFAULT_REMOTE}`)") do |remote|
            options[:remote] = remote
          end

          opts.on('--mr', '--merge-request', 'Create relevant security Merge Request targeting the stable branch') do
            options[:merge_request] = true
          end

          opts.on('-d', '--dry-run', 'Only show Git commands, without calling them') do
            options[:try] = true
          end

          opts.on('-h', '--help', 'Displays Help') do
            puts opts

            exit
          end
        end

        parser.parse!

        options[:sha] ||= `git rev-parse HEAD`.strip
        options[:branch] ||= `git rev-parse --abbrev-ref HEAD`.strip
        options[:remote] ||= DEFAULT_REMOTE

        nil_options = options.select {|_, v| v.nil? }
        unless nil_options.empty?
          abort("Missing: #{nil_options.keys.join(', ')}. Use #{$0} --help to see the list of options available".red)
        end

        abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
      end
    end

    private

    def checkout_original_branch
      "git checkout #{@options[:branch]}"
    end

    def push_to_remote
      [
        "git push #{@options[:remote]} #{source_branch} --no-verify",
        *merge_request_push_options
      ].join(' ')
    end

    def merge_request_push_options
      return [] unless @options[:merge_request]

      [
        "-o mr.create",
        "-o mr.target='#{stable_branch}'",
        "-o mr.description='Please apply Security Release template. /milestone %#{milestone}'"
      ]
    end

    def cherry_pick_commit
      "git cherry-pick #{@options[:sha]}"
    end

    def create_backport_branch
      "git checkout -B #{source_branch} #{@options[:remote]}/#{stable_branch} --no-track"
    end

    def fetch_stable_branch
      "git fetch #{@options[:remote]} #{stable_branch}"
    end

    def milestone
      @options[:version].gsub('-', '.')
    end
  end
end

Secpick::SecurityFix.new.create!