bard 2.0.0.beta → 2.0.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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -1
  3. data/CLAUDE.md +76 -0
  4. data/MIGRATION_GUIDE.md +24 -9
  5. data/PLUGINS.md +99 -0
  6. data/README.md +14 -6
  7. data/Rakefile +3 -1
  8. data/bard.gemspec +2 -1
  9. data/cucumber.yml +1 -0
  10. data/features/ci.feature +63 -0
  11. data/features/data.feature +13 -0
  12. data/features/deploy.feature +14 -0
  13. data/features/deploy_git_workflow.feature +89 -0
  14. data/features/run.feature +14 -0
  15. data/features/step_definitions/bard_steps.rb +136 -0
  16. data/features/support/bard-coverage +16 -0
  17. data/features/support/env.rb +14 -39
  18. data/features/support/test_server.rb +216 -0
  19. data/lib/bard/cli.rb +14 -31
  20. data/lib/bard/command.rb +10 -69
  21. data/lib/bard/config.rb +40 -183
  22. data/lib/bard/copy.rb +28 -103
  23. data/lib/bard/plugins/data.rb +56 -0
  24. data/lib/bard/{ci → plugins/deploy/ci}/github_actions.rb +3 -4
  25. data/lib/bard/plugins/deploy/ci/jenkins.rb +176 -0
  26. data/lib/bard/{ci → plugins/deploy/ci}/local.rb +7 -7
  27. data/lib/bard/{ci → plugins/deploy/ci}/runner.rb +38 -4
  28. data/lib/bard/plugins/deploy/ci.rb +38 -0
  29. data/lib/bard/plugins/deploy/ssh_strategy.rb +27 -0
  30. data/lib/bard/{deploy_strategy.rb → plugins/deploy/strategy.rb} +1 -1
  31. data/lib/bard/plugins/deploy.rb +240 -0
  32. data/lib/bard/{git.rb → plugins/git.rb} +6 -3
  33. data/lib/bard/{github.rb → plugins/github.rb} +4 -6
  34. data/lib/bard/{deploy_strategy/github_pages.rb → plugins/github_pages/strategy.rb} +13 -6
  35. data/lib/bard/plugins/github_pages.rb +30 -0
  36. data/lib/bard/plugins/hurt.rb +13 -0
  37. data/{install_files → lib/bard/plugins/install}/.github/dependabot.yml +2 -1
  38. data/{install_files → lib/bard/plugins/install}/.github/workflows/cache-ci.yml +1 -1
  39. data/{install_files → lib/bard/plugins/install}/.github/workflows/ci.yml +2 -2
  40. data/lib/bard/plugins/install.rb +9 -0
  41. data/lib/bard/plugins/open.rb +20 -0
  42. data/lib/bard/{ping.rb → plugins/ping/check.rb} +4 -4
  43. data/lib/bard/plugins/ping/target_methods.rb +23 -0
  44. data/lib/bard/plugins/ping.rb +10 -0
  45. data/lib/bard/plugins/run.rb +19 -0
  46. data/lib/bard/plugins/setup.rb +54 -0
  47. data/lib/bard/plugins/ssh/connection.rb +75 -0
  48. data/lib/bard/plugins/ssh/copy.rb +95 -0
  49. data/lib/bard/{ssh_server.rb → plugins/ssh/server.rb} +17 -42
  50. data/lib/bard/plugins/ssh/target_methods.rb +20 -0
  51. data/lib/bard/plugins/ssh.rb +10 -0
  52. data/lib/bard/plugins/url/target_methods.rb +23 -0
  53. data/lib/bard/plugins/url.rb +1 -0
  54. data/lib/bard/plugins/vim.rb +6 -0
  55. data/lib/bard/retryable.rb +25 -0
  56. data/lib/bard/secrets.rb +10 -0
  57. data/lib/bard/target.rb +27 -185
  58. data/lib/bard/version.rb +1 -1
  59. data/lib/bard.rb +1 -3
  60. data/spec/acceptance/docker/Dockerfile +3 -2
  61. data/spec/bard/capability_spec.rb +8 -50
  62. data/spec/bard/ci/github_actions_spec.rb +117 -14
  63. data/spec/bard/ci/jenkins_spec.rb +139 -0
  64. data/spec/bard/ci/runner_spec.rb +61 -0
  65. data/spec/bard/ci_spec.rb +1 -1
  66. data/spec/bard/cli/ci_spec.rb +34 -27
  67. data/spec/bard/cli/data_spec.rb +7 -26
  68. data/spec/bard/cli/deploy_spec.rb +87 -46
  69. data/spec/bard/cli/hurt_spec.rb +3 -9
  70. data/spec/bard/cli/install_spec.rb +5 -11
  71. data/spec/bard/cli/master_key_spec.rb +5 -19
  72. data/spec/bard/cli/open_spec.rb +14 -30
  73. data/spec/bard/cli/ping_spec.rb +8 -23
  74. data/spec/bard/cli/run_spec.rb +27 -21
  75. data/spec/bard/cli/setup_spec.rb +10 -27
  76. data/spec/bard/cli/ssh_spec.rb +10 -25
  77. data/spec/bard/cli/stage_spec.rb +28 -23
  78. data/spec/bard/cli/vim_spec.rb +3 -9
  79. data/spec/bard/command_spec.rb +1 -8
  80. data/spec/bard/config_spec.rb +78 -98
  81. data/spec/bard/copy_spec.rb +54 -18
  82. data/spec/bard/deploy_strategy/ssh_spec.rb +65 -7
  83. data/spec/bard/deploy_strategy_spec.rb +1 -1
  84. data/spec/bard/dynamic_dsl_spec.rb +18 -98
  85. data/spec/bard/git_spec.rb +9 -5
  86. data/spec/bard/github_spec.rb +2 -2
  87. data/spec/bard/ping_spec.rb +5 -5
  88. data/spec/bard/ssh_copy_spec.rb +44 -0
  89. data/spec/bard/ssh_server_spec.rb +8 -101
  90. data/spec/bard/target_spec.rb +66 -109
  91. data/spec/spec_helper.rb +6 -1
  92. metadata +79 -143
  93. data/README.rdoc +0 -15
  94. data/features/bard_check.feature +0 -94
  95. data/features/bard_deploy.feature +0 -18
  96. data/features/bard_pull.feature +0 -112
  97. data/features/bard_push.feature +0 -112
  98. data/features/podman_testcontainers.feature +0 -16
  99. data/features/step_definitions/check_steps.rb +0 -47
  100. data/features/step_definitions/git_steps.rb +0 -73
  101. data/features/step_definitions/global_steps.rb +0 -56
  102. data/features/step_definitions/podman_steps.rb +0 -23
  103. data/features/step_definitions/rails_steps.rb +0 -44
  104. data/features/step_definitions/submodule_steps.rb +0 -110
  105. data/features/support/grit_ext.rb +0 -13
  106. data/features/support/io.rb +0 -32
  107. data/features/support/podman.rb +0 -153
  108. data/lib/bard/ci/jenkins.rb +0 -105
  109. data/lib/bard/ci/retryable.rb +0 -27
  110. data/lib/bard/ci.rb +0 -50
  111. data/lib/bard/cli/ci.rb +0 -66
  112. data/lib/bard/cli/command.rb +0 -26
  113. data/lib/bard/cli/data.rb +0 -45
  114. data/lib/bard/cli/deploy.rb +0 -85
  115. data/lib/bard/cli/hurt.rb +0 -20
  116. data/lib/bard/cli/install.rb +0 -16
  117. data/lib/bard/cli/master_key.rb +0 -17
  118. data/lib/bard/cli/new.rb +0 -101
  119. data/lib/bard/cli/new_rails_template.rb +0 -197
  120. data/lib/bard/cli/open.rb +0 -22
  121. data/lib/bard/cli/ping.rb +0 -18
  122. data/lib/bard/cli/provision.rb +0 -34
  123. data/lib/bard/cli/run.rb +0 -24
  124. data/lib/bard/cli/setup.rb +0 -56
  125. data/lib/bard/cli/ssh.rb +0 -14
  126. data/lib/bard/cli/stage.rb +0 -27
  127. data/lib/bard/cli/vim.rb +0 -13
  128. data/lib/bard/default_config.rb +0 -35
  129. data/lib/bard/deploy_strategy/ssh.rb +0 -19
  130. data/lib/bard/github_pages.rb +0 -134
  131. data/lib/bard/provision/app.rb +0 -10
  132. data/lib/bard/provision/apt.rb +0 -16
  133. data/lib/bard/provision/authorizedkeys.rb +0 -25
  134. data/lib/bard/provision/data.rb +0 -27
  135. data/lib/bard/provision/deploy.rb +0 -10
  136. data/lib/bard/provision/http.rb +0 -16
  137. data/lib/bard/provision/logrotation.rb +0 -30
  138. data/lib/bard/provision/masterkey.rb +0 -18
  139. data/lib/bard/provision/mysql.rb +0 -22
  140. data/lib/bard/provision/passenger.rb +0 -37
  141. data/lib/bard/provision/repo.rb +0 -72
  142. data/lib/bard/provision/rvm.rb +0 -22
  143. data/lib/bard/provision/ssh.rb +0 -72
  144. data/lib/bard/provision/swapfile.rb +0 -21
  145. data/lib/bard/provision/user.rb +0 -42
  146. data/lib/bard/provision.rb +0 -16
  147. data/lib/bard/server.rb +0 -117
  148. data/spec/bard/cli/command_spec.rb +0 -50
  149. data/spec/bard/cli/new_spec.rb +0 -73
  150. data/spec/bard/cli/provision_spec.rb +0 -42
  151. data/spec/bard/github_pages_spec.rb +0 -143
  152. data/spec/bard/provision/app_spec.rb +0 -33
  153. data/spec/bard/provision/apt_spec.rb +0 -39
  154. data/spec/bard/provision/authorizedkeys_spec.rb +0 -40
  155. data/spec/bard/provision/data_spec.rb +0 -54
  156. data/spec/bard/provision/deploy_spec.rb +0 -33
  157. data/spec/bard/provision/http_spec.rb +0 -57
  158. data/spec/bard/provision/logrotation_spec.rb +0 -34
  159. data/spec/bard/provision/masterkey_spec.rb +0 -63
  160. data/spec/bard/provision/mysql_spec.rb +0 -55
  161. data/spec/bard/provision/passenger_spec.rb +0 -81
  162. data/spec/bard/provision/repo_spec.rb +0 -208
  163. data/spec/bard/provision/rvm_spec.rb +0 -49
  164. data/spec/bard/provision/ssh_spec.rb +0 -229
  165. data/spec/bard/provision/swapfile_spec.rb +0 -32
  166. data/spec/bard/provision/user_spec.rb +0 -103
  167. data/spec/bard/provision_spec.rb +0 -28
  168. data/spec/bard/server_spec.rb +0 -127
  169. /data/lib/bard/{ci → plugins/deploy/ci}/state.rb +0 -0
  170. /data/{install_files → lib/bard/plugins/install}/apt_dependencies.rb +0 -0
  171. /data/{install_files → lib/bard/plugins/install}/ci +0 -0
  172. /data/{install_files → lib/bard/plugins/install}/setup +0 -0
  173. /data/{install_files → lib/bard/plugins/install}/specified_bundler.rb +0 -0
  174. /data/{install_files → lib/bard/plugins/install}/specified_ruby.rb +0 -0
@@ -0,0 +1,54 @@
1
+ require "uri"
2
+ require "bard/plugins/url"
3
+
4
+ class Bard::CLI
5
+ desc "setup", "installs app in nginx"
6
+ def setup
7
+ path = "/etc/nginx/sites-available/#{project_name}"
8
+ system "sudo tee #{path} >/dev/null <<-'EOF'
9
+ upstream puma {
10
+ server 127.0.0.1:3000 fail_timeout=5;
11
+ }
12
+
13
+ server {
14
+ listen 80;
15
+ server_name #{nginx_server_name};
16
+ root #{Dir.pwd}/public;
17
+
18
+ try_files $uri @app;
19
+
20
+ location @app {
21
+ proxy_pass http://puma;
22
+ proxy_set_header Host $host;
23
+ proxy_set_header X-Real-IP $remote_addr;
24
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
25
+ proxy_set_header X-Forwarded-Proto $scheme;
26
+ }
27
+
28
+ location ~* \\-[0-9a-f]\\{64\\}\\.(ico|css|js|gif|jpe?g|png|webp)$ {
29
+ access_log off;
30
+ expires max;
31
+ add_header Cache-Control public;
32
+ }
33
+
34
+ gzip_static on;
35
+ }
36
+ EOF"
37
+
38
+ dest_path = path.sub("sites-available", "sites-enabled")
39
+ system "sudo ln -sf #{path} #{dest_path}" if !File.exist?(dest_path)
40
+
41
+ system "sudo service nginx restart"
42
+ end
43
+
44
+ no_commands do
45
+ def nginx_server_name
46
+ case ENV["RAILS_ENV"]
47
+ when "production"
48
+ "*.#{URI.parse(config[:production].url).host} _"
49
+ when "staging" then "#{project_name}.botandrose.com"
50
+ else "#{project_name}.localhost"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,75 @@
1
+ require "uri"
2
+ require "shellwords"
3
+ require "bard/plugins/ssh/server"
4
+ require "bard/command"
5
+
6
+ module Bard
7
+ module SSH
8
+ def server
9
+ @server
10
+ end
11
+
12
+ def gateway
13
+ server.gateway
14
+ end
15
+
16
+ def ssh_key
17
+ server.ssh_key
18
+ end
19
+
20
+ def env
21
+ server.env
22
+ end
23
+
24
+ def ssh_uri
25
+ server.ssh_uri
26
+ end
27
+
28
+ def scp_uri(file_path = nil)
29
+ full_path = "/#{path}"
30
+ full_path += "/#{file_path}" if file_path
31
+ URI::Generic.build(scheme: "scp", userinfo: server.user, host: server.host, port: server.port.to_i, path: full_path)
32
+ end
33
+
34
+ def rsync_uri(file_path = nil)
35
+ uri = ssh_uri
36
+ str = "#{uri.user}@#{uri.host}"
37
+ str += ":#{path}"
38
+ str += "/#{file_path}" if file_path
39
+ str
40
+ end
41
+
42
+ def run!(command, home: false, verbose: false, quiet: false, capture: false)
43
+ result = Command.run!(ssh_command(command, home:), verbose:, quiet:)
44
+ result if capture
45
+ end
46
+
47
+ def run(command, home: false, verbose: false, quiet: false)
48
+ Command.run(ssh_command(command, home:), verbose:, quiet:)
49
+ end
50
+
51
+ def exec!(command, home: false)
52
+ Command.exec!(ssh_command(command, home:))
53
+ end
54
+
55
+ private
56
+
57
+ def ssh_command(command, home: false)
58
+ cmd = command
59
+ cmd = "#{env} #{command}" if env
60
+
61
+ unless home
62
+ cmd = "cd #{path} && #{cmd}" if path
63
+ end
64
+
65
+ ssh_opts = ["-tt", "-o StrictHostKeyChecking=no", "-o UserKnownHostsFile=/dev/null", "-o LogLevel=ERROR"]
66
+ ssh_opts << "-i #{ssh_key}" if ssh_key
67
+ ssh_opts << "-p #{server.port}" if server.port && server.port != "22"
68
+ ssh_opts << "-o ProxyJump=#{gateway}" if gateway
69
+
70
+ ssh_target = "#{server.user}@#{server.host}"
71
+
72
+ "ssh #{ssh_opts.join(" ")} #{ssh_target} #{Shellwords.shellescape(cmd)}"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,95 @@
1
+ require "uri"
2
+ require "bard/copy"
3
+ require "bard/command"
4
+
5
+ module Bard
6
+ module SSH
7
+ class Copy < Bard::Copy
8
+ def self.can_handle?(from, to)
9
+ from.has_capability?(:ssh) || to.has_capability?(:ssh)
10
+ end
11
+
12
+ def file
13
+ if from.key == :local
14
+ scp_using_local :to, to
15
+ elsif to.key == :local
16
+ scp_using_local :from, from
17
+ else
18
+ scp_as_mediator
19
+ end
20
+ end
21
+
22
+ def scp_using_local direction, target
23
+ ssh_server = target.server
24
+
25
+ gateway = ssh_server.gateway ? "-oProxyCommand='ssh #{ssh_server.gateway} -W %h:%p'" : ""
26
+
27
+ ssh_key = ssh_server.ssh_key ? "-i #{ssh_server.ssh_key}" : ""
28
+
29
+ ssh_opts = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
30
+
31
+ port = ssh_server.port
32
+ port_opt = port && port.to_s != "22" ? "-P #{port}" : ""
33
+
34
+ from_and_to = [path, target.scp_uri(path).to_s]
35
+ from_and_to.reverse! if direction == :from
36
+
37
+ command = ["scp", ssh_opts, gateway, ssh_key, port_opt, *from_and_to].reject(&:empty?).join(" ")
38
+ Bard::Command.run! command, verbose: verbose
39
+ end
40
+
41
+ def scp_as_mediator
42
+ from_server = from.server
43
+ to_server = to.server
44
+
45
+ raise NotImplementedError if from_server.gateway || to_server.gateway || from_server.ssh_key || to_server.ssh_key
46
+ command = "scp -o ForwardAgent=yes #{from.scp_uri(path)} #{to.scp_uri(path)}"
47
+ Bard::Command.run! command, verbose: verbose
48
+ end
49
+
50
+ def dir
51
+ if from.key == :local
52
+ rsync_using_local :to, to
53
+ elsif to.key == :local
54
+ rsync_using_local :from, from
55
+ else
56
+ rsync_as_mediator
57
+ end
58
+ end
59
+
60
+ def rsync_using_local direction, target
61
+ ssh_server = target.server
62
+
63
+ ssh_uri = ssh_server.ssh_uri
64
+
65
+ gateway = ssh_server.gateway ? "-oProxyCommand=\"ssh #{ssh_server.gateway} -W %h:%p\"" : ""
66
+
67
+ ssh_key = ssh_server.ssh_key ? "-i #{ssh_server.ssh_key}" : ""
68
+ ssh = "-e'ssh #{gateway} -p#{ssh_uri.port || 22}'"
69
+
70
+ from_and_to = ["./#{path}", target.rsync_uri(path)]
71
+ from_and_to.reverse! if direction == :from
72
+ from_and_to[-1].sub! %r(/[^/]+$), '/'
73
+
74
+ command = "rsync #{ssh} --delete --info=progress2 -az #{from_and_to.join(" ")}"
75
+ Bard::Command.run! command, verbose: verbose
76
+ end
77
+
78
+ def rsync_as_mediator
79
+ from_server = from.server
80
+ to_server = to.server
81
+
82
+ raise NotImplementedError if from_server.gateway || to_server.gateway || from_server.ssh_key || to_server.ssh_key
83
+
84
+ from_uri = from_server.ssh_uri
85
+ to_uri = to_server.ssh_uri
86
+
87
+ from_str = "-p#{from_uri.port || 22} #{from_uri.user}@#{from_uri.host}"
88
+ to_str = to.rsync_uri(path).sub(%r(/[^/]+$), '/')
89
+
90
+ command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to_uri.port || 22} -o StrictHostKeyChecking=no -o LogLevel=ERROR\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
91
+ Bard::Command.run! command, verbose: verbose
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,5 +1,4 @@
1
1
  require "uri"
2
- require "bard/command"
3
2
 
4
3
  module Bard
5
4
  class SSHServer
@@ -9,13 +8,11 @@ module Bard
9
8
  @uri_string = uri_string
10
9
  @options = options
11
10
 
12
- # Parse URI
13
11
  uri = parse_uri(uri_string)
14
12
  @user = uri.user || ENV['USER']
15
13
  @host = uri.host
16
14
  @port = uri.port ? uri.port.to_s : "22"
17
15
 
18
- # Store options
19
16
  @path = options[:path]
20
17
  @gateway = options[:gateway]
21
18
  @ssh_key = options[:ssh_key]
@@ -23,39 +20,42 @@ module Bard
23
20
  end
24
21
 
25
22
  def ssh_uri
26
- "#{user}@#{host}:#{port}"
23
+ URI("ssh://#{user}@#{host}:#{port}")
27
24
  end
28
25
 
29
26
  def hostname
30
27
  host
31
28
  end
32
29
 
30
+ def to_s
31
+ str = "#{user}@#{host}"
32
+ str += ":#{port}" if port && port != "22"
33
+ str
34
+ end
35
+
33
36
  def connection_string
34
37
  "#{user}@#{host}"
35
38
  end
36
39
 
37
- def run(command)
38
- full_command = build_command(command)
39
- Open3.capture3(full_command)
40
+ def ==(other)
41
+ return false unless other.is_a?(Bard::SSHServer)
42
+ state == other.state
40
43
  end
44
+ alias_method :eql?, :==
41
45
 
42
- def run!(command)
43
- output, error, status = run(command)
44
- if status.to_i.nonzero?
45
- raise Command::Error, "Command failed: #{command}\n#{error}"
46
- end
47
- output
46
+ def hash
47
+ state.hash
48
48
  end
49
49
 
50
- def exec!(command)
51
- full_command = build_command(command)
52
- exec(full_command)
50
+ protected
51
+
52
+ def state
53
+ [user, host, port, path, gateway, ssh_key, env]
53
54
  end
54
55
 
55
56
  private
56
57
 
57
58
  def parse_uri(uri_string)
58
- # Handle user@host:port format
59
59
  if uri_string =~ /^([^@]+@)?([^:]+)(?::(\d+))?$/
60
60
  user_part = $1&.chomp('@')
61
61
  host_part = $2
@@ -71,30 +71,5 @@ module Bard
71
71
  URI.parse("ssh://#{uri_string}")
72
72
  end
73
73
  end
74
-
75
- def build_command(command)
76
- cmd = "ssh -tt"
77
-
78
- # Add port
79
- cmd += " -p #{port}" if port != "22"
80
-
81
- # Add gateway
82
- cmd += " -o ProxyJump=#{gateway}" if gateway
83
-
84
- # Add SSH key
85
- cmd += " -i #{ssh_key}" if ssh_key
86
-
87
- # Add user@host
88
- cmd += " #{user}@#{host}"
89
-
90
- # Add command with path and env
91
- remote_cmd = ""
92
- remote_cmd += "#{env} " if env
93
- remote_cmd += "cd #{path} && " if path
94
- remote_cmd += command
95
-
96
- cmd += " '#{remote_cmd}'"
97
- cmd
98
- end
99
74
  end
100
75
  end
@@ -0,0 +1,20 @@
1
+ require "bard/target"
2
+ require "bard/plugins/url/target_methods"
3
+ require "bard/plugins/ssh/connection"
4
+
5
+ class Bard::Target
6
+ def ssh(uri = nil, **options)
7
+ if uri.nil?
8
+ return @server
9
+ else
10
+ extend Bard::SSH
11
+
12
+ @server = Bard::SSHServer.new(uri, **options)
13
+ @path = options[:path] if options[:path]
14
+ enable_capability(:ssh)
15
+
16
+ hostname = @server.hostname
17
+ url("https://#{hostname}") if hostname
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ require "bard/plugins/ssh/target_methods"
2
+ require "bard/plugins/ssh/copy"
3
+
4
+ class Bard::CLI
5
+ option :home, type: :boolean
6
+ desc "ssh [to=production]", "logs into the specified server via SSH"
7
+ def ssh(to = :production)
8
+ config[to].exec! "exec $SHELL -l", home: options[:home]
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ require "bard/target"
2
+
3
+ class Bard::Target
4
+ def url(value = nil)
5
+ if value.nil?
6
+ @url
7
+ elsif value == false
8
+ @url = nil
9
+ @capabilities.delete(:url)
10
+ else
11
+ @url = normalize_url(value)
12
+ enable_capability(:url)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def normalize_url(value)
19
+ normalized = value.to_s
20
+ normalized = "https://#{normalized}" unless normalized.start_with?("http")
21
+ normalized
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ require "bard/plugins/url/target_methods"
@@ -0,0 +1,6 @@
1
+ class Bard::CLI
2
+ desc "vim [branch=master]", "open all files that have changed since master"
3
+ def vim(branch = "master")
4
+ exec "vim -p `(git diff #{branch} --name-only; git ls-files --others --exclude-standard) | grep -v '^app/assets/images/' | grep -v '^app/assets/stylesheets/' | while read f; do [ -f \"$f\" ] && ! file -b \"$f\" | grep -q \"binary\" && echo \"$f\"; done | tac`"
5
+ end
6
+ end
@@ -0,0 +1,25 @@
1
+ module Bard
2
+ module Retryable
3
+ MAX_RETRIES = 5
4
+ INITIAL_DELAY = 1
5
+
6
+ def retry_with_backoff(max_retries: MAX_RETRIES)
7
+ retries = 0
8
+ delay = INITIAL_DELAY
9
+
10
+ begin
11
+ yield
12
+ rescue => e
13
+ if retries < max_retries
14
+ retries += 1
15
+ puts " Network error (attempt #{retries}/#{max_retries}): #{e.message}. Retrying in #{delay}s..."
16
+ sleep(delay)
17
+ delay *= 2
18
+ retry
19
+ else
20
+ raise "Network error after #{max_retries} attempts: #{e.message}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Bard
2
+ module Secrets
3
+ REPO = "git@github.com:botandrosedesign/secrets"
4
+
5
+ def self.fetch(key)
6
+ raw = `git ls-remote -t #{REPO}`
7
+ raw[/#{Regexp.escape(key)}\|(.+)$/, 1]
8
+ end
9
+ end
10
+ end
data/lib/bard/target.rb CHANGED
@@ -1,22 +1,14 @@
1
- require "uri"
2
1
  require "bard/command"
3
- require "bard/copy"
4
- require "bard/deploy_strategy"
5
2
 
6
3
  module Bard
7
4
  class Target
8
- attr_reader :key, :config, :path
9
- attr_accessor :server, :gateway, :ssh_key, :env
5
+ attr_reader :key, :config
10
6
 
11
7
  def initialize(key, config)
12
8
  @key = key
13
9
  @config = config
14
10
  @capabilities = []
15
- @ping_urls = []
16
- @strategy_options_hash = {}
17
- @deploy_strategy = nil
18
11
  @path = nil
19
- @server = nil
20
12
  end
21
13
 
22
14
  # Capability tracking
@@ -30,193 +22,25 @@ module Bard
30
22
 
31
23
  def require_capability!(capability)
32
24
  unless has_capability?(capability)
33
- error_message = case capability
34
- when :ssh
35
- "SSH not configured for this target"
36
- when :ping
37
- "Ping URL not configured for this target"
38
- else
39
- "#{capability} capability not configured for this target"
40
- end
41
- raise error_message
42
- end
43
- end
44
-
45
- # SSH configuration
46
- def ssh(uri_or_false = nil, **options)
47
- if uri_or_false.nil?
48
- # Getter - return false if explicitly disabled, otherwise return server
49
- return @ssh_disabled ? false : @server
50
- elsif uri_or_false == false
51
- # Disable SSH
52
- @server = nil
53
- @ssh_disabled = true
54
- @capabilities.delete(:ssh)
55
- else
56
- # Enable SSH
57
- require "bard/ssh_server"
58
- @server = SSHServer.new(uri_or_false, **options)
59
- @path = options[:path] if options[:path]
60
- @gateway = options[:gateway] if options[:gateway]
61
- @ssh_key = options[:ssh_key] if options[:ssh_key]
62
- @env = options[:env] if options[:env]
63
- enable_capability(:ssh)
64
-
65
- # Set SSH as default deployment strategy if none set
66
- @deploy_strategy ||= :ssh
67
-
68
- # Auto-configure ping from hostname
69
- hostname = @server.hostname
70
- ping(hostname) if hostname
71
- end
72
- end
73
-
74
- def ssh_uri
75
- server&.ssh_uri
76
- end
77
-
78
- # Path configuration
79
- def path(new_path = nil)
80
- if new_path
81
- @path = new_path
82
- else
83
- @path || config.project_name
84
- end
85
- end
86
-
87
- # Ping configuration
88
- def ping(*urls)
89
- if urls.empty?
90
- # Getter
91
- @ping_urls
92
- elsif urls.first == false
93
- # Disable ping
94
- @ping_urls = []
95
- @capabilities.delete(:ping)
96
- else
97
- # Enable ping
98
- @ping_urls = urls.flatten
99
- enable_capability(:ping)
100
- end
101
- end
102
-
103
- def ping_urls
104
- @ping_urls
105
- end
106
-
107
- def ping!
108
- require_capability!(:ping)
109
- require "bard/ping"
110
- failed_urls = Bard::Ping.call(self)
111
- if failed_urls.any?
112
- raise "Ping failed for: #{failed_urls.join(', ')}"
113
- end
114
- end
115
-
116
- def open
117
- require_capability!(:ping)
118
- system "open #{ping_urls.first}"
119
- end
120
-
121
- # Deploy strategy
122
- attr_reader :deploy_strategy
123
-
124
- # GitHub Pages deployment configuration
125
- def github_pages(url = nil)
126
- if url.nil?
127
- # Getter
128
- @github_pages_url
129
- else
130
- # Setter
131
- @deploy_strategy = :github_pages
132
- @github_pages_url = url
133
- enable_capability(:github_pages)
134
- end
135
- end
136
-
137
- def strategy_options(strategy_name)
138
- @strategy_options_hash[strategy_name] || {}
139
- end
140
-
141
- def deploy_strategy_instance
142
- raise "No deployment strategy configured for target #{key}" unless @deploy_strategy
143
-
144
- strategy_class = DeployStrategy[@deploy_strategy]
145
- raise "Unknown deployment strategy: #{@deploy_strategy}" unless strategy_class
146
-
147
- strategy_class.new(self)
148
- end
149
-
150
- # Dynamic strategy DSL via method_missing
151
- def method_missing(method, *args, **kwargs, &block)
152
- strategy_class = DeployStrategy[method]
153
-
154
- if strategy_class
155
- # This is a deployment strategy
156
- @deploy_strategy = method
157
-
158
- # Store options
159
- @strategy_options_hash[method] = kwargs
160
-
161
- # Auto-configure ping if first arg is a URL
162
- if args.first && args.first.to_s =~ /^https?:\/\//
163
- ping(args.first)
164
- end
165
-
166
- # Call the strategy's initializer if it wants to configure the target
167
- # (This will be handled by the strategy class)
168
- else
169
- super
25
+ raise "#{capability} capability not configured for this target"
170
26
  end
171
27
  end
172
28
 
173
- def respond_to_missing?(method, include_private = false)
174
- DeployStrategy[method] || super
29
+ def path
30
+ @path || config.project_name
175
31
  end
176
32
 
177
- # Remote command execution
178
- def run!(command, home: false, verbose: false, quiet: false)
179
- require_capability!(:ssh)
180
- Command.run!(command, on: server, home: home, verbose: verbose, quiet: quiet)
33
+ def run!(command, home: false, verbose: false, quiet: false, capture: false)
34
+ result = Command.run!(command, verbose:, quiet:)
35
+ result if capture
181
36
  end
182
37
 
183
38
  def run(command, home: false, verbose: false, quiet: false)
184
- require_capability!(:ssh)
185
- Command.run(command, on: server, home: home, verbose: verbose, quiet: quiet)
39
+ Command.run(command, verbose:, quiet:)
186
40
  end
187
41
 
188
42
  def exec!(command, home: false)
189
- require_capability!(:ssh)
190
- Command.exec!(command, on: server, home: home)
191
- end
192
-
193
- # File transfer
194
- def copy_file(path, to:, verbose: false)
195
- require_capability!(:ssh)
196
- to.require_capability!(:ssh)
197
- Copy.file(path, from: self, to: to, verbose: verbose)
198
- end
199
-
200
- def copy_dir(path, to:, verbose: false)
201
- require_capability!(:ssh)
202
- to.require_capability!(:ssh)
203
- Copy.dir(path, from: self, to: to, verbose: verbose)
204
- end
205
-
206
- # URI methods for compatibility
207
- def scp_uri(file_path = nil)
208
- uri = URI("scp://#{ssh_uri}")
209
- uri.path = "/#{path}"
210
- uri.path += "/#{file_path}" if file_path
211
- uri
212
- end
213
-
214
- def rsync_uri(file_path = nil)
215
- uri = URI("ssh://#{ssh_uri}")
216
- str = "#{uri.user}@#{uri.host}"
217
- str += ":#{path}"
218
- str += "/#{file_path}" if file_path
219
- str
43
+ Command.exec!(command)
220
44
  end
221
45
 
222
46
  # Utility methods
@@ -235,5 +59,23 @@ module Bard
235
59
  end
236
60
  end
237
61
  end
62
+
63
+ def ==(other)
64
+ return false unless other.is_a?(Bard::Target)
65
+ comparable_state == other.comparable_state
66
+ end
67
+ alias_method :eql?, :==
68
+
69
+ def hash
70
+ comparable_state.hash
71
+ end
72
+
73
+ protected
74
+
75
+ def comparable_state
76
+ (instance_variables - [:@key, :@config]).sort.map do |ivar|
77
+ [ivar, instance_variable_get(ivar)]
78
+ end
79
+ end
238
80
  end
239
81
  end
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "2.0.0.beta"
2
+ VERSION = "2.0.0"
3
3
  end
4
4
 
data/lib/bard.rb CHANGED
@@ -1,3 +1 @@
1
- module Bard
2
- end
3
-
1
+ require "bard/cli"