renuo-cli 4.21.2 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9120767921d12d64d169446d7dc3a7e0329f498e80fc5c4f4afca46583ff9418
4
- data.tar.gz: e16008accec27c375cb831856ce4fb9acce1ac2630fcdae9dfffcbc0a9a63c2f
3
+ metadata.gz: d49d5760372b8bb7bd5ada89efeb64580a32f62997a701597a48a5ce7fda72ed
4
+ data.tar.gz: 384d94a32debe72aab7d4d68b0c3b4241ee4260e74caeaae63751daf8b044fba
5
5
  SHA512:
6
- metadata.gz: 93c42b19caa65fb26adf806fe0079c842b6ed8661aaaa639ed5325bc4cd64d62d783363a6374db2aed5b3df50bc6769af4220b703359f75c0a38f5ef77dbc0c8
7
- data.tar.gz: 3966f6f6510994401559c46b32931645d51f726d84a67a402977b5df6d9640b8e146c3208f04d54c05c3e0decee308c54da9a3c952a8ce6a068af637a2551c53
6
+ metadata.gz: e9a012730f76d8428624dbe15a56f78bab6b9c1f0109d30b1eebc0cd31434ebb1657d1704c4a5d887d6d4b8c1ec5157c1eb2dc0ab54f426a3a2f2d72317b90aa
7
+ data.tar.gz: cdf2265362bedb6f2172ca1058ccfdcf06f5823ea244a26aff46d1bf9bb68a537828cc4f571780eafdd41daeec3ae00e82b4d7f77869ed472233edaa5e30a9e8
data/README.md CHANGED
@@ -14,13 +14,24 @@ gem install renuo-cli
14
14
  renuo -h
15
15
  ```
16
16
 
17
+ ### Shell Autocompletion
18
+
19
+ To enable zsh autocompletion for renuo-cli commands:
20
+
21
+ ```sh
22
+ renuo setup-autocompletion
23
+ ```
24
+
25
+ This will install the completion script and provide instructions to add it to your shell configuration.
26
+
17
27
  ## Development
18
28
 
19
29
  After checking out the repo, run `bin/setup` to install dependencies.
20
30
 
21
31
  Run `rake spec` to run the tests.
22
32
 
23
- Run `bundle exec ruby -Ilib ./bin/renuo` to run the executable. (e.g. `ruby -Ilib ./bin/renuo -v`)
33
+ Run `bin/renuo` to run the executable against the working tree. (e.g. `bin/renuo -v`)
34
+ The shipped executable lives in `exe/renuo`; `bin/renuo` is a bundler binstub that loads it with the local code.
24
35
 
25
36
  You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
37
 
@@ -36,7 +47,7 @@ This binds the `renuo` command to your local version, so you can run commands di
36
47
 
37
48
  ```shell
38
49
  renuo -h
39
- renuo generate_github_settings
50
+ renuo generate-password
40
51
  ```
41
52
 
42
53
  To install this gem onto your local machine, run `bundle exec rake install`.
data/exe/renuo ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "renuo/cli"
5
+ Renuo::Cli.new.start
@@ -2,9 +2,10 @@
2
2
 
3
3
  # :nocov:
4
4
  module Renuo::Cli::Commands::Ci
5
- class CheckDeploioStatus
5
+ class CheckDeploioStatus # rubocop:disable Metrics/ClassLength
6
6
  TIMEOUT_IN_SECONDS = 600
7
7
  INTERVAL_IN_SECONDS = 5
8
+ LOG_DRAIN_SECONDS = 2
8
9
 
9
10
  command "ci check-deploio-status" do |c|
10
11
  c.syntax = "renuo ci check-deploio-status [options] <app-name>"
@@ -17,11 +18,11 @@ module Renuo::Cli::Commands::Ci
17
18
 
18
19
  def run
19
20
  puts "(1/2) Checking build status for revision #{@revision}..."
20
- poll "build"
21
+ with_streamed_logs("build") { poll "build" }
21
22
  abort "build check timed out after #{TIMEOUT_IN_SECONDS} seconds" if build.nil?
22
23
 
23
24
  puts "(2/2) Checking release status for build #{build_name}..."
24
- poll "release"
25
+ with_streamed_logs("app") { poll "release" }
25
26
  abort "release check timed out after #{TIMEOUT_IN_SECONDS} seconds" if release.nil?
26
27
  end
27
28
 
@@ -49,12 +50,24 @@ module Renuo::Cli::Commands::Ci
49
50
  Psych.load_stream stdout
50
51
  end
51
52
 
52
- def fetch_build_logs
53
- command = "nctl logs build -a #{@app} -p #{@project} -l 5000 --no-labels"
54
- stdout, stderr, status = Open3.capture3 command
53
+ def with_streamed_logs(type)
54
+ pid = Process.spawn(
55
+ "nctl logs application #{@app} -p #{@project} --type=#{type} -f --lines=100 --since 15m --no-labels",
56
+ out: $stdout, err: $stdout
57
+ )
58
+ yield
59
+ ensure
60
+ sleep LOG_DRAIN_SECONDS
61
+ stop_streaming(pid)
62
+ end
63
+
64
+ def stop_streaming(pid)
65
+ return unless pid
55
66
 
56
- puts "error fetching build logs: #{stderr}" unless status.success?
57
- stdout
67
+ Process.kill("KILL", pid)
68
+ Process.wait(pid)
69
+ rescue Errno::ESRCH, Errno::ECHILD
70
+ nil
58
71
  end
59
72
 
60
73
  def build
@@ -82,7 +95,7 @@ module Renuo::Cli::Commands::Ci
82
95
  end
83
96
 
84
97
  def succeeded?(status, type) # rubocop:disable Metrics/MethodLength
85
- time = "[#{Time.now.utc.iso8601}]"
98
+ time = Time.current.iso8601.to_s
86
99
 
87
100
  case status
88
101
  when "available", "success"
@@ -94,7 +107,6 @@ module Renuo::Cli::Commands::Ci
94
107
  when "paused"
95
108
  abort "#{time} app is paused"
96
109
  when "error", "failed", "failure"
97
- puts fetch_build_logs
98
110
  abort "#{time} #{type} failed"
99
111
  else
100
112
  puts "#{time} #{type} status is #{status}, waiting..."
@@ -15,7 +15,7 @@ module Renuo::Cli::Commands::Ci
15
15
  end
16
16
 
17
17
  def run
18
- system! "nctl update app #{@app} -p #{@project} --env APP_REVISION=#{@git_revision} " \
18
+ system! "nctl update app #{@app} -p #{@project} " \
19
19
  "#{"--build-env RUBY_VERSION=#{File.read(".ruby-version").strip} " if @docker}" \
20
20
  "--git-revision #{@git_revision} --skip-repo-access-check"
21
21
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Renuo::Cli::Commands::ConfigureSemaphore
4
+ include CredentialsHelper
5
+
4
6
  attr_accessor :project_name, :environment, :slack_endpoint
5
7
 
6
8
  command "configure-semaphore" do |c|
@@ -13,7 +15,7 @@ class Renuo::Cli::Commands::ConfigureSemaphore
13
15
 
14
16
  def initialize
15
17
  @project_name = File.basename(Dir.getwd)
16
- @slack_endpoint = "https://hooks.slack.com/services/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"
18
+ @slack_endpoint = credentials[:slack_endpoint]
17
19
  end
18
20
 
19
21
  def run # rubocop:disable Metrics/MethodLength
@@ -96,11 +96,11 @@ class Renuo::Cli::Commands::CreateDeploioApp # rubocop:disable Metrics/ClassLeng
96
96
 
97
97
  def set_postgres_version
98
98
  loop do
99
- @postgres_version = ask("Enter Postgres version (e.g., 17) or leave empty to skip database creation: ")
99
+ @postgres_version = ask("Enter Postgres version (e.g., 18) or leave empty to skip database creation: ")
100
100
  break if @postgres_version.empty? || @postgres_version.match?(/^\d+$/)
101
101
 
102
102
  say ">> The postgres version is invalid. Only major versions are allowed. " \
103
- "For example, use 17 instead of 17.4. Please try again.".colorize(:red)
103
+ "For example, use 18 instead of 18.4. Please try again.".colorize(:red)
104
104
  end
105
105
  end
106
106
 
@@ -175,6 +175,7 @@ class Renuo::Cli::Commands::CreateDeploioApp # rubocop:disable Metrics/ClassLeng
175
175
  --basic-auth=#{environment == "develop"} \\
176
176
  --build-env=SECRET_KEY_BASE="$(rails secret)" \\
177
177
  --language=ruby \\
178
+ --buildpack-stack=heroku \\
178
179
  --size=mini
179
180
  OUTPUT
180
181
  end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/environments"
4
+
5
+ class Renuo::Cli::Commands::CreateNineCloudvm
6
+ SSH_KEY_FILE_NAME = "temporary_cloudvm_ssh_key"
7
+ SSH_ALGORITHM = "Ed25519"
8
+ DEFAULT_LOCATION = "nine-es34"
9
+ DEFAULT_MACHINE_TYPE = "nine-small-1"
10
+ DEFAULT_OS = "debian13"
11
+ VPS_SETUP_GUIDE_REPO = "https://github.com/renuo/vps-setup-guide"
12
+
13
+ command "create-nine-cloudvm" do |c|
14
+ c.syntax = "renuo create-nine-cloudvm [project-name]"
15
+ c.summary = "Generates the script necessary to setup a cloud VM on Nine."
16
+ c.description = <<~DESC
17
+ Generates the script necessary to setup a cloud VM on Nine.
18
+ This command will:
19
+ - Prompt for the organization, project name, location, machine type, OS, and 1Password vault name.
20
+ - Validate the provided inputs.
21
+ - Generate an SSH key (stored in 1Password) to access the cloud VM.
22
+ - Generate the commands to create a cloud VM for each environment on Nine.
23
+ DESC
24
+ c.option "-o", "--org <organization>", String, "The organization name"
25
+ c.example "renuo create-nine-cloudvm my-app",
26
+ "Prompts the user for the necessary information and generates the commands " \
27
+ "to create the cloud VMs on Nine."
28
+ c.action { |args, options| new.run(args, options) }
29
+ end
30
+
31
+ def run(args, options = {})
32
+ @project_name, = args
33
+ @options = options
34
+ parse_arguments
35
+ setup_ssh_key
36
+ setup_environments
37
+ say_configuration_note
38
+ ensure
39
+ cleanup
40
+ end
41
+
42
+ private
43
+
44
+ def valid_name?(name)
45
+ name&.length&.between?(3, 63) && name.match?(/^[a-z0-9-]+$/)
46
+ end
47
+
48
+ def parse_arguments
49
+ set_organization
50
+ set_project_name
51
+ set_location
52
+ set_machine_type
53
+ set_os
54
+ set_vault_name
55
+ end
56
+
57
+ def set_project_name
58
+ loop do
59
+ break if valid_name?(@project_name)
60
+
61
+ say ">> Project name must be between 3 and 63 characters and can only contain lowercase letters, " \
62
+ "numbers, and hyphens. Please try again.".colorize(:red)
63
+
64
+ @project_name = ask("Enter the project name (e.g: my-app): ")
65
+ end
66
+
67
+ @project_name_with_org_prefix = "#{@organization}-#{@project_name}"
68
+ end
69
+
70
+ def set_organization
71
+ @organization = @options.org if @options.org
72
+
73
+ loop do
74
+ break if valid_name?(@organization)
75
+
76
+ say ">> Organization name must be between 3 and 63 characters and can only contain lowercase letters, " \
77
+ "numbers, and hyphens. Please try again.".colorize(:red)
78
+
79
+ @organization = ask("Enter the organization name (default: renuo): ") { |q| q.default = "renuo" }
80
+ end
81
+ end
82
+
83
+ def set_location
84
+ @location = ask("Enter the location (default: #{DEFAULT_LOCATION}): ") { |q| q.default = DEFAULT_LOCATION }
85
+ end
86
+
87
+ def set_machine_type
88
+ @machine_type = ask("Enter the machine type (default: #{DEFAULT_MACHINE_TYPE}): ") do |q|
89
+ q.default = DEFAULT_MACHINE_TYPE
90
+ end
91
+ end
92
+
93
+ def set_os
94
+ @os = ask("Enter the operating system (default: #{DEFAULT_OS}): ") { |q| q.default = DEFAULT_OS }
95
+ end
96
+
97
+ def set_vault_name
98
+ @vault_name = ask("An SSH key to access the cloud VM will be stored on 1Password. " \
99
+ "Enter the 1Password vault name: ") do |q|
100
+ q.default = "Deploio"
101
+ end
102
+ end
103
+
104
+ def setup_ssh_key
105
+ say "\n# Generate the SSH key to access the cloud VM\n".bold
106
+ generate_ssh_key(SSH_KEY_FILE_NAME)
107
+ end
108
+
109
+ def setup_environments
110
+ ENVIRONMENTS.each do |environment|
111
+ say "\n# Commands to create #{environment} cloud VM \n".bold
112
+ say_cloudvm_creation(environment)
113
+ end
114
+ end
115
+
116
+ def say_configuration_note
117
+ say "\n# Configure the cloud VMs\n".bold
118
+ say <<~OUTPUT
119
+ Once the cloud VMs are up, configure them (SSH access, monitoring, backups) with the
120
+ Ansible VPS setup guide: #{VPS_SETUP_GUIDE_REPO}
121
+ OUTPUT
122
+ end
123
+
124
+ def cleanup
125
+ say "\n# Cleanup temporary files\n".bold
126
+ say "rm #{SSH_KEY_FILE_NAME} #{SSH_KEY_FILE_NAME}.pub"
127
+ end
128
+
129
+ def say_cloudvm_creation(environment)
130
+ say <<~OUTPUT
131
+ nctl create cloudvm #{environment} \\
132
+ --project #{@project_name_with_org_prefix} \\
133
+ --wait \\
134
+ --location="#{@location}" \\
135
+ --machine-type="#{@machine_type}" \\
136
+ --hostname="#{hostname(environment)}" \\
137
+ --os="#{@os}" \\
138
+ --ssh-keys-from-files=#{SSH_KEY_FILE_NAME}.pub
139
+ OUTPUT
140
+ end
141
+
142
+ def hostname(environment)
143
+ "#{@project_name_with_org_prefix}-#{environment}"
144
+ end
145
+
146
+ def generate_ssh_key(file_name)
147
+ say <<~OUTPUT
148
+ # Create SSH key item in 1Password directly
149
+ op item create \\
150
+ --account renuo.1password.com \\
151
+ --category ssh \\
152
+ --ssh-generate-key #{SSH_ALGORITHM.downcase} \\
153
+ --title "#{@project_name_with_org_prefix}-cloudvm-key" \\
154
+ --vault #{@vault_name} \\
155
+ --tags #{@project_name_with_org_prefix}
156
+
157
+ # Extract keys to temp files for cloud VM usage
158
+ op item get "#{@project_name_with_org_prefix}-cloudvm-key" --vault #{@vault_name} \\
159
+ --account renuo.1password.com \\
160
+ --reveal --fields "public key" > #{file_name}.pub
161
+ op item get "#{@project_name_with_org_prefix}-cloudvm-key" --vault #{@vault_name} --reveal \\
162
+ --account renuo.1password.com \\
163
+ --fields "private key" --format json | jq -r '.ssh_formats.openssh.value' > #{file_name}
164
+ OUTPUT
165
+ end
166
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/commander_introspector"
4
+ require_relative "../helpers/zsh_completion_script"
5
+
6
+ class Renuo::Cli::Commands::SetupAutocompletion
7
+ include CommandHelper
8
+
9
+ COMPLETION_DIR = File.expand_path("~/.zfunc")
10
+ COMPLETION_FILENAME = "_renuo"
11
+ COMPLETION_FILE_PATH = File.join(COMPLETION_DIR, COMPLETION_FILENAME)
12
+ VERSION_STAMP_PREFIX = "# renuo-cli-version: "
13
+
14
+ command "setup-autocompletion" do |c|
15
+ c.syntax = "renuo setup-autocompletion"
16
+ c.summary = "Sets up zsh autocompletion for renuo-cli"
17
+ c.description = "Installs zsh autocompletion script for renuo-cli commands and options"
18
+ c.example "renuo setup-autocompletion", "Sets up zsh autocompletion"
19
+ c.action { new.run }
20
+ end
21
+
22
+ def self.refresh_if_stale
23
+ return unless File.exist?(COMPLETION_FILE_PATH)
24
+ return if File.foreach(COMPLETION_FILE_PATH).first(2).last&.include?("#{VERSION_STAMP_PREFIX}#{Renuo::Cli::VERSION}")
25
+
26
+ new.write_completion_file(silent: true)
27
+ end
28
+
29
+ def run
30
+ write_completion_file
31
+ print_installation_instructions
32
+ end
33
+
34
+ def write_completion_file(silent: false)
35
+ say "Setting up zsh autocompletion for renuo-cli...".colorize(:green) unless silent
36
+ FileUtils.mkdir_p(COMPLETION_DIR)
37
+ File.write(COMPLETION_FILE_PATH, stamped_script)
38
+ say "✓ Completion script installed to #{COMPLETION_FILE_PATH}".colorize(:green) unless silent
39
+ end
40
+
41
+ private
42
+
43
+ def stamped_script
44
+ script = completion_script
45
+ first_line, rest = script.split("\n", 2)
46
+ "#{first_line}\n#{VERSION_STAMP_PREFIX}#{Renuo::Cli::VERSION}\n#{rest}"
47
+ end
48
+
49
+ def completion_script
50
+ ZshCompletionScript.new(CommanderIntrospector.commands_data).render
51
+ end
52
+
53
+ def print_installation_instructions
54
+ say ""
55
+ say "To enable autocompletion, add the following to your ~/.zshrc:".colorize(:blue)
56
+ say ""
57
+ say " # Enable renuo-cli autocompletion"
58
+ say " fpath+=#{COMPLETION_DIR}"
59
+ say ""
60
+ say "Then restart your shell or run:".colorize(:blue)
61
+ say " exec zsh"
62
+ say "On the next renuo-cli update, the completion script will be automatically refreshed".colorize(:cyan)
63
+ end
64
+ end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Renuo::Cli::Commands::SetupUptimerobot
4
- CONTACT_GROUP_IDS = "2811620_0_0-6298845_0_0-5223624_0_0"
5
- HTTPS_MONITORING = 1
6
- STATUS_PAUSED = 0
7
-
8
4
  command "setup-uptimerobot" do |c|
9
5
  c.syntax = "renuo setup-uptimerobot"
10
6
  c.summary = "Sets up uptimerobot for the given project"
@@ -17,8 +13,8 @@ class Renuo::Cli::Commands::SetupUptimerobot
17
13
 
18
14
  def initialize(args)
19
15
  abort("No project name given.") if args.nil?
20
- @api_key = ask_for_api_key
21
16
  @url = validate_url(args)
17
+ @api_key = read_api_key
22
18
  end
23
19
 
24
20
  def run
@@ -30,10 +26,18 @@ class Renuo::Cli::Commands::SetupUptimerobot
30
26
 
31
27
  private
32
28
 
33
- def ask_for_api_key
29
+ LIST_API_KEY_CMD = "op item list --tags renuo-cli-uptimerobot-api-key --format=json"
30
+ EXTRACT_API_KEY_CMD = "op item get --reveal --field credential"
31
+ def read_api_key
34
32
  say "This command will configure uptimerobot for your project."
35
- say "Please give in the api_key for uptimerobot"
36
- ask("API-key: ")
33
+ say "Fetching the uptimerobot api_key from 1Password..."
34
+ stdout, stderr, status = Open3.capture3("#{LIST_API_KEY_CMD} | #{EXTRACT_API_KEY_CMD}")
35
+ abort("Failed to fetch the uptimerobot api_key from 1Password: #{stderr.strip}") unless status.success?
36
+
37
+ api_key = stdout.strip
38
+ abort("No 1Password item tagged 'renuo-cli-uptimerobot-api-key' was found.") if api_key.empty?
39
+
40
+ api_key
37
41
  end
38
42
 
39
43
  def validate_url(args)
@@ -42,13 +46,19 @@ class Renuo::Cli::Commands::SetupUptimerobot
42
46
  URI.parse(args.first)
43
47
  end
44
48
 
49
+ HTTPS_MONITORING = 1
50
+ STATUS_PAUSED = 0
51
+ DOMAIN_EXPIRY_NOTIFICATIONS_DISABLED = 1
52
+ # Format is "id_threshold_recurrence": notify #uptime after 10 minutes of downtime, without re-notifying.
53
+ ALERT_CONTACTS = "7783511_10_0"
45
54
  def create_robot_params
46
55
  {
47
56
  friendly_name: @url.host.downcase,
48
57
  url: @url.to_s,
49
58
  type: HTTPS_MONITORING,
50
59
  interval: 300,
51
- alert_contacts: CONTACT_GROUP_IDS
60
+ alert_contacts: ALERT_CONTACTS,
61
+ disable_domain_expire_notifications: DOMAIN_EXPIRY_NOTIFICATIONS_DISABLED
52
62
  }
53
63
  end
54
64
 
@@ -0,0 +1 @@
1
+ kQgNz+OSHLaY1YKXAXJi5Nb/qP2N2+2BHy2anqFasu1HBitNqrEfV+rfFKNfzFY1hNtAezy3SiCA3csD3jsIa7Ew/UHcys493oGNF6p9m07y7/gE+sK4fiQVIRMWxVZ7RPy5qszve+E=--emz3hoXKrKnjz9cq--GxOYIZ9IVxnCjhrg/3kHpQ==
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module CommanderIntrospector
6
+ module_function
7
+
8
+ HELP_FLAGS = ["-h", "--help"].freeze
9
+
10
+ def commands_data
11
+ runner_commands = Commander::Runner.instance.commands
12
+ entries = runner_commands.reject { |name, _| name.to_s == "help" }
13
+ .map { |name, cmd| build_command_data(name.to_s, cmd) }
14
+ (entries + [help_entry]).sort_by { |cmd| cmd[:name] }
15
+ end
16
+
17
+ def build_command_data(name, command)
18
+ {
19
+ name: name,
20
+ summary: clean(command.summary),
21
+ description: first_line(clean(command.description.to_s)),
22
+ syntax: command.syntax.to_s,
23
+ options: extract_options(command.options),
24
+ examples: extract_examples(name, command.examples)
25
+ }
26
+ end
27
+
28
+ def help_entry
29
+ {
30
+ name: "help",
31
+ summary: "Display global or command help documentation",
32
+ description: "Display global or command help documentation",
33
+ syntax: "renuo help [command]",
34
+ options: [], examples: []
35
+ }
36
+ end
37
+
38
+ def extract_options(options) # rubocop:disable Metrics/AbcSize
39
+ options.filter_map do |opt|
40
+ switches = opt[:switches]
41
+ next if switches.intersect?(HELP_FLAGS)
42
+
43
+ long = switches.find { |s| s.start_with?("--") }
44
+ next unless long
45
+
46
+ {
47
+ flags: [switches.find { |s| s.match?(/\A-[^-]/) }, long.split.first].compact,
48
+ description: clean(opt[:description]),
49
+ argument: switches.filter_map { |s| s[/<([^>]+)>/, 1] }.first
50
+ }
51
+ end
52
+ end
53
+
54
+ def extract_examples(command_name, examples)
55
+ return [] if examples.blank?
56
+
57
+ prefix = ["renuo", *command_name.split]
58
+ examples.filter_map { |pair| pick_command(pair, prefix) }.uniq
59
+ end
60
+
61
+ def pick_command(pair, prefix)
62
+ Array(pair).find do |text|
63
+ next false unless text.is_a?(String)
64
+
65
+ tokens = safe_split(text)
66
+ tokens && tokens.first(prefix.size) == prefix
67
+ end
68
+ end
69
+
70
+ def safe_split(text)
71
+ Shellwords.split(text)
72
+ rescue ArgumentError
73
+ nil
74
+ end
75
+
76
+ def clean(text)
77
+ text.to_s.gsub(/['\[\]]/, "").strip
78
+ end
79
+
80
+ def first_line(text)
81
+ text.split("\n").map(&:strip).reject(&:empty?).first.to_s
82
+ end
83
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/encrypted_configuration"
4
+
5
+ module CredentialsHelper
6
+ CONFIG = ActiveSupport::EncryptedConfiguration.new(
7
+ config_path: File.expand_path("../credentials.yml.enc", __dir__),
8
+ key_path: "", env_key: "RENUO_CLI_MASTER_KEY", raise_if_missing_key: true
9
+ )
10
+
11
+ def credentials
12
+ CONFIG.config
13
+ end
14
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ class ZshCompletionScript
6
+ TEMPLATE_PATH = File.expand_path("../templates/zsh_completion.erb", __dir__)
7
+
8
+ def initialize(commands)
9
+ @commands = commands
10
+ end
11
+
12
+ def render
13
+ ERB.new(File.read(TEMPLATE_PATH), trim_mode: "-").result(binding)
14
+ end
15
+
16
+ private
17
+
18
+ def namespaced_groups
19
+ groups.select { |_, sub_commands| sub_commands.first[:subname] }
20
+ end
21
+
22
+ def flat_commands
23
+ groups.reject { |_, sub_commands| sub_commands.first[:subname] }.map { |_, sub_commands| sub_commands.first }
24
+ end
25
+
26
+ def top_level_entries
27
+ groups.map do |name, sub_commands|
28
+ summary = sub_commands.first[:subname] ? "#{name} subcommands" : sub_commands.first[:summary]
29
+ { name: name, summary: summary }
30
+ end
31
+ end
32
+
33
+ def groups
34
+ @groups ||= @commands.each_with_object({}) do |cmd, acc|
35
+ head, sub = cmd[:name].split(" ", 2)
36
+ key = sub ? head : cmd[:name]
37
+ (acc[key] ||= []) << cmd.merge(subname: sub)
38
+ end
39
+ end
40
+
41
+ def command_body(cmd, indent:)
42
+ pad = " " * indent
43
+ messages = message_blocks(cmd, pad)
44
+ return [arguments_block(cmd, pad), *messages].join("\n") if cmd[:options].any?
45
+
46
+ inner_messages = message_blocks(cmd, "#{pad} ")
47
+ [
48
+ %(#{pad}if [[ "${words[CURRENT]}" == -* ]]; then),
49
+ %(#{pad} _arguments '--help[show help]'),
50
+ "#{pad}else",
51
+ *inner_messages,
52
+ "#{pad}fi"
53
+ ].join("\n")
54
+ end
55
+
56
+ def message_blocks(cmd, pad) # rubocop:disable Metrics/AbcSize
57
+ blocks = []
58
+ blocks << message_block("syntax", [cmd[:syntax]], pad) unless cmd[:syntax].to_s.empty?
59
+ blocks << message_block("description", [cmd[:description]], pad) unless cmd[:description].to_s.empty?
60
+ blocks << message_block("example", cmd[:examples], pad) unless cmd[:examples].empty?
61
+ blocks
62
+ end
63
+
64
+ def arguments_block(cmd, pad)
65
+ specs = ["'--help[show help]'"]
66
+ cmd[:options].each { |opt| specs << option_spec(opt) }
67
+ specs << "'*: :( )'"
68
+ "#{pad}_arguments \\\n#{specs.map { |s| "#{pad} #{s}" }.join(" \\\n")}"
69
+ end
70
+
71
+ def option_spec(option)
72
+ flags = option[:flags]
73
+ bracket = "[#{escape_bracket(option[:description])}]"
74
+ arg_part = option[:argument] ? ":#{option[:argument]}:" : ""
75
+ return "'#{flags.first}#{bracket}#{arg_part}'" if flags.size < 2
76
+
77
+ "'(#{flags.join(" ")})'{#{flags.join(",")}}'#{bracket}#{arg_part}'"
78
+ end
79
+
80
+ def message_block(header, lines, pad)
81
+ payload = (["-- #{header} --"] + lines + [""]).join("\n")
82
+ "#{pad}_message -r '#{escape_single(payload)}'"
83
+ end
84
+
85
+ def escape_bracket(text)
86
+ text.to_s.gsub("]") { "\\]" }.gsub("'") { "'\\''" }
87
+ end
88
+
89
+ def escape_single(text)
90
+ text.to_s.gsub("'") { "'\\''" }
91
+ end
92
+ end
@@ -24,7 +24,7 @@ global_job_config:
24
24
  - source .semaphore/bin/cache_restore rails
25
25
  - bundle config set deployment 'true'
26
26
  - bundle config set path 'vendor/bundle'
27
- - bundle install
27
+ - bundle install --retry 3
28
28
 
29
29
  blocks:
30
30
  - name: linting
@@ -49,7 +49,7 @@ blocks:
49
49
  value: test
50
50
  prologue:
51
51
  commands:
52
- - sem-service start postgres 17
52
+ - sem-service start postgres 18
53
53
  - cp .env.example .env
54
54
  - bundle exec rails db:create db:schema:load
55
55
  jobs:
@@ -0,0 +1,61 @@
1
+ #compdef renuo
2
+
3
+ _renuo() {
4
+ local curcontext="$curcontext" state
5
+ typeset -A opt_args
6
+
7
+ zstyle ":completion:${curcontext}:*:descriptions" format '\n -- %d --'
8
+ zstyle ":completion:${curcontext}:*" group-name ''
9
+ zstyle ":completion:${curcontext}:*" prefix-needed no
10
+
11
+ _arguments -C \
12
+ '--help[show help]' \
13
+ '--version[show version]' \
14
+ '--trace[show backtrace on error]' \
15
+ '1: :_renuo_commands' \
16
+ '*:: :->args' && return 0
17
+
18
+ case $state in
19
+ args)
20
+ local cmd_key="${words[1]}"
21
+ <% namespaced_groups.each do |group, subs| -%>
22
+ if [[ "$cmd_key" == "<%= group %>" ]]; then
23
+ case "${words[2]}" in
24
+ <% subs.each do |sub| -%>
25
+ <%= sub[:subname] %>)
26
+ <%= command_body(sub, indent: 24) %>
27
+ ;;
28
+ <% end -%>
29
+ esac
30
+ return
31
+ fi
32
+ <% end -%>
33
+ case "$cmd_key" in
34
+ <% flat_commands.each do |cmd| -%>
35
+ <%= cmd[:name] %>)
36
+ <% if cmd[:name] == "help" -%>
37
+ _arguments \
38
+ '--help[show help]' \
39
+ '*:command:_renuo_commands'
40
+ <% else -%>
41
+ <%= command_body(cmd, indent: 20) %>
42
+ <% end -%>
43
+ ;;
44
+ <% end -%>
45
+ esac
46
+ ;;
47
+ esac
48
+ }
49
+
50
+ _renuo_commands() {
51
+ local commands
52
+ commands=(
53
+ <% top_level_entries.each do |entry| -%>
54
+ '<%= entry[:name] %>:<%= entry[:summary] %>'
55
+ <% end -%>
56
+ )
57
+
58
+ _describe 'commands' commands
59
+ }
60
+
61
+ _renuo "$@"
@@ -3,7 +3,7 @@
3
3
  # :nocov:
4
4
  module Renuo
5
5
  class Cli
6
- VERSION = "4.21.2"
6
+ VERSION = "4.23.0"
7
7
  NAME = "renuo-cli"
8
8
  end
9
9
  end
data/lib/renuo/cli.rb CHANGED
@@ -35,6 +35,7 @@ module Renuo
35
35
  program :help_paging, false
36
36
 
37
37
  Dir[File.expand_path("cli/{commands,services}/**/*.rb", __dir__)].each { |f| require f }
38
+ Renuo::Cli::Commands::SetupAutocompletion.refresh_if_stale
38
39
  default_command :help
39
40
  end
40
41
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: renuo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.21.2
4
+ version: 4.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renuo AG
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -62,9 +62,8 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - LICENSE.txt
64
64
  - README.md
65
- - bin/renuo
65
+ - exe/renuo
66
66
  - lib/renuo/cli.rb
67
- - lib/renuo/cli/commands/check_deploio_status.rb
68
67
  - lib/renuo/cli/commands/ci/check_deploio_status.rb
69
68
  - lib/renuo/cli/commands/ci/setup.rb
70
69
  - lib/renuo/cli/commands/ci/update_deploio_app.rb
@@ -77,6 +76,7 @@ files:
77
76
  - lib/renuo/cli/commands/create_deploio_object_storage.rb
78
77
  - lib/renuo/cli/commands/create_heroku_app.rb
79
78
  - lib/renuo/cli/commands/create_new_logins.rb
79
+ - lib/renuo/cli/commands/create_nine_cloudvm.rb
80
80
  - lib/renuo/cli/commands/create_pr.rb
81
81
  - lib/renuo/cli/commands/create_slidev_presentation.rb
82
82
  - lib/renuo/cli/commands/debug.rb
@@ -88,11 +88,16 @@ files:
88
88
  - lib/renuo/cli/commands/heroku_users.rb
89
89
  - lib/renuo/cli/commands/list_large_git_files.rb
90
90
  - lib/renuo/cli/commands/release.rb
91
+ - lib/renuo/cli/commands/setup_autocompletion.rb
91
92
  - lib/renuo/cli/commands/setup_uptimerobot.rb
92
93
  - lib/renuo/cli/commands/upgrade_laptop.rb
94
+ - lib/renuo/cli/credentials.yml.enc
93
95
  - lib/renuo/cli/helpers/command_helper.rb
96
+ - lib/renuo/cli/helpers/commander_introspector.rb
97
+ - lib/renuo/cli/helpers/credentials_helper.rb
94
98
  - lib/renuo/cli/helpers/environments.rb
95
99
  - lib/renuo/cli/helpers/renuo_version.rb
100
+ - lib/renuo/cli/helpers/zsh_completion_script.rb
96
101
  - lib/renuo/cli/services/cache.rb
97
102
  - lib/renuo/cli/services/cloudfront_config_service.rb
98
103
  - lib/renuo/cli/services/deploio.rb
@@ -106,6 +111,7 @@ files:
106
111
  - lib/renuo/cli/templates/semaphore/semaphore.yml.erb
107
112
  - lib/renuo/cli/templates/slidev/README.md.erb
108
113
  - lib/renuo/cli/templates/slidev/package.json.erb
114
+ - lib/renuo/cli/templates/zsh_completion.erb
109
115
  - lib/renuo/cli/version.rb
110
116
  homepage: https://github.com/renuo/renuo-cli
111
117
  licenses:
@@ -126,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
132
  - !ruby/object:Gem::Version
127
133
  version: '0'
128
134
  requirements: []
129
- rubygems_version: 4.0.8
135
+ rubygems_version: 4.0.15
130
136
  specification_version: 4
131
137
  summary: The Renuo CLI automates some common workflows.
132
138
  test_files: []
data/bin/renuo DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'renuo/cli'
4
- Renuo::Cli.new.start
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # :nocov:
4
- class Renuo::Cli::Commands::CheckDeploioStatus
5
- TIMEOUT_IN_SECONDS = 600
6
- INTERVAL_IN_SECONDS = 5
7
-
8
- command "check-deploio-status" do |c|
9
- c.syntax = "renuo check-deploio-status"
10
- c.summary = "DEPRECATED: Use 'renuo ci check-deploio-status' instead."
11
- c.description = "Checks the build and release status of the deployment."
12
- c.option "-a", "--app <name>", String, "The name of the app"
13
- c.option "-p", "--project <name>", String, "The name of the project"
14
- c.option "-g", "--git-revision <revision>", String, "The git revision to check"
15
- c.action { |_, options| new(options).run }
16
- end
17
-
18
- def initialize(options)
19
- @revision = options.git_revision || `git rev-parse HEAD`.strip
20
- @app = options.app || ENV.fetch("DEPLOIO_APP_NAME", nil)
21
- @project = options.project || ENV.fetch("DEPLOIO_PROJECT", nil)
22
-
23
- abort "missing app name" if @app.nil?
24
- abort "missing project name" if @project.nil?
25
- end
26
-
27
- def run
28
- warn "DEPRECATED: Use 'renuo ci check-deploio-status' instead."
29
- puts "(1/2) Checking build status for revision #{@revision}..."
30
- poll "build"
31
- abort "build check timed out after #{TIMEOUT_IN_SECONDS} seconds" if build.nil?
32
-
33
- puts "(2/2) Checking release status for build #{build_name}..."
34
- poll "release"
35
- abort "release check timed out after #{TIMEOUT_IN_SECONDS} seconds" if release.nil?
36
- end
37
-
38
- private
39
-
40
- def system!(cmd)
41
- abort unless system cmd
42
- end
43
-
44
- def fetch(type)
45
- command = "nctl get #{type} -a #{@app} -p #{@project} -o yaml"
46
- stdout, stderr, status = Open3.capture3 command
47
-
48
- abort "error fetching #{type} information: #{stderr}" unless status.success?
49
- Psych.load_stream stdout
50
- end
51
-
52
- def fetch_build_logs
53
- command = "nctl logs build -a #{@app} -p #{@project} -l 5000 --no-labels"
54
- stdout, stderr, status = Open3.capture3 command
55
-
56
- puts "error fetching build logs: #{stderr}" unless status.success?
57
- stdout
58
- end
59
-
60
- def build
61
- @build ||= fetch("builds").find do |build|
62
- build.dig("spec", "forProvider", "sourceConfig", "git", "revision") == @revision
63
- end
64
- end
65
-
66
- def build_name
67
- @build_name ||= build.dig("metadata", "name")
68
- end
69
-
70
- def build_status
71
- build.dig("status", "atProvider", "buildStatus")
72
- end
73
-
74
- def release
75
- @release ||= fetch("releases").find do |release|
76
- release.dig("spec", "forProvider", "build", "name") == build_name
77
- end
78
- end
79
-
80
- def release_status
81
- release.dig("status", "atProvider", "releaseStatus")
82
- end
83
-
84
- def succeeded?(status, type) # rubocop:disable Metrics/MethodLength
85
- time = "[#{Time.now.utc.iso8601}]"
86
-
87
- case status
88
- when "available", "success"
89
- puts "#{time} #{type} succeeded"
90
- true
91
- when "superseded"
92
- puts "#{time} release was superseded"
93
- true
94
- when "paused"
95
- abort "#{time} app is paused"
96
- when "error", "failed", "failure"
97
- puts fetch_build_logs
98
- abort "#{time} #{type} failed"
99
- else
100
- puts "#{time} #{type} status is #{status}, waiting..."
101
- instance_variable_set :"@#{type}", nil
102
- end
103
- end
104
-
105
- def poll(type)
106
- elapsed = 0
107
-
108
- while elapsed < TIMEOUT_IN_SECONDS
109
- if send type
110
- break if succeeded?(send(:"#{type}_status"), type)
111
- else
112
- puts "no matching #{type} found, waiting..."
113
- end
114
-
115
- sleep INTERVAL_IN_SECONDS
116
- elapsed += INTERVAL_IN_SECONDS
117
- end
118
- end
119
- end
120
- # :nocov: