bard 1.0.1 → 1.0.3

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.
data/lib/bard/cli.rb CHANGED
@@ -1,17 +1,9 @@
1
1
  # this file gets loaded in the CLI context, not the Rails boot context
2
2
 
3
3
  require "thor"
4
- require "bard/git"
5
- require "bard/ci"
6
- require "bard/copy"
7
- require "bard/github"
8
- require "bard/ping"
9
4
  require "bard/config"
10
5
  require "bard/command"
11
- require "bard/provision"
12
6
  require "term/ansicolor"
13
- require "open3"
14
- require "uri"
15
7
 
16
8
  module Bard
17
9
  class CLI < Thor
@@ -19,244 +11,24 @@ module Bard
19
11
 
20
12
  class_option :verbose, type: :boolean, aliases: :v
21
13
 
22
- desc "data --from=production --to=local", "copy database and assets from from to to"
23
- option :from, default: "production"
24
- option :to, default: "local"
25
- def data
26
- from = config[options[:from]]
27
- to = config[options[:to]]
28
-
29
- if to.key == :production
30
- url = to.ping.first
31
- puts yellow "WARNING: You are about to push data to production, overwriting everything that is there!"
32
- answer = ask("If you really want to do this, please type in the full HTTPS url of the production server:")
33
- if answer != url
34
- puts red("!!! ") + "Failed! We expected #{url}. Is this really where you want to overwrite all the data?"
35
- exit 1
36
- end
37
- end
38
-
39
- puts "Dumping #{from.key} database to file..."
40
- from.run! "bin/rake db:dump"
41
-
42
- puts "Transfering file from #{from.key} to #{to.key}..."
43
- from.copy_file "db/data.sql.gz", to: to, verbose: true
44
-
45
- puts "Loading file into #{to.key} database..."
46
- to.run! "bin/rake db:load"
47
-
48
- config.data.each do |path|
49
- puts "Synchronizing files in #{path}..."
50
- from.copy_dir path, to: to, verbose: true
51
- end
52
- end
53
-
54
- desc "master_key --from=production --to=local", "copy master key from from to to"
55
- option :from, default: "production"
56
- option :to, default: "local"
57
- def master_key
58
- from = config[options[:from]]
59
- to = config[options[:to]]
60
- from.copy_file "config/master.key", to:
61
- end
62
-
63
- desc "stage [branch=HEAD]", "pushes current branch, and stages it"
64
- def stage branch=Git.current_branch
65
- unless config.servers.key?(:production)
66
- raise Thor::Error.new("`bard stage` is disabled until a production server is defined. Until then, please use `bard deploy` to deploy to the staging server.")
67
- end
68
-
69
- run! "git push -u origin #{branch}", verbose: true
70
- config[:staging].run! "git fetch && git checkout -f origin/#{branch} && bin/setup"
71
- puts green("Stage Succeeded")
72
-
73
- ping :staging
74
- end
75
-
76
- option :"skip-ci", type: :boolean
77
- option :"local-ci", type: :boolean
78
- desc "deploy [TO=production]", "checks that current branch is a ff with master, checks with ci, merges into master, deploys to target, and then deletes branch."
79
- def deploy to=:production
80
- branch = Git.current_branch
81
-
82
- if branch == "master"
83
- if !Git.up_to_date_with_remote?(branch)
84
- run! "git push origin #{branch}:#{branch}"
85
- end
86
- invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
87
-
88
- else
89
- run! "git fetch origin master:master"
90
-
91
- unless Git.fast_forward_merge?("origin/master", branch)
92
- puts "The master branch has advanced. Attempting rebase..."
93
- run! "git rebase origin/master"
94
- end
95
-
96
- run! "git push -f origin #{branch}:#{branch}"
97
-
98
- invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
99
-
100
- run! "git push origin #{branch}:master"
101
- run! "git fetch origin master:master"
102
- end
103
-
104
- if `git remote` =~ /\bgithub\b/
105
- run! "git push github"
106
- end
107
-
108
- config[to].run! "git pull origin master && bin/setup"
109
-
110
- puts green("Deploy Succeeded")
111
-
112
- if branch != "master"
113
- puts "Deleting branch: #{branch}"
114
- run! "git push --delete origin #{branch}"
115
-
116
- if branch == Git.current_branch
117
- run! "git checkout master"
118
- end
119
-
120
- run! "git branch -D #{branch}"
121
- end
122
-
123
- ping to
124
- end
125
-
126
- option :"local-ci", type: :boolean
127
- option :status, type: :boolean
128
- desc "ci [branch=HEAD]", "runs ci against BRANCH"
129
- def ci branch=Git.current_branch
130
- ci = CI.new(project_name, branch, local: options["local-ci"])
131
- if ci.exists?
132
- return puts ci.status if options["status"]
133
-
134
- puts "Continuous integration: starting build on #{branch}..."
135
-
136
- success = ci.run do |elapsed_time, last_time|
137
- if last_time
138
- percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
139
- output = " Estimated completion: #{percentage}%"
140
- else
141
- output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
142
- end
143
- print "\x08" * output.length
144
- print output
145
- $stdout.flush
146
- end
147
-
148
- if success
149
- puts
150
- puts "Continuous integration: success!"
151
- puts "Deploying..."
152
- else
153
- puts
154
- puts ci.last_response
155
- puts ci.console
156
- puts red("Automated tests failed!")
157
- exit 1
158
- end
159
-
160
- else
161
- puts red("No CI found for #{project_name}!")
162
- puts "Re-run with --skip-ci to bypass CI, if you absolutely must, and know what you're doing."
163
- exit 1
164
- end
165
- end
166
-
167
- desc "open [server=production]", "opens the url in the web browser."
168
- def open server=:production
169
- exec "xdg-open #{config[server].ping.first}"
170
- end
171
-
172
- option :home, type: :boolean
173
- desc "ssh [to=production]", "logs into the specified server via SSH"
174
- def ssh to=:production
175
- config[to].exec! "exec $SHELL -l", home: options[:home]
176
- end
177
-
178
- desc "install", "copies bin/setup and bin/ci scripts into current project."
179
- def install
180
- install_files_path = File.expand_path(File.join(__dir__, "../../install_files/*"))
181
- system "cp -R #{install_files_path} bin/"
182
- github_files_path = File.expand_path(File.join(__dir__, "../../install_files/.github"))
183
- system "cp -R #{github_files_path} ./"
184
- end
185
-
186
- desc "provision [ssh_url]", "takes an ssh url to a raw ubuntu 22.04 install, and readies it in the shape of :production"
187
- def provision ssh_url
188
- Provision.call(config, ssh_url.dup) # dup unfreezes the string for later mutation
189
- end
190
-
191
- desc "setup", "installs app in nginx"
192
- def setup
193
- path = "/etc/nginx/sites-available/#{project_name}"
194
- dest_path = path.sub("sites-available", "sites-enabled")
195
- server_name = case ENV["RAILS_ENV"]
196
- when "production"
197
- (config[:production].ping.map do |str|
198
- "*.#{URI.parse(str).host}"
199
- end + ["_"]).join(" ")
200
- when "staging" then "#{project_name}.botandrose.com"
201
- else "#{project_name}.localhost"
202
- end
203
-
204
- system "sudo tee #{path} >/dev/null <<-EOF
205
- server {
206
- listen 80;
207
- server_name #{server_name};
208
-
209
- root #{Dir.pwd}/public;
210
- passenger_enabled on;
211
-
212
- location ~* \\.(ico|css|js|gif|jp?g|png|webp) {
213
- access_log off;
214
- if (\\$request_filename ~ \"-[0-9a-f]{32}\\.\") {
215
- expires max;
216
- add_header Cache-Control public;
217
- }
218
- }
219
- gzip_static on;
220
- }
221
- EOF"
222
- system "sudo ln -sf #{path} #{dest_path}" if !File.exist?(dest_path)
223
- system "sudo service nginx restart"
224
- end
225
-
226
- desc "ping [server=production]", "hits the server over http to verify that its up."
227
- def ping server=:production
228
- server = config[server]
229
- down_urls = Bard::Ping.call(config[server])
230
- down_urls.each { |url| puts "#{url} is down!" }
231
- exit 1 if down_urls.any?
232
- end
233
-
234
- # HACK: we don't use Thor::Base#run, so its okay to stomp on it here
235
- original_verbose, $VERBOSE = $VERBOSE, nil
236
- Thor::THOR_RESERVED_WORDS -= ["run"]
237
- $VERBOSE = original_verbose
238
-
239
- desc "run <command>", "run the given command on production"
240
- def run *args
241
- server = config[:production]
242
- server.run! *args, verbose: true
243
- end
244
-
245
- desc "hurt <command>", "reruns a command until it fails"
246
- def hurt *args
247
- 1.upto(Float::INFINITY) do |count|
248
- puts "Running attempt #{count}"
249
- system *args
250
- unless $?.success?
251
- puts "Ran #{count-1} times before failing"
252
- break
253
- end
254
- end
255
- end
256
-
257
- desc "vim [branch=master]", "open all files that have changed since master"
258
- def vim branch="master"
259
- exec "vim -p `git diff #{branch} --name-only | grep -v sass$ | tac`"
14
+ {
15
+ data: "Data",
16
+ stage: "Stage",
17
+ deploy: "Deploy",
18
+ ci: "CI",
19
+ master_key: "MasterKey",
20
+ setup: "Setup",
21
+ run: "Run",
22
+ open: "Open",
23
+ ssh: "SSH",
24
+ install: "Install",
25
+ provision: "Provision",
26
+ ping: "Ping",
27
+ hurt: "Hurt",
28
+ vim: "Vim",
29
+ }.each do |command, klass|
30
+ require "bard/cli/#{command}"
31
+ include const_get(klass)
260
32
  end
261
33
 
262
34
  def self.exit_on_failure? = true
@@ -273,6 +45,9 @@ EOF"
273
45
 
274
46
  def run!(...)
275
47
  Bard::Command.run!(...)
48
+ rescue Bard::Command::Error => e
49
+ puts red("!!! ") + "Running command failed: #{yellow(e.message)}"
50
+ exit 1
276
51
  end
277
52
  end
278
53
  end
data/lib/bard/command.rb CHANGED
@@ -1,5 +1,9 @@
1
+ require "open3"
2
+
1
3
  module Bard
2
4
  class Command < Struct.new(:command, :on, :home)
5
+ class Error < RuntimeError; end
6
+
3
7
  def self.run! command, on: :local, home: false, verbose: false, quiet: false
4
8
  new(command, on, home).run! verbose:, quiet:
5
9
  end
@@ -14,9 +18,7 @@ module Bard
14
18
 
15
19
  def run! verbose: false, quiet: false
16
20
  if !run(verbose:, quiet:)
17
- raise "Running command failed: #{full_command}"
18
- # puts red("!!! ") + "Running command failed: #{yellow(command)}"
19
- # exit 1
21
+ raise Error.new(full_command)
20
22
  end
21
23
  end
22
24
 
@@ -49,7 +51,6 @@ module Bard
49
51
  end
50
52
 
51
53
  def remote_command quiet: false
52
- uri = on.ssh_uri
53
54
  ssh_key = on.ssh_key ? "-i #{on.ssh_key} " : ""
54
55
  cmd = command
55
56
  if on.env
@@ -58,10 +59,11 @@ module Bard
58
59
  unless home
59
60
  cmd = "cd #{on.path} && #{cmd}"
60
61
  end
61
- cmd = "ssh -tt #{ssh_key}#{"-p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} '#{cmd}'"
62
+ uri = on.ssh_uri
63
+ cmd = "ssh -tt #{ssh_key} -p#{uri.port} #{uri.user}@#{uri.host} '#{cmd}'"
62
64
  if on.gateway
63
65
  uri = on.ssh_uri(:gateway)
64
- cmd = "ssh -tt #{" -p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} \"#{cmd}\""
66
+ cmd = "ssh -tt -p#{uri.port} #{uri.user}@#{uri.host} \"#{cmd}\""
65
67
  end
66
68
  cmd += " 2>&1" if quiet
67
69
  cmd
data/lib/bard/config.rb CHANGED
@@ -12,13 +12,6 @@ module Bard
12
12
  "./",
13
13
  false,
14
14
  ),
15
- theia: Server.new(
16
- project_name,
17
- :theia,
18
- "gubito@gubs.pagekite.me",
19
- "Sites/#{project_name}",
20
- false,
21
- ),
22
15
  gubs: Server.new(
23
16
  project_name,
24
17
  :gubs,
@@ -40,7 +33,7 @@ module Bard
40
33
  ),
41
34
  }
42
35
  if path && File.exist?(path)
43
- source = File.read(File.expand_path(path))
36
+ source = File.read(path)
44
37
  end
45
38
  if source
46
39
  instance_eval source
@@ -51,9 +44,7 @@ module Bard
51
44
 
52
45
  def server key, &block
53
46
  key = key.to_sym
54
- @servers[key] ||= Server.new(project_name, key)
55
- @servers[key].instance_eval &block if block_given?
56
- @servers[key]
47
+ @servers[key] = Server.define(project_name, key, &block)
57
48
  end
58
49
 
59
50
  def [] key
data/lib/bard/copy.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require "uri"
2
+ require "bard/command"
3
+
1
4
  module Bard
2
5
  class Copy < Struct.new(:path, :from, :to, :verbose)
3
6
  def self.file path, from:, to:, verbose: false
@@ -19,33 +22,20 @@ module Bard
19
22
  end
20
23
 
21
24
  def scp_using_local direction, server
22
- uri = URI.parse("ssh://#{server.gateway}")
23
- port = uri.port ? "-p#{uri.port}" : ""
24
- gateway = server.gateway ? "-oProxyCommand='ssh #{port} #{uri.user}@#{uri.host} -W %h:%p'" : ""
25
+ gateway = server.gateway ? "-oProxyCommand='ssh #{server.ssh_uri(:gateway)} -W %h:%p'" : ""
25
26
 
26
27
  ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
27
28
 
28
- uri = URI.parse("ssh://#{server.ssh}")
29
- port = uri.port ? "-P#{uri.port}" : ""
30
- from_and_to = [path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
31
-
29
+ from_and_to = [path, server.scp_uri(path)]
32
30
  from_and_to.reverse! if direction == :from
33
- command = "scp #{gateway} #{ssh_key} #{port} #{from_and_to.join(" ")}"
34
31
 
32
+ command = ["scp", gateway, ssh_key, *from_and_to].join(" ")
35
33
  Bard::Command.run! command, verbose: verbose
36
34
  end
37
35
 
38
36
  def scp_as_mediator
39
37
  raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
40
-
41
- from_uri = URI.parse("ssh://#{from.ssh}")
42
- from_str = "scp://#{from_uri.user}@#{from_uri.host}:#{from_uri.port || 22}/#{from.path}/#{path}"
43
-
44
- to_uri = URI.parse("ssh://#{to.ssh}")
45
- to_str = "scp://#{to_uri.user}@#{to_uri.host}:#{to_uri.port || 22}/#{to.path}/#{path}"
46
-
47
- command = "scp -o ForwardAgent=yes #{from_str} #{to_str}"
48
-
38
+ command = "scp -o ForwardAgent=yes #{from.scp_uri(path)} #{to.scp_uri(path)}"
49
39
  Bard::Command.run! command, verbose: verbose
50
40
  end
51
41
 
@@ -55,46 +45,31 @@ module Bard
55
45
  elsif to.key == :local
56
46
  rsync_using_local :from, from
57
47
  else
58
- rsync_as_mediator from, to
48
+ rsync_as_mediator
59
49
  end
60
50
  end
61
51
 
62
52
  def rsync_using_local direction, server
63
- uri = URI.parse("ssh://#{server.gateway}")
64
- port = uri.port ? "-p#{uri.port}" : ""
65
- gateway = server.gateway ? "-oProxyCommand=\"ssh #{port} #{uri.user}@#{uri.host} -W %h:%p\"" : ""
53
+ gateway = server.gateway ? "-oProxyCommand=\"ssh #{server.ssh_uri(:gateway)} -W %h:%p\"" : ""
66
54
 
67
55
  ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
68
- uri = URI.parse("ssh://#{server.ssh}")
69
- port = uri.port ? "-p#{uri.port}" : ""
70
- ssh = "-e'ssh #{ssh_key} #{port} #{gateway}'"
56
+ ssh = "-e'ssh #{gateway} -p#{server.ssh_uri.port || 22}'"
71
57
 
72
- dest_path = path.dup
73
- dest_path = "./#{dest_path}"
74
- from_and_to = [dest_path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
58
+ from_and_to = ["./#{path}", server.rsync_uri(path)]
75
59
  from_and_to.reverse! if direction == :from
76
60
  from_and_to[-1].sub! %r(/[^/]+$), '/'
77
61
 
78
62
  command = "rsync #{ssh} --delete --info=progress2 -az #{from_and_to.join(" ")}"
79
-
80
63
  Bard::Command.run! command, verbose: verbose
81
64
  end
82
65
 
83
- def rsync_as_mediator from, to
66
+ def rsync_as_mediator
84
67
  raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
85
68
 
86
- dest_path = path.dup
87
- dest_path = "./#{dest_path}"
88
-
89
- from_uri = URI.parse("ssh://#{from.ssh}")
90
- from_str = "-p#{from_uri.port || 22} #{from_uri.user}@#{from_uri.host}"
91
-
92
- to_uri = URI.parse("ssh://#{to.ssh}")
93
- to_str = "#{to_uri.user}@#{to_uri.host}:#{to.path}/#{path}"
94
- to_str.sub! %r(/[^/]+$), '/'
95
-
96
- command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to_uri.port || 22} -o StrictHostKeyChecking=no\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
69
+ from_str = "-p#{from.ssh_uri.port || 22} #{from.ssh_uri.user}@#{from.ssh_uri.host}"
70
+ to_str = to.rsync_uri(path).sub(%r(/[^/]+$), '/')
97
71
 
72
+ command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to.ssh_uri.port || 22} -o StrictHostKeyChecking=no\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
98
73
  Bard::Command.run! command, verbose: verbose
99
74
  end
100
75
  end
data/lib/bard/git.rb CHANGED
@@ -8,10 +8,6 @@ module Bard
8
8
  ref.sub(/refs\/heads\//, '') # refs/heads/master ... we want "master"
9
9
  end
10
10
 
11
- def current_sha
12
- sha_of("HEAD")
13
- end
14
-
15
11
  def fast_forward_merge?(root, branch)
16
12
  root_head = sha_of(root)
17
13
  branch_head = sha_of(branch)
data/lib/bard/ping.rb CHANGED
@@ -9,7 +9,6 @@ module Bard
9
9
 
10
10
  def call
11
11
  server.ping.reject do |url|
12
- next true if url == false
13
12
  response = get_response_with_redirect(url) rescue nil
14
13
  response.is_a?(Net::HTTPSuccess)
15
14
  end
@@ -27,7 +27,7 @@ class Bard::Provision::SSH < Bard::Provision
27
27
  ssh_url << ":#{target_port}"
28
28
  puts " ✓"
29
29
  end
30
-
30
+
31
31
  private
32
32
 
33
33
  def target_port
@@ -3,17 +3,10 @@ module Bard
3
3
  def self.call(...) = new(...).call
4
4
 
5
5
  def call
6
- SSH.call(*values)
7
- User.call(*values)
8
- Apt.call(*values)
9
- MySQL.call(*values)
10
- Repo.call(*values)
11
- MasterKey.call(*values)
12
- RVM.call(*values)
13
- App.call(*values)
14
- Passenger.call(*values)
15
- Data.call(*values)
16
- HTTP.call(*values)
6
+ %w[SSH User Apt MySQL Repo MasterKey RVM App Passenger Data HTTP].each do |step|
7
+ require "bard/provision/#{step.downcase}"
8
+ self.class.const_get(step).call(*values)
9
+ end
17
10
  end
18
11
 
19
12
  private
@@ -28,15 +21,3 @@ module Bard
28
21
  end
29
22
  end
30
23
 
31
- require "bard/provision/ssh"
32
- require "bard/provision/user"
33
- require "bard/provision/apt"
34
- require "bard/provision/mysql"
35
- require "bard/provision/repo"
36
- require "bard/provision/master_key"
37
- require "bard/provision/rvm"
38
- require "bard/provision/app"
39
- require "bard/provision/passenger"
40
- require "bard/provision/data"
41
- require "bard/provision/http"
42
-
data/lib/bard/server.rb CHANGED
@@ -3,7 +3,13 @@ require "bard/command"
3
3
  require "bard/copy"
4
4
 
5
5
  module Bard
6
- class Server < Struct.new(:project_name, :key, :ssh, :path, :ping, :gateway, :ssh_key, :env, :provision)
6
+ class Server < Struct.new(:project_name, :key, :ssh, :path, :ping, :gateway, :ssh_key, :env)
7
+ def self.define project_name, key, &block
8
+ new(project_name, key).tap do |server|
9
+ server.instance_eval &block
10
+ end
11
+ end
12
+
7
13
  def self.setting *fields
8
14
  fields.each do |field|
9
15
  define_method field do |*args|
@@ -18,20 +24,19 @@ module Bard
18
24
  end
19
25
  end
20
26
 
21
- setting :ssh, :path, :ping, :gateway, :ssh_key, :env, :provision
27
+ setting :ssh, :path, :ping, :gateway, :ssh_key, :env
22
28
 
23
29
  def ping(*args)
24
30
  if args.length == 0
25
- (super() || [nil]).map(&method(:normalize_ping))
31
+ (super() || [nil]).map(&method(:normalize_ping)).flatten
26
32
  else
27
33
  self.ping = args
28
34
  end
29
35
  end
30
36
 
31
37
  private def normalize_ping value
32
- return value if value == false
33
- uri = URI.parse("ssh://#{ssh}")
34
- normalized = "https://#{uri.host}" # default if none specified
38
+ return [] if value == false
39
+ normalized = "https://#{ssh_uri.host}" # default if none specified
35
40
  if value =~ %r{^/}
36
41
  normalized += value
37
42
  elsif value.to_s.length > 0
@@ -55,7 +60,24 @@ module Bard
55
60
 
56
61
  def ssh_uri which=:ssh
57
62
  value = send(which)
58
- URI.parse("ssh://#{value}")
63
+ URI("ssh://#{value}")
64
+ end
65
+
66
+ def scp_uri file_path=nil
67
+ ssh_uri.dup.tap do |uri|
68
+ uri.scheme = "scp"
69
+ uri.path = "/#{path}"
70
+ uri.path += "/#{file_path}" if file_path
71
+ end
72
+ end
73
+
74
+ def rsync_uri file_path=nil
75
+ ssh_uri.dup.tap do |uri|
76
+ uri.scheme = nil
77
+ uri.port = nil
78
+ uri.path = ":#{path}"
79
+ uri.path += "/#{file_path}" if file_path
80
+ end.to_s[2..]
59
81
  end
60
82
 
61
83
  def with(attrs)
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.3"
3
3
  end
4
4
 
@@ -0,0 +1,10 @@
1
+ require "bard/ci"
2
+
3
+ describe Bard::CI do
4
+ subject { described_class.new("tracker", "master") }
5
+
6
+ describe "#exists?"
7
+ describe "#status"
8
+ describe "#console"
9
+ describe "#run"
10
+ end