Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'bin/saas-feature.rb')
-rwxr-xr-xbin/saas-feature.rb381
1 files changed, 381 insertions, 0 deletions
diff --git a/bin/saas-feature.rb b/bin/saas-feature.rb
new file mode 100755
index 00000000000..878c204d381
--- /dev/null
+++ b/bin/saas-feature.rb
@@ -0,0 +1,381 @@
+#!/usr/bin/env ruby
+#
+# Generate a SaaS feature entry file in the correct location.
+#
+# Automatically stages the file and amends the previous commit if the `--amend`
+# argument is used.
+
+require 'fileutils'
+require 'httparty'
+require 'json'
+require 'optparse'
+require 'readline'
+require 'shellwords'
+require 'uri'
+require 'yaml'
+
+require_relative '../lib/gitlab/popen'
+
+module SaasFeatureHelpers
+ Abort = Class.new(StandardError)
+ Done = Class.new(StandardError)
+
+ def capture_stdout(cmd)
+ output = IO.popen(cmd, &:read)
+ fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
+ output
+ end
+
+ def fail_with(message)
+ raise Abort, "\e[31merror\e[0m #{message}"
+ end
+end
+
+class SaasFeatureOptionParser
+ extend SaasFeatureHelpers
+
+ WWW_GITLAB_COM_SITE = 'https://about.gitlab.com'
+ WWW_GITLAB_COM_GROUPS_JSON = "#{WWW_GITLAB_COM_SITE}/groups.json".freeze
+ COPY_COMMANDS = [
+ 'pbcopy', # macOS
+ 'xclip -selection clipboard', # Linux
+ 'xsel --clipboard --input', # Linux
+ 'wl-copy' # Wayland
+ ].freeze
+ OPEN_COMMANDS = [
+ 'open', # macOS
+ 'xdg-open' # Linux
+ ].freeze
+
+ Options = Struct.new(
+ :name,
+ :group,
+ :milestone,
+ :amend,
+ :dry_run,
+ :force,
+ :introduced_by_url,
+ keyword_init: true
+ )
+
+ class << self
+ def parse(argv)
+ options = Options.new
+
+ parser = OptionParser.new do |opts|
+ opts.banner = "Usage: #{__FILE__} [options] <saas-feature>\n\n"
+
+ # Note: We do not provide a shorthand for this in order to match the `git
+ # commit` interface
+ opts.on('--amend', 'Amend the previous commit') do |value|
+ options.amend = value
+ end
+
+ opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
+ options.force = value
+ end
+
+ opts.on('-m', '--introduced-by-url [string]', String, 'URL of merge request introducing the SaaS feature') do |value|
+ options.introduced_by_url = value
+ end
+
+ opts.on('-M', '--milestone [string]', String, 'Milestone in which the SaaS feature was introduced') do |value|
+ options.milestone = value
+ end
+
+ opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
+ options.dry_run = value
+ end
+
+ opts.on('-g', '--group [string]', String, 'The group introducing a SaaS feature, like: `group::project management`') do |value|
+ options.group = value if group_labels.include?(value)
+ end
+
+ opts.on('-h', '--help', 'Print help message') do
+ $stdout.puts opts
+ raise Done.new
+ end
+ end
+
+ parser.parse!(argv)
+
+ unless argv.one?
+ $stdout.puts parser.help
+ $stdout.puts
+ raise Abort, 'SaaS feature name is required'
+ end
+
+ # Name is a first name
+ options.name = argv.first.downcase.tr('-', '_')
+
+ options
+ end
+
+ def groups
+ @groups ||= fetch_json(WWW_GITLAB_COM_GROUPS_JSON)
+ end
+
+ def group_labels
+ @group_labels ||= groups.map { |_, group| group['label'] }.sort
+ end
+
+ def find_group_by_label(label)
+ groups.find { |_, group| group['label'] == label }[1]
+ end
+
+ def group_list
+ group_labels.map.with_index do |group_label, index|
+ "#{index + 1}. #{group_label}"
+ end
+ end
+
+ def fzf_available?
+ find_compatible_command(%w[fzf])
+ end
+
+ def prompt_readline(prompt:)
+ Readline.readline('?> ', false)&.strip
+ end
+
+ def prompt_fzf(list:, prompt:)
+ arr = list.join("\n")
+
+ selection = IO.popen(%W[fzf --tac --prompt #{prompt}], "r+") do |pipe|
+ pipe.puts(arr)
+ pipe.close_write
+ pipe.readlines
+ end.join.strip
+
+ selection[/(\d+)\./, 1]
+ end
+
+ def print_list(list)
+ return if list.empty?
+
+ $stdout.puts list.join("\n")
+ end
+
+ def print_prompt(prompt)
+ $stdout.puts
+ $stdout.puts ">> #{prompt}:"
+ $stdout.puts
+ end
+
+ def prompt_list(prompt:, list: nil)
+ if fzf_available?
+ prompt_fzf(list: list, prompt: prompt)
+ else
+ prompt_readline(prompt: prompt)
+ end
+ end
+
+ def fetch_json(json_url)
+ json = with_retries { HTTParty.get(json_url, format: :plain) }
+ JSON.parse(json)
+ end
+
+ def with_retries(attempts: 3)
+ yield
+ rescue Errno::ECONNRESET, OpenSSL::SSL::SSLError, Net::OpenTimeout
+ retry if (attempts -= 1).positive?
+ raise
+ end
+
+ def read_group
+ prompt = 'Specify the group label to which the SaaS feature belongs, from the following list'
+
+ unless fzf_available?
+ print_prompt(prompt)
+ print_list(group_list)
+ end
+
+ loop do
+ group = prompt_list(prompt: prompt, list: group_list)
+ group = group_labels[group.to_i - 1] unless group.to_i.zero?
+
+ if group_labels.include?(group)
+ $stdout.puts "You picked the group '#{group}'"
+ return group
+ else
+ $stderr.puts "The group label isn't in the above labels list"
+ end
+
+ end
+ end
+
+ def read_introduced_by_url
+ read_url('URL of the MR introducing the SaaS feature (enter to skip and let Danger provide a suggestion directly in the MR):')
+ end
+
+ def read_milestone
+ milestone = File.read('VERSION')
+ milestone.gsub(/^(\d+\.\d+).*$/, '\1').chomp
+ end
+
+ def read_url(prompt)
+ $stdout.puts
+ $stdout.puts ">> #{prompt}"
+
+ loop do
+ url = Readline.readline('?> ', false)&.strip
+ url = nil if url.empty?
+ return url if url.nil? || valid_url?(url)
+ end
+ end
+
+ def valid_url?(url)
+ unless url.start_with?('https://')
+ $stderr.puts 'URL needs to start with https://'
+ return false
+ end
+
+ response = HTTParty.head(url)
+
+ return true if response.success?
+
+ $stderr.puts "URL '#{url}' isn't valid!"
+ end
+
+ def open_url!(url)
+ _, open_url_status = Gitlab::Popen.popen([open_command, url])
+
+ open_url_status
+ end
+
+ def copy_to_clipboard!(text)
+ IO.popen(copy_to_clipboard_command.shellsplit, 'w') do |pipe|
+ pipe.print(text)
+ end
+ end
+
+ def copy_to_clipboard_command
+ find_compatible_command(COPY_COMMANDS)
+ end
+
+ def open_command
+ find_compatible_command(OPEN_COMMANDS)
+ end
+
+ def find_compatible_command(commands)
+ commands.find do |command|
+ Gitlab::Popen.popen(%W[which #{command.split(' ')[0]}])[1] == 0
+ end
+ end
+ end
+end
+
+class SaasFeatureCreator
+ include SaasFeatureHelpers
+
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def execute
+ assert_feature_branch!
+ assert_name!
+ assert_existing_saas_feature!
+
+ options.group ||= SaasFeatureOptionParser.read_group
+ options.introduced_by_url ||= SaasFeatureOptionParser.read_introduced_by_url
+ options.milestone ||= SaasFeatureOptionParser.read_milestone
+
+ $stdout.puts "\e[32mcreate\e[0m #{file_path}"
+ $stdout.puts contents
+
+ unless options.dry_run
+ write
+ amend_commit if options.amend
+ end
+
+ if editor
+ system(editor, file_path)
+ end
+ end
+
+ private
+
+ def contents
+ config_hash.to_yaml
+ end
+
+ def config_hash
+ {
+ 'name' => options.name,
+ 'introduced_by_url' => options.introduced_by_url,
+ 'milestone' => options.milestone,
+ 'group' => options.group
+ }
+ end
+
+ def write
+ FileUtils.mkdir_p(File.dirname(file_path))
+ File.write(file_path, contents)
+ end
+
+ def editor
+ ENV['EDITOR']
+ end
+
+ def amend_commit
+ fail_with 'git add failed' unless system(*%W[git add #{file_path}])
+
+ system('git commit --amend')
+ end
+
+ def assert_feature_branch!
+ return unless branch_name == 'master'
+
+ fail_with 'Create a branch first!'
+ end
+
+ def assert_existing_saas_feature!
+ existing_path = all_saas_feature_names[options.name]
+ return unless existing_path
+ return if options.force
+
+ fail_with "#{existing_path} already exists! Use `--force` to overwrite."
+ end
+
+ def assert_name!
+ return if options.name.match(/\A[a-z0-9_-]+\Z/)
+
+ fail_with 'Provide a name for the SaaS feature that is [a-z0-9_-]'
+ end
+
+ def file_path
+ saas_features_path.sub('*.yml', options.name + '.yml')
+ end
+
+ def all_saas_feature_names
+ # check flatten needs
+ @all_saas_feature_names ||=
+ Dir.glob(saas_features_path).map do |path|
+ [File.basename(path, '.yml'), path]
+ end.to_h
+ end
+
+ def saas_features_path
+ File.join('ee', 'config', 'saas_features', '*.yml')
+ end
+
+ def branch_name
+ @branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
+ end
+end
+
+if $0 == __FILE__
+ begin
+ options = SaasFeatureOptionParser.parse(ARGV)
+ SaasFeatureCreator.new(options).execute
+ rescue SaasFeatureHelpers::Abort => ex
+ $stderr.puts ex.message
+ exit 1
+ rescue SaasFeatureHelpers::Done
+ exit
+ end
+end
+
+# vim: ft=ruby