capistrano-ops 1.0.11 → 1.2.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +18 -0
  3. data/.github/workflows/ci.yml +44 -0
  4. data/.github/workflows/release.yml +1 -1
  5. data/.gitignore +1 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +4 -5
  8. data/.rubocop_todo.yml +55 -0
  9. data/.ruby-version +1 -1
  10. data/CHANGELOG.md +48 -1
  11. data/README.md +101 -5
  12. data/Rakefile +5 -1
  13. data/bin/console +1 -1
  14. data/capistrano-ops.gemspec +10 -8
  15. data/lib/capistrano/ops/backup/helper.rb +4 -4
  16. data/lib/capistrano/ops/backup/tasks/database/create.rake +1 -0
  17. data/lib/capistrano/ops/backup/tasks/database/pull.rake +1 -0
  18. data/lib/capistrano/ops/backup/tasks/storage/create.rake +1 -0
  19. data/lib/capistrano/ops/backup/tasks/storage/pull.rake +1 -0
  20. data/lib/capistrano/ops/capistrano.rb +1 -0
  21. data/lib/capistrano/ops/figaro_yml/helpers.rb +3 -6
  22. data/lib/capistrano/ops/figaro_yml/tasks/check.rake +1 -0
  23. data/lib/capistrano/ops/figaro_yml/tasks/check_config_present.rake +1 -0
  24. data/lib/capistrano/ops/figaro_yml/tasks/check_figaro_file_exists.rake +1 -0
  25. data/lib/capistrano/ops/figaro_yml/tasks/check_git_tracking.rake +1 -0
  26. data/lib/capistrano/ops/figaro_yml/tasks/create_local.rake +1 -0
  27. data/lib/capistrano/ops/figaro_yml/tasks/figaro_yml_symlink.rake +1 -0
  28. data/lib/capistrano/ops/figaro_yml/tasks/get.rake +4 -3
  29. data/lib/capistrano/ops/figaro_yml/tasks/get_stage.rake +1 -0
  30. data/lib/capistrano/ops/figaro_yml/tasks/sort_local.rake +1 -0
  31. data/lib/capistrano/ops/figaro_yml.rb +2 -1
  32. data/lib/capistrano/ops/invoke/tasks/invoke.rake +1 -1
  33. data/lib/capistrano/ops/local/helpers.rb +71 -0
  34. data/lib/capistrano/ops/local/tasks/console.rake +59 -0
  35. data/lib/capistrano/ops/local/tasks/run.rake +40 -0
  36. data/lib/capistrano/ops/local.rb +9 -0
  37. data/lib/capistrano/ops/logrotate/paths.rb +2 -0
  38. data/lib/capistrano/ops/logrotate/tasks/disable.rake +1 -0
  39. data/lib/capistrano/ops/logrotate.rb +2 -1
  40. data/lib/capistrano/ops/rails/lib/backup/api.rb +1 -1
  41. data/lib/capistrano/ops/rails/lib/backup/s3.rb +9 -8
  42. data/lib/capistrano/ops/rails/lib/backup.rb +2 -0
  43. data/lib/capistrano/ops/rails/lib/notification/api.rb +1 -1
  44. data/lib/capistrano/ops/rails/lib/notification/slack.rb +4 -4
  45. data/lib/capistrano/ops/rails/lib/notification/webhook.rb +4 -4
  46. data/lib/capistrano/ops/rails/lib/tasks/pg/dump.rake +3 -3
  47. data/lib/capistrano/ops/rails/lib/tasks/pg/postgres_helper.rb +3 -3
  48. data/lib/capistrano/ops/rails/lib/tasks/pg/remove_old_dumps.rake +1 -1
  49. data/lib/capistrano/ops/rails/lib/tasks/storage/backup.rake +1 -1
  50. data/lib/capistrano/ops/rails/lib/tasks/storage/remove_old_backups.rake +2 -2
  51. data/lib/capistrano/ops/rails/lib/tasks/storage/storage_helper.rb +5 -5
  52. data/lib/capistrano/ops/version.rb +1 -1
  53. data/lib/capistrano/ops/whenever.rb +2 -1
  54. data/lib/capistrano/ops/wkhtmltopdf/tasks/setup.rake +1 -0
  55. data/spec/capistrano/ops/local/helpers_spec.rb +121 -0
  56. data/spec/capistrano/ops/version_spec.rb +9 -0
  57. data/spec/spec_helper.rb +20 -0
  58. metadata +63 -27
  59. data/.travis.yml +0 -13
  60. data/lib/capistrano/ops/capistrano/v3/tasks/backup.rake +0 -25
@@ -6,7 +6,7 @@ namespace :invoke do
6
6
 
7
7
  desc 'Execute a rake task on a remote server (cap invoke:rake TASK=db:migrate)'
8
8
  task :rake do
9
- task_name = ENV['TASK']
9
+ task_name = ENV.fetch('TASK', nil)
10
10
  unless task_name
11
11
  puts "\n\nFailed! You need to specify the 'TASK' parameter!",
12
12
  'Usage: cap <stage> invoke:rake TASK=your:task',
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Capistrano
6
+ module Ops
7
+ module Local
8
+ module Helpers
9
+ def scripts_dir
10
+ File.expand_path(fetch(:local_scripts_dir, 'scripts/local'), Dir.pwd)
11
+ end
12
+
13
+ def available_scripts
14
+ Dir.glob(File.join(scripts_dir, '*.rb')).map { |f| File.basename(f) }.sort
15
+ end
16
+
17
+ def resolve_script(name)
18
+ return nil if name.nil? || name.empty?
19
+
20
+ candidates = [name, "#{name}.rb"].map { |n| File.join(scripts_dir, n) }
21
+ candidates.find { |p| File.exist?(p) }
22
+ end
23
+
24
+ def print_available_scripts
25
+ scripts = available_scripts
26
+ if scripts.empty?
27
+ puts " (no scripts found in #{fetch(:local_scripts_dir, 'scripts/local')}/)"
28
+ else
29
+ scripts.each { |s| puts " • #{s}" }
30
+ end
31
+ end
32
+
33
+ def abort_script_not_given(command)
34
+ puts "\n#{color('Usage:', :bold)} cap <stage> local:#{command} SCRIPT=<filename>"
35
+ puts "\n#{color('Available scripts:', :bold)}"
36
+ print_available_scripts
37
+ abort
38
+ end
39
+
40
+ def abort_script_not_found(name)
41
+ puts "\n#{color("Script not found: #{name}", :red)}"
42
+ puts "\n#{color('Available scripts:', :bold)}"
43
+ print_available_scripts
44
+ abort
45
+ end
46
+
47
+ def confirm_dangerous_stage!(action)
48
+ stage = fetch(:stage).to_s
49
+ dangerous = fetch(:local_dangerous_stages, %w[production])
50
+ return unless dangerous.include?(stage)
51
+
52
+ print "\n#{color("⚠ #{action} on #{stage.upcase}. Continue? [y/N] ", :yellow)}"
53
+ abort 'Aborted.' unless $stdin.gets.to_s.strip.casecmp('y').zero?
54
+ end
55
+
56
+ def remote_tmp_path(script_path)
57
+ "/tmp/cap_local_#{SecureRandom.hex(6)}_#{File.basename(script_path)}"
58
+ end
59
+
60
+ def color(text, style)
61
+ return text unless $stdout.tty?
62
+
63
+ code = { bold: '1', red: '31', green: '32', yellow: '33', cyan: '36', dim: '2' }[style]
64
+ return text unless code
65
+
66
+ "\e[#{code}m#{text}\e[0m"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :local do
4
+ include Capistrano::Ops::Local::Helpers
5
+
6
+ desc 'Rails console with a local script pre-loaded: cap <stage> local:console SCRIPT=<filename>'
7
+ task :console do
8
+ script = ENV.fetch('SCRIPT', nil)
9
+ abort_script_not_given('console') unless script
10
+
11
+ script_path = resolve_script(script)
12
+ abort_script_not_found(script) unless script_path
13
+
14
+ stage = fetch(:stage).to_s
15
+ puts "\n#{color('Script:', :bold)} #{File.basename(script_path)}"
16
+ puts "#{color('Target:', :bold)} #{stage} (interactive console)"
17
+
18
+ confirm_dangerous_stage!('Opening console')
19
+
20
+ remote_tmp = remote_tmp_path(script_path)
21
+ remote_irbrc = "/tmp/cap_local_irbrc_#{SecureRandom.hex(6)}.rb"
22
+
23
+ on primary(fetch(:console_role, :app)) do
24
+ upload! script_path, remote_tmp
25
+ # Wrapper loads the script into the console's main context and deletes
26
+ # both temp files server-side when IRB exits. The at_exit puts often
27
+ # gets swallowed by Sentry/etc during shutdown; the local puts in the
28
+ # ensure block below is what the user actually sees.
29
+ wrapper = <<~RUBY
30
+ load '#{remote_tmp}'
31
+ at_exit do
32
+ begin; File.delete('#{remote_tmp}'); rescue StandardError; end
33
+ begin; File.delete('#{remote_irbrc}'); rescue StandardError; end
34
+ end
35
+ RUBY
36
+ upload! StringIO.new(wrapper), remote_irbrc
37
+ end
38
+
39
+ puts "#{color("▶ Opening console with #{File.basename(script_path)} on #{stage}…", :cyan)}\n\n"
40
+
41
+ begin
42
+ run_interactively primary(fetch(:console_role, :app)), shell: fetch(:console_shell, nil) do
43
+ within current_path do
44
+ as user: fetch(:console_user, nil) do
45
+ args = ['-e', fetch(:console_env, fetch(:rails_env, fetch(:stage)))]
46
+ args.push('--', '-r', remote_irbrc)
47
+ execute(:rails, :console, *args)
48
+ end
49
+ end
50
+ end
51
+ ensure
52
+ # Local puts — no `on {}` block, since Capistrano's Configuration is not
53
+ # safely Marshal-dumpable after run_interactively (fails with
54
+ # "no _dump_data is defined for class Thread::Mutex"). Server-side
55
+ # cleanup is handled by the wrapper's at_exit hook.
56
+ puts color("\n\u{1F9F9} Console closed on #{stage} — server temp files cleaned up on exit.", :dim)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :local do
4
+ include Capistrano::Ops::Local::Helpers
5
+
6
+ desc 'Run a local script on remote via rails runner: cap <stage> local:run SCRIPT=<filename>'
7
+ task :run do
8
+ script = ENV.fetch('SCRIPT', nil)
9
+ abort_script_not_given('run') unless script
10
+
11
+ script_path = resolve_script(script)
12
+ abort_script_not_found(script) unless script_path
13
+
14
+ stage = fetch(:stage).to_s
15
+ puts "\n#{color('Script:', :bold)} #{File.basename(script_path)}"
16
+ puts "#{color('Target:', :bold)} #{stage}"
17
+
18
+ confirm_dangerous_stage!('Running script')
19
+
20
+ on roles(fetch(:console_role, :app)) do
21
+ remote_tmp = remote_tmp_path(script_path)
22
+
23
+ begin
24
+ upload! script_path, remote_tmp
25
+ puts "#{color("▶ Running #{File.basename(script_path)} on #{stage}…", :cyan)}\n\n"
26
+
27
+ within current_path do
28
+ with rails_env: fetch(:console_env, fetch(:rails_env, fetch(:stage))) do
29
+ execute :bundle, :exec, :rails, 'runner', remote_tmp
30
+ end
31
+ end
32
+ ensure
33
+ execute :rm, '-f', remote_tmp
34
+ puts color("\u{1F9F9} Cleaned up temp file on server", :dim)
35
+ end
36
+ end
37
+
38
+ puts "\n#{color('✔ Done.', :green)}"
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capistrano/ops/task_loader'
4
+ require 'capistrano/ops/local/helpers'
5
+
6
+ TaskLoader.load_tasks_if_gem_present(
7
+ 'capistrano-rails', 'local/tasks',
8
+ 'WARNING: Gemfile does not include capistrano-rails gem which is required for local:console (provides capistrano/rails/console)'
9
+ )
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
4
 
3
5
  module Capistrano
@@ -3,6 +3,7 @@
3
3
  namespace :logrotate do
4
4
  include Capistrano::Ops::Logrotate::Paths
5
5
  include Capistrano::Ops::Logrotate::Helpers
6
+
6
7
  desc 'disable logrotate'
7
8
  task :disable do
8
9
  on roles(:app) do
@@ -4,4 +4,5 @@ require 'capistrano/ops/task_loader'
4
4
  require 'capistrano/ops/logrotate/helpers'
5
5
  require 'capistrano/ops/logrotate/paths'
6
6
 
7
- TaskLoader.load_tasks_if_gem_present('whenever', 'logrotate/tasks', 'WARNING: Gemfile does not include whenever gem which is required for logrotate tasks')
7
+ TaskLoader.load_tasks_if_gem_present('whenever', 'logrotate/tasks',
8
+ 'WARNING: Gemfile does not include whenever gem which is required for logrotate tasks')
@@ -4,7 +4,7 @@ module Backup
4
4
  class Api
5
5
  attr_accessor :backup_provider, :provider_config
6
6
 
7
- def initialize(provider: ENV['BACKUP_PROVIDER'], provider_config: {})
7
+ def initialize(provider: ENV.fetch('BACKUP_PROVIDER', nil), provider_config: {})
8
8
  self.backup_provider = provider
9
9
  self.provider_config = provider_config
10
10
  end
@@ -12,8 +12,9 @@ module Backup
12
12
 
13
13
  attr_accessor :endpoint, :region, :access_key_id, :secret_access_key, :s3_resource, :s3_client
14
14
 
15
- def initialize(endpoint: ENV['S3_BACKUP_ENDPOINT'], region: ENV['S3_BACKUP_REGION'], access_key_id: ENV['S3_BACKUP_KEY'],
16
- secret_access_key: ENV['S3_BACKUP_SECRET'])
15
+ def initialize(endpoint: ENV.fetch('S3_BACKUP_ENDPOINT',
16
+ nil), region: ENV.fetch('S3_BACKUP_REGION', nil), access_key_id: ENV.fetch('S3_BACKUP_KEY', nil),
17
+ secret_access_key: ENV.fetch('S3_BACKUP_SECRET', nil))
17
18
  self.endpoint = endpoint
18
19
  self.region = region
19
20
  self.access_key_id = access_key_id
@@ -31,7 +32,7 @@ module Backup
31
32
 
32
33
  def upload(backup_file, key)
33
34
  begin
34
- s3_resource.bucket(ENV['S3_BACKUP_BUCKET']).object(key).upload_file(backup_file)
35
+ s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).object(key).upload_file(backup_file)
35
36
  rescue Backup::Error => e
36
37
  puts "Upload failed: #{e.message}"
37
38
  raise e
@@ -49,7 +50,7 @@ module Backup
49
50
 
50
51
  # rubocop:disable Metrics/MethodLength
51
52
  def upload_file_as_stream(file_path, key)
52
- bucket = ENV['S3_BACKUP_BUCKET']
53
+ bucket = ENV.fetch('S3_BACKUP_BUCKET', nil)
53
54
  # Calculate total size of the file to be uploaded
54
55
  total_size = File.size(file_path)
55
56
  chunk_size = calculate_chunk_size(total_size)
@@ -130,7 +131,7 @@ module Backup
130
131
  end
131
132
 
132
133
  def upload_folder_as_tar_gz_stream(folder_path, key)
133
- bucket = ENV['S3_BACKUP_BUCKET']
134
+ bucket = ENV.fetch('S3_BACKUP_BUCKET', nil)
134
135
 
135
136
  # Calculate total size of the files to be uploaded
136
137
  total_size = calculate_total_size(folder_path)
@@ -256,7 +257,7 @@ module Backup
256
257
  end
257
258
 
258
259
  def remove_old_backups(basename, keep: 5)
259
- all_items = s3_resource.bucket(ENV['S3_BACKUP_BUCKET']).objects(prefix: basename).map do |item|
260
+ all_items = s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).objects(prefix: basename).map do |item|
260
261
  { key: item.key, last_modified: item.last_modified }
261
262
  end
262
263
 
@@ -272,7 +273,7 @@ module Backup
272
273
  puts "Removing #{month} from S3"
273
274
  delete_items.each do |item_obj|
274
275
  puts "Removing #{item_obj[:key]} from S3"
275
- s3_resource.bucket(ENV['S3_BACKUP_BUCKET']).object(item_obj[:key]).delete
276
+ s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).object(item_obj[:key]).delete
276
277
  end
277
278
  end
278
279
  puts 'Old months removed from S3'
@@ -286,7 +287,7 @@ module Backup
286
287
 
287
288
  current_month_delete_items.each do |item_obj|
288
289
  puts "Removing #{item_obj[:key]} from S3"
289
- s3_resource.bucket(ENV['S3_BACKUP_BUCKET']).object(item_obj[:key]).delete
290
+ s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).object(item_obj[:key]).delete
290
291
  end
291
292
  puts 'Old backups removed from S3'
292
293
  rescue Backup::Error => e
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'capistrano/ops/rails/lib/backup/api'
2
4
  require 'capistrano/ops/rails/lib/backup/s3'
3
5
 
@@ -4,7 +4,7 @@ module Notification
4
4
  class Api
5
5
  attr_accessor :notification_type, :notification_level
6
6
 
7
- def initialize(notification_type: ENV['NOTIFICATION_TYPE'], notification_level: ENV['NOTIFICATION_LEVEL'])
7
+ def initialize(notification_type: ENV.fetch('NOTIFICATION_TYPE', nil), notification_level: ENV.fetch('NOTIFICATION_LEVEL', nil))
8
8
  self.notification_type = notification_type
9
9
  self.notification_level = notification_level || 'error'
10
10
  end
@@ -6,8 +6,8 @@ require 'json'
6
6
  module Notification
7
7
  class Slack
8
8
  def initialize
9
- @slack_secret = ENV['SLACK_SECRET']
10
- @slack_channel = ENV['SLACK_CHANNEL']
9
+ @slack_secret = ENV.fetch('SLACK_SECRET', nil)
10
+ @slack_channel = ENV.fetch('SLACK_CHANNEL', nil)
11
11
  @conn = Faraday.new(url: 'https://slack.com/api/') do |faraday|
12
12
  faraday.headers['Content-Type'] = 'application/json'
13
13
  faraday.headers['Authorization'] = "Bearer #{@slack_secret}"
@@ -20,8 +20,8 @@ module Notification
20
20
  begin
21
21
  res = @conn.post('chat.postMessage') do |req|
22
22
  req.body = {
23
- "channel": @slack_channel,
24
- "text": message
23
+ channel: @slack_channel,
24
+ text: message
25
25
  }.to_json
26
26
  end
27
27
  response = JSON.parse(res.body)
@@ -7,18 +7,18 @@ module Notification
7
7
  require 'json'
8
8
 
9
9
  def initialize
10
- @webhook_url = ENV['WEBHOOK_URL']
11
- @secret = ENV['WEBHOOK_SECRET']
10
+ @webhook_url = ENV.fetch('WEBHOOK_URL', nil)
11
+ @secret = ENV.fetch('WEBHOOK_SECRET', nil)
12
12
  @conn = Faraday.new(url: @webhook_url) do |faraday|
13
13
  faraday.headers['Content-Type'] = 'application/json'
14
14
  end
15
15
  end
16
16
 
17
17
  def generate_signature(payload_body)
18
- "md5=#{OpenSSL::HMAC.hexdigest('md5', ENV['WEBHOOK_SECRET'], payload_body)}"
18
+ "md5=#{OpenSSL::HMAC.hexdigest('md5', ENV.fetch('WEBHOOK_SECRET', nil), payload_body)}"
19
19
  end
20
20
 
21
- def backup_notification(result, webhook_data, _notification_level)
21
+ def backup_notification(result, webhook_data, notification_level)
22
22
  return if @webhook_url.nil? || @secret.nil?
23
23
  return if result && notification_level == 'error'
24
24
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './postgres_helper'
3
+ require_relative 'postgres_helper'
4
4
  namespace :pg do
5
5
  include PostgresHelper
6
6
 
@@ -25,11 +25,11 @@ namespace :pg do
25
25
  result = system(commandlist)
26
26
 
27
27
  if ENV['BACKUP_PROVIDER'].present? && external_backup && result
28
- puts "Uploading #{filename} to #{ENV['BACKUP_PROVIDER']}..."
28
+ puts "Uploading #{filename} to #{ENV.fetch('BACKUP_PROVIDER', nil)}..."
29
29
  provider = Backup::Api.new
30
30
  begin
31
31
  provider.upload("#{backup_path}/#{filename}", filename.to_s, 'file')
32
- puts "#{filename} uploaded to #{ENV['BACKUP_PROVIDER']}"
32
+ puts "#{filename} uploaded to #{ENV.fetch('BACKUP_PROVIDER', nil)}"
33
33
  rescue StandardError => e
34
34
  puts "#{filename} upload failed: #{e.message}"
35
35
  end
@@ -27,7 +27,7 @@ module PostgresHelper
27
27
  messages = []
28
28
  if result
29
29
  messages << "Backup of #{database} successfully finished at #{Time.now}"
30
- messages << "Backup path:\`#{backup_path}/#{filename}\`"
30
+ messages << "Backup path:`#{backup_path}/#{filename}`"
31
31
  else
32
32
  messages << "Backup of #{database} failed at #{Time.now}"
33
33
  end
@@ -54,14 +54,14 @@ module PostgresHelper
54
54
  commandlist = []
55
55
  commandlist << "export PGPASSWORD='#{password}'"
56
56
  commandlist << "cd #{backup_path}"
57
- commandlist << "pg_dump --no-acl --no-owner #{options.join('')} > #{filename}"
57
+ commandlist << "pg_dump --no-acl --no-owner #{options.join} > #{filename}"
58
58
  commandlist.join(' && ')
59
59
  end
60
60
 
61
61
  def size_str(size)
62
62
  units = %w[B KB MB GB TB]
63
63
  e = (Math.log(size) / Math.log(1024)).floor
64
- s = format('%.2f', size.to_f / 1024**e)
64
+ s = format('%.2f', size.to_f / (1024**e))
65
65
  s.sub(/\.?0*$/, units[e])
66
66
  end
67
67
  end
@@ -12,7 +12,7 @@ namespace :pg do
12
12
  external_backup = Rails.env.production? || ENV['EXTERNAL_BACKUP_ENABLED'] == 'true'
13
13
 
14
14
  task :remove_old_dumps do
15
- bash_regex = "'#{@database}.{0,}\.dump'"
15
+ bash_regex = "'#{@database}.{0,}.dump'"
16
16
 
17
17
  unless backups_enabled
18
18
  puts 'remove_old_dumps: Backups are disabled'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './storage_helper'
3
+ require_relative 'storage_helper'
4
4
  namespace :storage do
5
5
  include StorageHelper
6
6
 
@@ -14,7 +14,7 @@ namespace :storage do
14
14
  @total_external_backups_no = (@env_external_no || ENV['NUMBER_OF_BACKUPS'] || 7).to_i
15
15
  desc 'remove old storage backups'
16
16
  task :remove_old_backups do
17
- bash_regex = "'storage_.{0,}\.tar.gz'"
17
+ bash_regex = "'storage_.{0,}.tar.gz'"
18
18
 
19
19
  unless backups_enabled
20
20
  puts 'remove_old_backups: Backups are disabled'
@@ -37,7 +37,7 @@ namespace :storage do
37
37
  'xargs rm -rf'
38
38
  ]
39
39
 
40
- result = system(commandlist.join(' | ')) if @total_local_backups_no.positive? && local_backup || !local_backup && external_backup
40
+ result = system(commandlist.join(' | ')) if (@total_local_backups_no.positive? && local_backup) || (!local_backup && external_backup)
41
41
  puts 'remove_old_backups: local cleanup finished' if result
42
42
 
43
43
  if ENV['BACKUP_PROVIDER'].present? && external_backup
@@ -9,7 +9,7 @@ module StorageHelper
9
9
  backups_enabled: env_or_production('BACKUPS_ENABLED'),
10
10
  external_backup: env_or_production('EXTERNAL_BACKUP_ENABLED'),
11
11
  keep_local_backups: env_or_production('KEEP_LOCAL_STORAGE_BACKUPS'),
12
- backup_provider: ENV['BACKUP_PROVIDER']
12
+ backup_provider: ENV.fetch('BACKUP_PROVIDER', nil)
13
13
  }
14
14
  end
15
15
 
@@ -23,7 +23,7 @@ module StorageHelper
23
23
  messages = []
24
24
  if result
25
25
  messages << "Backup of storage folder successfully finished at #{Time.now}"
26
- messages << "Backup path:\`#{@backup_path}/#{@filename}\`"
26
+ messages << "Backup path:`#{@backup_path}/#{@filename}`"
27
27
  else
28
28
  messages << "Backup of storage folder failed at #{Time.now}"
29
29
  end
@@ -34,19 +34,19 @@ module StorageHelper
34
34
  @backup_path = settings[:backup_path]
35
35
  @date = Time.now.to_i
36
36
  @filename = "storage_#{@date}.tar.gz"
37
- FileUtils.mkdir_p(@backup_path) unless Dir.exist?(@backup_path)
37
+ FileUtils.mkdir_p(@backup_path)
38
38
  "tar -zcf #{@backup_path}/#{@filename} -C #{settings[:storage_path]} ."
39
39
  end
40
40
 
41
41
  def size_str(size)
42
42
  units = %w[B KB MB GB TB]
43
43
  e = (Math.log(size) / Math.log(1024)).floor
44
- s = format('%.2f', size.to_f / 1024**e)
44
+ s = format('%.2f', size.to_f / (1024**e))
45
45
  s.sub(/\.?0*$/, units[e])
46
46
  end
47
47
 
48
48
  def create_local_backup(filename, storage_path, backup_path)
49
- FileUtils.mkdir_p(backup_path) unless Dir.exist?(backup_path)
49
+ FileUtils.mkdir_p(backup_path)
50
50
  response = system(backup_cmd(backup_path: backup_path, storage_path: storage_path))
51
51
  FileUtils.rm_rf("#{@backup_path}/#{filename}") unless response
52
52
  puts response ? "Backup created: #{backup_path}/#{@filename} (#{size_str(File.size("#{@backup_path}/#{@filename}"))})" : 'Backup failed removing dump file'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capistrano
4
4
  module Ops
5
- VERSION = '1.0.11'
5
+ VERSION = '1.2.0'
6
6
  end
7
7
  end
@@ -2,4 +2,5 @@
2
2
 
3
3
  require 'capistrano/ops/task_loader'
4
4
 
5
- TaskLoader.load_tasks_if_gem_present('whenever', 'whenever/tasks', 'WARNING: Gemfile does not include whenever gem which is required for whenever tasks')
5
+ TaskLoader.load_tasks_if_gem_present('whenever', 'whenever/tasks',
6
+ 'WARNING: Gemfile does not include whenever gem which is required for whenever tasks')
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace :wkhtmltopdf do
4
4
  include Capistrano::Ops::Wkhtmltopdf::Helpers
5
+
5
6
  after 'deploy:symlink:release', 'wkhtmltopdf:setup'
6
7
 
7
8
  desc 'unzip wkhtmltopdf if necessary'
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+ require 'capistrano/ops/local/helpers'
6
+
7
+ RSpec.describe Capistrano::Ops::Local::Helpers do
8
+ let(:host) do
9
+ Class.new do
10
+ include Capistrano::Ops::Local::Helpers
11
+
12
+ def initialize(scripts_dir_override)
13
+ @scripts_dir_override = scripts_dir_override
14
+ end
15
+
16
+ def fetch(key, default = nil)
17
+ return @scripts_dir_override if key == :local_scripts_dir
18
+
19
+ default
20
+ end
21
+ end
22
+ end
23
+
24
+ around do |example|
25
+ Dir.mktmpdir do |dir|
26
+ @tmp = dir
27
+ Dir.chdir(dir) { example.run }
28
+ end
29
+ end
30
+
31
+ def write_script(name)
32
+ dir = File.join(@tmp, 'ops', 'scripts')
33
+ FileUtils.mkdir_p(dir)
34
+ File.write(File.join(dir, name), '# example')
35
+ dir
36
+ end
37
+
38
+ describe '#available_scripts' do
39
+ it 'returns sorted basenames of .rb files' do
40
+ write_script('b.rb')
41
+ write_script('a.rb')
42
+ write_script('note.txt')
43
+
44
+ instance = host.new('ops/scripts')
45
+
46
+ expect(instance.available_scripts).to eq(%w[a.rb b.rb])
47
+ end
48
+
49
+ it 'is empty when the directory does not exist' do
50
+ instance = host.new('does/not/exist')
51
+
52
+ expect(instance.available_scripts).to eq([])
53
+ end
54
+ end
55
+
56
+ describe '#resolve_script' do
57
+ it 'returns the full path when the exact filename exists' do
58
+ write_script('foo.rb')
59
+
60
+ instance = host.new('ops/scripts')
61
+
62
+ expect(instance.resolve_script('foo.rb')).to end_with('/ops/scripts/foo.rb')
63
+ end
64
+
65
+ it 'auto-appends .rb when omitted' do
66
+ write_script('foo.rb')
67
+
68
+ instance = host.new('ops/scripts')
69
+
70
+ expect(instance.resolve_script('foo')).to end_with('/ops/scripts/foo.rb')
71
+ end
72
+
73
+ it 'returns nil for unknown names' do
74
+ write_script('foo.rb')
75
+
76
+ instance = host.new('ops/scripts')
77
+
78
+ expect(instance.resolve_script('bar')).to be_nil
79
+ end
80
+
81
+ it 'returns nil for empty input' do
82
+ instance = host.new('ops/scripts')
83
+
84
+ expect(instance.resolve_script(nil)).to be_nil
85
+ expect(instance.resolve_script('')).to be_nil
86
+ end
87
+ end
88
+
89
+ describe '#remote_tmp_path' do
90
+ it 'produces a unique /tmp path preserving the basename' do
91
+ instance = host.new('ops/scripts')
92
+
93
+ path = instance.remote_tmp_path('/some/where/foo.rb')
94
+
95
+ expect(path).to match(%r{\A/tmp/cap_local_[0-9a-f]{12}_foo\.rb\z})
96
+ end
97
+ end
98
+
99
+ describe '#color' do
100
+ it 'returns plain text when stdout is not a TTY' do
101
+ instance = host.new('ops/scripts')
102
+ allow($stdout).to receive(:tty?).and_return(false)
103
+
104
+ expect(instance.color('hello', :red)).to eq('hello')
105
+ end
106
+
107
+ it 'wraps text in ANSI escape when stdout is a TTY and style is known' do
108
+ instance = host.new('ops/scripts')
109
+ allow($stdout).to receive(:tty?).and_return(true)
110
+
111
+ expect(instance.color('hello', :red)).to eq("\e[31mhello\e[0m")
112
+ end
113
+
114
+ it 'passes text through unchanged for unknown styles' do
115
+ instance = host.new('ops/scripts')
116
+ allow($stdout).to receive(:tty?).and_return(true)
117
+
118
+ expect(instance.color('hello', :magenta)).to eq('hello')
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Capistrano::Ops do
6
+ it 'has a version number' do
7
+ expect(Capistrano::Ops::VERSION).to match(/\A\d+\.\d+\.\d+\z/)
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capistrano/ops/version'
4
+
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |expectations|
7
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
8
+ end
9
+
10
+ config.mock_with :rspec do |mocks|
11
+ mocks.verify_partial_doubles = true
12
+ end
13
+
14
+ config.shared_context_metadata_behavior = :apply_to_host_groups
15
+ config.disable_monkey_patching!
16
+ config.warnings = false
17
+ config.default_formatter = 'doc' if config.files_to_run.one?
18
+ config.order = :random
19
+ Kernel.srand config.seed
20
+ end