bard 0.68.0 → 0.69.1

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: 9d6ffeb50c8bcafe4462e42e462d34c81a1d44690c38cef2b44682e11fed4854
4
- data.tar.gz: 602a01d2fd94765904b0f767945149e8984d84856b2598f8a44afceda82ac62b
3
+ metadata.gz: 8e77aaa3b46702d144f1ed078868c7992c7283d5a97cc74113eb47de070a9ecc
4
+ data.tar.gz: e3f5210ab5741c06f5ee6a9010b885718895eec302b69cdb75c1f53b2a5d573f
5
5
  SHA512:
6
- metadata.gz: f5c40d167fe14e473dd56a3e14ccebe922068dcff113783b7074df31de8de100da799c10470a00779fc257cd85efa99568f8accf988886cafc7e0606f8423e6a
7
- data.tar.gz: 97fa25538c174668498f4a38de33b9cc17e4d60304675256a4a130d50920bfd814fc77aa48c76b88dda2d2dbe44648323cdc45736debe4b184201b3bf82e2a55
6
+ metadata.gz: f162644185e01bb6ee64adc4e4973868195a27fcc5c1e7fafdf40cab8f2b45bc8bd43b5f3210086549de543f01eec3f88487525bc1cc7f40f5034208731cedfb
7
+ data.tar.gz: b8ce44b788f1ccb4f057498ed61049a66d4f6e1019d44f97fac272933ee7aa1aba3198ca80e6256d8da7a3cb090793bfd4a35046d7cce1905d97dea3db14b02c
data/bard.gemspec CHANGED
@@ -9,7 +9,6 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Micah Geisel"]
10
10
  spec.email = ["micah@botandrose.com"]
11
11
  spec.summary = "CLI to automate common development tasks."
12
- spec.description = "CLI to automate common development tasks."
13
12
  spec.homepage = "http://github.com/botandrose/bard"
14
13
  spec.license = "MIT"
15
14
 
@@ -21,7 +20,6 @@ Gem::Specification.new do |spec|
21
20
  spec.add_dependency "thor", ">= 0.19.0"
22
21
  spec.add_dependency "rvm"
23
22
  spec.add_dependency "term-ansicolor", ">= 1.0.3"
24
- spec.add_dependency "bard-rake", ">= 0.19.0"
25
23
 
26
24
  spec.add_development_dependency "byebug"
27
25
  spec.add_development_dependency "rspec"
data/bin/bard CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- require "rubygems"
3
- require_relative "../lib/bard"
2
+ require_relative "../lib/bard/cli"
4
3
  Bard::CLI.start ARGV
5
4
 
@@ -1,8 +1,7 @@
1
- require "thor"
2
1
  require "time"
3
2
  require "bard/github"
4
3
 
5
- class Bard::CLI < Thor
4
+ module Bard
6
5
  class CI
7
6
  class GithubActions < Struct.new(:project_name, :branch, :sha)
8
7
  def run
@@ -1,6 +1,6 @@
1
1
  require "json"
2
2
 
3
- class Bard::CLI < Thor
3
+ module Bard
4
4
  class CI
5
5
  class Jenkins < Struct.new(:project_name, :branch, :sha)
6
6
  def run
data/lib/bard/ci/local.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "open3"
2
2
 
3
- class Bard::CLI < Thor
3
+ module Bard
4
4
  class CI
5
5
  class Local < Struct.new(:project_name, :branch, :sha)
6
6
  def run
data/lib/bard/ci.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "forwardable"
2
2
 
3
- class Bard::CLI < Thor
3
+ module Bard
4
4
  class CI
5
5
  def initialize project_name, branch, local: false
6
6
  @project_name = project_name
data/lib/bard/cli.rb ADDED
@@ -0,0 +1,262 @@
1
+ # this file gets loaded in the CLI context, not the Rails boot context
2
+
3
+ require "thor"
4
+ require "bard/git"
5
+ require "bard/ci"
6
+ require "bard/copy"
7
+ require "bard/github"
8
+ require "bard/ping"
9
+ require "bard/config"
10
+ require "bard/command"
11
+ require "term/ansicolor"
12
+ require "open3"
13
+ require "uri"
14
+
15
+ module Bard
16
+ class CLI < Thor
17
+ include Term::ANSIColor
18
+
19
+ class_option :verbose, type: :boolean, aliases: :v
20
+
21
+ desc "data --from=production --to=local", "copy database and assets from from to to"
22
+ option :from, default: "production"
23
+ option :to, default: "local"
24
+ def data
25
+ from = config[options[:from]]
26
+ to = config[options[:to]]
27
+
28
+ if to.key == :production
29
+ url = to.ping.first
30
+ puts yellow "WARNING: You are about to push data to production, overwriting everything that is there!"
31
+ answer = ask("If you really want to do this, please type in the full HTTPS url of the production server:")
32
+ if answer != url
33
+ puts red("!!! ") + "Failed! We expected #{url}. Is this really where you want to overwrite all the data?"
34
+ exit 1
35
+ end
36
+ end
37
+
38
+ puts "Dumping #{from.key} database to file..."
39
+ from.run! "bin/rake db:dump"
40
+
41
+ puts "Transfering file from #{from.key} to #{to.key}..."
42
+ from.copy_file "db/data.sql.gz", to: to, verbose: true
43
+
44
+ puts "Loading file into #{to.key} database..."
45
+ to.run! "bin/rake db:load"
46
+
47
+ config.data.each do |path|
48
+ puts "Synchronizing files in #{path}..."
49
+ from.copy_dir path, to: to, verbose: true
50
+ end
51
+ end
52
+
53
+ desc "stage [branch=HEAD]", "pushes current branch, and stages it"
54
+ def stage branch=Git.current_branch
55
+ unless config.servers.key?(:production)
56
+ 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.")
57
+ end
58
+
59
+ run! "git push -u origin #{branch}", verbose: true
60
+ config[:staging].run! "git fetch && git checkout -f origin/#{branch} && bin/setup"
61
+ puts green("Stage Succeeded")
62
+
63
+ ping :staging
64
+ end
65
+
66
+ option :"skip-ci", type: :boolean
67
+ option :"local-ci", type: :boolean
68
+ 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."
69
+ def deploy to=:production
70
+ branch = Git.current_branch
71
+
72
+ if branch == "master"
73
+ if !Git.up_to_date_with_remote?(branch)
74
+ run! "git push origin #{branch}:#{branch}"
75
+ end
76
+ invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
77
+
78
+ else
79
+ run! "git fetch origin master:master"
80
+
81
+ unless Git.fast_forward_merge?("origin/master", branch)
82
+ puts "The master branch has advanced. Attempting rebase..."
83
+ run! "git rebase origin/master"
84
+ end
85
+
86
+ run! "git push -f origin #{branch}:#{branch}"
87
+
88
+ invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
89
+
90
+ run! "git push origin #{branch}:master"
91
+ run! "git fetch origin master:master"
92
+ end
93
+
94
+ if `git remote` =~ /\bgithub\b/
95
+ run! "git push github"
96
+ end
97
+
98
+ config[to].run! "git pull origin master && bin/setup"
99
+
100
+ puts green("Deploy Succeeded")
101
+
102
+ if branch != "master"
103
+ puts "Deleting branch: #{branch}"
104
+ run! "git push --delete origin #{branch}"
105
+
106
+ if branch == Git.current_branch
107
+ run! "git checkout master"
108
+ end
109
+
110
+ run! "git branch -D #{branch}"
111
+ end
112
+
113
+ ping to
114
+ end
115
+
116
+ option :"local-ci", type: :boolean
117
+ option :status, type: :boolean
118
+ desc "ci [branch=HEAD]", "runs ci against BRANCH"
119
+ def ci branch=Git.current_branch
120
+ ci = CI.new(project_name, branch, local: options["local-ci"])
121
+ if ci.exists?
122
+ return puts ci.status if options["status"]
123
+
124
+ puts "Continuous integration: starting build on #{branch}..."
125
+
126
+ success = ci.run do |elapsed_time, last_time|
127
+ if last_time
128
+ percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
129
+ output = " Estimated completion: #{percentage}%"
130
+ else
131
+ output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
132
+ end
133
+ print "\x08" * output.length
134
+ print output
135
+ $stdout.flush
136
+ end
137
+
138
+ if success
139
+ puts
140
+ puts "Continuous integration: success!"
141
+ puts "Deploying..."
142
+ else
143
+ puts
144
+ puts ci.last_response
145
+ puts ci.console
146
+ puts red("Automated tests failed!")
147
+ exit 1
148
+ end
149
+
150
+ else
151
+ puts red("No CI found for #{project_name}!")
152
+ puts "Re-run with --skip-ci to bypass CI, if you absolutely must, and know what you're doing."
153
+ exit 1
154
+ end
155
+ end
156
+
157
+ desc "open [server=production]", "opens the url in the web browser."
158
+ def open server=:production
159
+ exec "xdg-open #{config[server].ping.first}"
160
+ end
161
+
162
+ desc "hurt <command>", "reruns a command until it fails"
163
+ def hurt *args
164
+ 1.upto(Float::INFINITY) do |count|
165
+ puts "Running attempt #{count}"
166
+ system *args
167
+ unless $?.success?
168
+ puts "Ran #{count-1} times before failing"
169
+ break
170
+ end
171
+ end
172
+ end
173
+
174
+ option :home, type: :boolean
175
+ desc "ssh [to=production]", "logs into the specified server via SSH"
176
+ def ssh to=:production
177
+ config[to].exec! "exec $SHELL -l", home: options[:home]
178
+ end
179
+
180
+ desc "install", "copies bin/setup and bin/ci scripts into current project."
181
+ def install
182
+ install_files_path = File.expand_path(File.join(__dir__, "../../install_files/*"))
183
+ system "cp -R #{install_files_path} bin/"
184
+ github_files_path = File.expand_path(File.join(__dir__, "../../install_files/.github"))
185
+ system "cp -R #{github_files_path} ./"
186
+ end
187
+
188
+ desc "setup", "installs app in nginx"
189
+ def setup
190
+ path = "/etc/nginx/sites-available/#{project_name}"
191
+ dest_path = path.sub("sites-available", "sites-enabled")
192
+ server_name = "#{project_name}.localhost"
193
+
194
+ system "sudo tee #{path} >/dev/null <<-EOF
195
+ server {
196
+ listen 80;
197
+ server_name #{server_name};
198
+
199
+ root #{Dir.pwd}/public;
200
+ passenger_enabled on;
201
+
202
+ location ~* \\.(ico|css|js|gif|jp?g|png|webp) {
203
+ access_log off;
204
+ if (\\$request_filename ~ \"-[0-9a-f]{32}\\.\") {
205
+ expires max;
206
+ add_header Cache-Control public;
207
+ }
208
+ }
209
+ gzip_static on;
210
+ }
211
+ EOF"
212
+ system "sudo ln -sf #{path} #{dest_path}" if !File.exist?(dest_path)
213
+ system "sudo service nginx restart"
214
+ end
215
+
216
+ desc "ping [server=production]", "hits the server over http to verify that its up."
217
+ def ping server=:production
218
+ server = config[server]
219
+ down_urls = Bard::Ping.call(config[server])
220
+ down_urls.each { |url| puts "#{url} is down!" }
221
+ exit 1 if down_urls.any?
222
+ end
223
+
224
+ option :on, default: "production"
225
+ desc "command <command> --on=production", "run the given command on the remote server"
226
+ def command command
227
+ server = config[options[:on]]
228
+ server.run! remote_command, verbose: true
229
+ end
230
+
231
+ desc "master_key --from=production --to=local", "copy master key from from to to"
232
+ option :from, default: "production"
233
+ option :to, default: "local"
234
+ def master_key
235
+ from = config[options[:from]]
236
+ to = config[options[:to]]
237
+ from.copy_file "config/master.key", to:
238
+ end
239
+
240
+ desc "vim [branch=master]", "open all files that have changed since master"
241
+ def vim branch="master"
242
+ exec "vim -p `git diff #{branch} --name-only | grep -v sass$ | tac`"
243
+ end
244
+
245
+ def self.exit_on_failure? = true
246
+
247
+ private
248
+
249
+ def config
250
+ @config ||= Bard::Config.new(project_name, path: "bard.rb")
251
+ end
252
+
253
+ def project_name
254
+ @project_name ||= File.expand_path(".").split("/").last
255
+ end
256
+
257
+ def run!(...)
258
+ Bard::Command.run!(...)
259
+ end
260
+ end
261
+ end
262
+
@@ -0,0 +1,64 @@
1
+ module Bard
2
+ class Command < Struct.new(:command, :on, :home)
3
+ def self.run! command, on: :local, home: false, verbose: false
4
+ new(command, on, home).run! verbose: verbose
5
+ end
6
+
7
+ def self.exec! command, on: :local, home: false
8
+ new(command, on, home).exec!
9
+ end
10
+
11
+ def run! verbose: false
12
+ failed = false
13
+
14
+ if verbose
15
+ failed = !(system full_command)
16
+ else
17
+ stdout, stderr, status = Open3.capture3(full_command)
18
+ failed = status.to_i.nonzero?
19
+ if failed
20
+ $stdout.puts stdout
21
+ $stderr.puts stderr
22
+ end
23
+ end
24
+
25
+ if failed
26
+ raise "Running command failed: #{full_command}"
27
+ # puts red("!!! ") + "Running command failed: #{yellow(command)}"
28
+ # exit 1
29
+ end
30
+ end
31
+
32
+ def exec!
33
+ exec full_command
34
+ end
35
+
36
+ private
37
+
38
+ def full_command
39
+ if on.to_sym == :local
40
+ command
41
+ else
42
+ remote_command
43
+ end
44
+ end
45
+
46
+ def remote_command
47
+ uri = URI.parse("ssh://#{on.ssh}")
48
+ ssh_key = on.ssh_key ? "-i #{on.ssh_key} " : ""
49
+ cmd = command
50
+ if on.env
51
+ cmd = "#{on.env} #{command}"
52
+ end
53
+ unless home
54
+ cmd = "cd #{on.path} && #{cmd}"
55
+ end
56
+ cmd = "ssh -tt #{ssh_key}#{"-p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} '#{cmd}'"
57
+ if on.gateway
58
+ uri = URI.parse("ssh://#{on.gateway}")
59
+ cmd = "ssh -tt #{" -p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} \"#{cmd}\""
60
+ end
61
+ cmd
62
+ end
63
+ end
64
+ end
data/lib/bard/config.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require "uri"
2
+ require "bard/command"
3
+ require "bard/copy"
2
4
 
3
5
  module Bard
4
6
  class Config
@@ -48,11 +50,20 @@ module Bard
48
50
  attr_reader :project_name, :servers
49
51
 
50
52
  def server key, &block
53
+ key = key.to_sym
51
54
  @servers[key] ||= Server.new(project_name, key)
52
55
  @servers[key].instance_eval &block if block_given?
53
56
  @servers[key]
54
57
  end
55
58
 
59
+ def [] key
60
+ key = key.to_sym
61
+ if @servers[key].nil? && key == :production
62
+ key = :staging
63
+ end
64
+ @servers[key]
65
+ end
66
+
56
67
  def data *paths
57
68
  if paths.length == 0
58
69
  Array(@data)
@@ -123,6 +134,26 @@ module Bard
123
134
  raise ArgumentError
124
135
  end
125
136
  end
137
+
138
+ def to_sym
139
+ key
140
+ end
141
+
142
+ def run! command, home: false, verbose: false
143
+ Bard::Command.run! command, on: self, home:, verbose:
144
+ end
145
+
146
+ def exec! command, home: false
147
+ Bard::Command.exec! command, on: self, home:
148
+ end
149
+
150
+ def copy_file path, to:, verbose: false
151
+ Bard::Copy.file path, from: self, to:, verbose:
152
+ end
153
+
154
+ def copy_dir
155
+ Bard::Copy.dir path, from: self, to:, verbose:
156
+ end
126
157
  end
127
158
  end
128
159
  end
data/lib/bard/copy.rb ADDED
@@ -0,0 +1,101 @@
1
+ module Bard
2
+ class Copy < Struct.new(:path, :from, :to, :verbose)
3
+ def self.file path, from:, to:, verbose: false
4
+ new(path, from, to, verbose).scp
5
+ end
6
+
7
+ def self.dir path, from:, to:, verbose: false
8
+ new(path, from, to, verbose).rsync
9
+ end
10
+
11
+ def scp
12
+ if from.key == :local
13
+ scp_using_local :to, to
14
+ elsif to.key == :local
15
+ scp_using_local :from, from
16
+ else
17
+ scp_as_mediator
18
+ end
19
+ end
20
+
21
+ 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
+
26
+ ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
27
+
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
+
32
+ from_and_to.reverse! if direction == :from
33
+ command = "scp #{gateway} #{ssh_key} #{port} #{from_and_to.join(" ")}"
34
+
35
+ Bard::Command.run! command, verbose: verbose
36
+ end
37
+
38
+ def scp_as_mediator
39
+ 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
+
49
+ Bard::Command.run! command, verbose: verbose
50
+ end
51
+
52
+ def rsync
53
+ if from.key == :local
54
+ rsync_using_local :to, to
55
+ elsif to.key == :local
56
+ rsync_using_local :from, from
57
+ else
58
+ rsync_as_mediator from, to
59
+ end
60
+ end
61
+
62
+ 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\"" : ""
66
+
67
+ 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}'"
71
+
72
+ dest_path = path.dup
73
+ dest_path = "./#{dest_path}"
74
+ from_and_to = [dest_path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
75
+ from_and_to.reverse! if direction == :from
76
+ from_and_to[-1].sub! %r(/[^/]+$), '/'
77
+
78
+ command = "rsync #{ssh} --delete --info=progress2 -az #{from_and_to.join(" ")}"
79
+
80
+ Bard::Command.run! command, verbose: verbose
81
+ end
82
+
83
+ def rsync_as_mediator from, to
84
+ raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
85
+
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}')
97
+
98
+ Bard::Command.run! command, verbose: verbose
99
+ end
100
+ end
101
+ end
data/lib/bard/git.rb CHANGED
@@ -1,29 +1,31 @@
1
- module Bard::CLI::Git
2
- module_function
1
+ module Bard
2
+ module Git
3
+ module_function
3
4
 
4
- def current_branch
5
- ref = `git symbolic-ref HEAD 2>&1`.chomp
6
- return false if ref =~ /^fatal:/
7
- ref.sub(/refs\/heads\//, '') # refs/heads/master ... we want "master"
8
- end
5
+ def current_branch
6
+ ref = `git symbolic-ref HEAD 2>&1`.chomp
7
+ return false if ref =~ /^fatal:/
8
+ ref.sub(/refs\/heads\//, '') # refs/heads/master ... we want "master"
9
+ end
9
10
 
10
- def current_sha
11
- sha_of("HEAD")
12
- end
11
+ def current_sha
12
+ sha_of("HEAD")
13
+ end
13
14
 
14
- def fast_forward_merge?(root, branch)
15
- root_head = sha_of(root)
16
- branch_head = sha_of(branch)
17
- common_ancestor = `git merge-base #{root_head} #{branch_head}`.chomp
18
- common_ancestor == root_head
19
- end
15
+ def fast_forward_merge?(root, branch)
16
+ root_head = sha_of(root)
17
+ branch_head = sha_of(branch)
18
+ common_ancestor = `git merge-base #{root_head} #{branch_head}`.chomp
19
+ common_ancestor == root_head
20
+ end
20
21
 
21
- def up_to_date_with_remote? branch
22
- sha_of(branch) == sha_of("origin/#{branch}")
23
- end
22
+ def up_to_date_with_remote? branch
23
+ sha_of(branch) == sha_of("origin/#{branch}")
24
+ end
24
25
 
25
- def sha_of ref
26
- `git rev-parse #{ref}`.chomp
26
+ def sha_of ref
27
+ `git rev-parse #{ref}`.chomp
28
+ end
27
29
  end
28
30
  end
29
31
 
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "0.68.0"
2
+ VERSION = "0.69.1"
3
3
  end
4
4
 
data/lib/bard.rb CHANGED
@@ -1,253 +1,3 @@
1
- module Bard; end
2
-
3
- require "bard/base"
4
- require "bard/git"
5
- require "bard/ci"
6
- require "bard/data"
7
- require "bard/github"
8
- require "bard/ping"
9
- require "bard/config"
10
- require "bard/remote_command"
11
-
12
- class Bard::CLI < Thor
13
- include Thor::Actions
14
-
15
- def initialize(*args, **kwargs, &block)
16
- super
17
- @config = Bard::Config.new(project_name, path: "bard.rb")
18
- end
19
-
20
- desc "data --from=production --to=local", "copy database and assets from from to to"
21
- method_options %w[from] => :string, %w[to] => :string
22
- def data
23
- default_from = @config.servers.key?(:production) ? "production" : "staging"
24
- from = options.fetch(:from, default_from)
25
- to = options.fetch(:to, "local")
26
- Data.new(self, from, to).call
27
- end
28
-
29
- method_options %w( verbose -v ) => :boolean
30
- desc "stage [BRANCH=HEAD]", "pushes current branch, and stages it"
31
- def stage branch=Git.current_branch
32
- unless @config.servers.key?(:production)
33
- 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.")
34
- end
35
-
36
- run_crucial "git push -u origin #{branch}", verbose: true
37
- command = "git fetch && git checkout -f origin/#{branch} && bin/setup"
38
- run_crucial ssh_command(:staging, command)
39
- puts green("Stage Succeeded")
40
-
41
- ping :staging
42
- end
43
-
44
- method_options %w[verbose -v] => :boolean, %w[skip-ci] => :boolean, %w[local-ci -l] => :boolean
45
- 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."
46
- def deploy to=nil
47
- branch = Git.current_branch
48
-
49
- if branch == "master"
50
- run_crucial "git push origin #{branch}:#{branch}" if !Git.up_to_date_with_remote?(branch)
51
- invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
52
-
53
- else
54
- run_crucial "git fetch origin master:master"
55
-
56
- unless Git.fast_forward_merge?("origin/master", branch)
57
- puts "The master branch has advanced. Attempting rebase..."
58
- run_crucial "git rebase origin/master"
59
- end
60
-
61
- run_crucial "git push -f origin #{branch}:#{branch}"
62
-
63
- invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
64
-
65
- run_crucial "git push origin #{branch}:master"
66
- run_crucial "git fetch origin master:master"
67
- end
68
-
69
- if `git remote` =~ /\bgithub\b/
70
- run_crucial "git push github"
71
- end
72
-
73
- to ||= @config.servers.key?(:production) ? :production : :staging
74
-
75
- command = "git pull origin master && bin/setup"
76
- run_crucial ssh_command(to, command)
77
-
78
- puts green("Deploy Succeeded")
79
-
80
- if branch != "master"
81
- puts "Deleting branch: #{branch}"
82
- run_crucial "git push --delete origin #{branch}"
83
-
84
- if branch == Git.current_branch
85
- run_crucial "git checkout master"
86
- end
87
-
88
- run_crucial "git branch -D #{branch}"
89
- end
90
-
91
- ping to
92
- end
93
-
94
- method_options %w[verbose -v] => :boolean, %w[local-ci -l] => :boolean, %w[status -s] => :boolean
95
- desc "ci [BRANCH=HEAD]", "runs ci against BRANCH"
96
- def ci branch=Git.current_branch
97
- ci = CI.new(project_name, branch, local: options["local-ci"])
98
- if ci.exists?
99
- return puts ci.status if options["status"]
100
-
101
- puts "Continuous integration: starting build on #{branch}..."
102
-
103
- success = ci.run do |elapsed_time, last_time|
104
- if last_time
105
- percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
106
- output = " Estimated completion: #{percentage}%"
107
- else
108
- output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
109
- end
110
- print "\x08" * output.length
111
- print output
112
- $stdout.flush
113
- end
114
-
115
- if success
116
- puts
117
- puts "Continuous integration: success!"
118
- if ci.jenkins? && File.exist?("coverage")
119
- puts "Downloading test coverage from CI..."
120
- download_ci_test_coverage
121
- end
122
- puts "Deploying..."
123
- else
124
- puts
125
- puts ci.last_response
126
- puts ci.console
127
- puts red("Automated tests failed!")
128
- exit 1
129
- end
130
-
131
- else
132
- puts red("No CI found for #{project_name}!")
133
- puts "Re-run with --skip-ci to bypass CI, if you absolutely must, and know what you're doing."
134
- exit 1
135
- end
136
- end
137
-
138
- desc "open [SERVER=production]", "opens the url in the web browser."
139
- def open server=nil
140
- server ||= @config.servers.key?(:production) ? :production : :staging
141
- server = @config.servers[server.to_sym]
142
- exec "xdg-open #{server.ping.first}"
143
- end
144
-
145
- desc "hurt", "reruns a command until it fails"
146
- def hurt *args
147
- 1.upto(Float::INFINITY) do |count|
148
- puts "Running attempt #{count}"
149
- system *args
150
- unless $?.success?
151
- puts "Ran #{count-1} times before failing"
152
- break
153
- end
154
- end
155
- end
156
-
157
- method_options %w[home] => :boolean
158
- desc "ssh [TO=production]", "logs into the specified server via SSH"
159
- def ssh to=:production
160
- command = "exec $SHELL -l"
161
- if to == "theia" && !options["home"]
162
- server = @config.servers[:theia]
163
- command = %(bash -lic "exec ./vagrant \\"cd #{server.path} && #{command}\\"")
164
- exec ssh_command(to, command, home: true)
165
- else
166
- exec ssh_command(to, command, home: options["home"])
167
- end
168
- end
169
-
170
- desc "install", "copies bin/setup and bin/ci scripts into current project."
171
- def install
172
- install_files_path = File.expand_path(File.join(__dir__, "../install_files/*"))
173
- system "cp -R #{install_files_path} bin/"
174
- github_files_path = File.expand_path(File.join(__dir__, "../install_files/.github"))
175
- system "cp -R #{github_files_path} ./"
176
- end
177
-
178
- desc "setup", "installs app in nginx"
179
- def setup
180
- path = "/etc/nginx/sites-available/#{project_name}"
181
- dest_path = path.sub("sites-available", "sites-enabled")
182
- server_name = "#{project_name}.localhost"
183
-
184
- create_file path, <<~NGINX
185
- server {
186
- listen 80;
187
- server_name #{server_name};
188
-
189
- root #{Dir.pwd}/public;
190
- passenger_enabled on;
191
-
192
- location ~* \\.(ico|css|js|gif|jp?g|png|webp) {
193
- access_log off;
194
- if ($request_filename ~ "-[0-9a-f]{32}\\.") {
195
- expires max;
196
- add_header Cache-Control public;
197
- }
198
- }
199
- gzip_static on;
200
- }
201
- NGINX
202
-
203
- FileUtils.ln_sf(path, dest_path) if !File.exist?(dest_path)
204
- run "service nginx restart"
205
- rescue Errno::EACCES
206
- raise InvocationError.new("please re-run with sudo")
207
- end
208
-
209
- desc "ping [SERVER=production]", "hits the server over http to verify that its up."
210
- def ping server=:production
211
- server = @config.servers[server.to_sym]
212
- down_urls = Bard::Ping.call(server)
213
- down_urls.each { |url| puts "#{url} is down!" }
214
- exit 1 if down_urls.any?
215
- end
216
-
217
- desc "command <command> --on=production", "run the given command on the remote server"
218
- method_options %w[on] => :string
219
- def command command
220
- default_from = @config.servers.key?(:production) ? "production" : "staging"
221
- on = options.fetch(:on, default_from)
222
- server = @config.servers[on.to_sym]
223
- remote_command = Bard::RemoteCommand.new(server, command).local_command
224
- run_crucial remote_command, verbose: true
225
- end
226
-
227
- desc "master_key --from=production --to=local", "copy master key from from to to"
228
- method_options %w[from] => :string, %w[to] => :string
229
- def master_key
230
- default_from = @config.servers.key?(:production) ? "production" : "staging"
231
- from = options.fetch(:from, default_from)
232
- to = options.fetch(:to, "local")
233
- if to == "local"
234
- copy :from, from, "config/master.key"
235
- end
236
- if from == "local"
237
- copy :to, to, "config/master.key"
238
- end
239
- end
240
-
241
- desc "download_ci_test_coverage", "download latest test coverage information from CI"
242
- def download_ci_test_coverage
243
- rsync :from, :ci, "coverage"
244
- end
245
-
246
- desc "vim", "open all files that have changed since master"
247
- def vim branch="master"
248
- exec "vim -p `git diff #{branch} --name-only | grep -v sass$ | tac`"
249
- end
250
-
251
- def self.exit_on_failure? = true
1
+ module Bard
252
2
  end
253
3
 
@@ -1,6 +1,6 @@
1
1
  require "bard/ci/github_actions"
2
2
 
3
- describe Bard::CLI::CI::GithubActions do
3
+ describe Bard::CI::GithubActions do
4
4
  subject { described_class.new("metrc", "master", "0966308e204b256fdcc11457eb53306d84884c60") }
5
5
 
6
6
  xit "works" do
@@ -8,7 +8,7 @@ describe Bard::CLI::CI::GithubActions do
8
8
  end
9
9
  end
10
10
 
11
- describe Bard::CLI::CI::GithubActions::API do
11
+ describe Bard::CI::GithubActions::API do
12
12
  subject { described_class.new("metrc") }
13
13
 
14
14
  describe "#last_successful_run" do
@@ -29,7 +29,7 @@ describe Bard::CLI::CI::GithubActions::API do
29
29
  end
30
30
  end
31
31
 
32
- describe Bard::CLI::CI::GithubActions::Client do
32
+ describe Bard::Github do
33
33
  subject { described_class.new("metrc") }
34
34
  end
35
35
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.68.0
4
+ version: 0.69.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-15 00:00:00.000000000 Z
11
+ date: 2024-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 1.0.3
55
- - !ruby/object:Gem::Dependency
56
- name: bard-rake
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: 0.19.0
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: 0.19.0
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: byebug
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +94,7 @@ dependencies:
108
94
  - - ">="
109
95
  - !ruby/object:Gem::Version
110
96
  version: '0'
111
- description: CLI to automate common development tasks.
97
+ description:
112
98
  email:
113
99
  - micah@botandrose.com
114
100
  executables:
@@ -147,17 +133,17 @@ files:
147
133
  - install_files/specified_bundler.rb
148
134
  - install_files/specified_ruby.rb
149
135
  - lib/bard.rb
150
- - lib/bard/base.rb
151
136
  - lib/bard/ci.rb
152
137
  - lib/bard/ci/github_actions.rb
153
138
  - lib/bard/ci/jenkins.rb
154
139
  - lib/bard/ci/local.rb
140
+ - lib/bard/cli.rb
141
+ - lib/bard/command.rb
155
142
  - lib/bard/config.rb
156
- - lib/bard/data.rb
143
+ - lib/bard/copy.rb
157
144
  - lib/bard/git.rb
158
145
  - lib/bard/github.rb
159
146
  - lib/bard/ping.rb
160
- - lib/bard/remote_command.rb
161
147
  - lib/bard/version.rb
162
148
  - spec/bard/ci/github_actions_spec.rb
163
149
  - spec/bard_spec.rb
data/lib/bard/base.rb DELETED
@@ -1,119 +0,0 @@
1
- require "thor"
2
- require "term/ansicolor"
3
- require "open3"
4
- require "uri"
5
- require "bard/remote_command"
6
-
7
- class Bard::CLI < Thor
8
- include Term::ANSIColor
9
-
10
- private
11
-
12
- def run_crucial command, verbose: false
13
- failed = false
14
-
15
- if verbose
16
- failed = !(system command)
17
- else
18
- stdout, stderr, status = Open3.capture3(command)
19
- failed = status.to_i.nonzero?
20
- if failed
21
- $stdout.puts stdout
22
- $stderr.puts stderr
23
- end
24
- end
25
-
26
- if failed
27
- puts red("!!! ") + "Running command failed: #{yellow(command)}"
28
- exit 1
29
- end
30
- end
31
-
32
- def project_name
33
- @project_name ||= File.expand_path(".").split("/").last
34
- end
35
-
36
- def ssh_command server_name, command, home: false
37
- server = @config.servers.fetch(server_name.to_sym)
38
- Bard::RemoteCommand.new(server, command, home).local_command
39
- end
40
-
41
- def copy direction, server_name, path, verbose: false
42
- server = @config.servers.fetch(server_name.to_sym)
43
-
44
- uri = URI.parse("ssh://#{server.gateway}")
45
- port = uri.port ? "-p#{uri.port}" : ""
46
- gateway = server.gateway ? "-oProxyCommand='ssh #{port} #{uri.user}@#{uri.host} -W %h:%p'" : ""
47
-
48
- ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
49
-
50
- uri = URI.parse("ssh://#{server.ssh}")
51
- port = uri.port ? "-P#{uri.port}" : ""
52
- from_and_to = [path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
53
-
54
- from_and_to.reverse! if direction == :from
55
- command = "scp #{gateway} #{ssh_key} #{port} #{from_and_to.join(" ")}"
56
-
57
- run_crucial command, verbose: verbose
58
- end
59
-
60
- def move from_name, to_name, path, verbose: false
61
- from = @config.servers.fecth(from_name.to_sym)
62
- to = @config.servers.fetch(to_name.to_sym)
63
- raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
64
-
65
- from_uri = URI.parse("ssh://#{from.ssh}")
66
- from_str = "scp://#{from_uri.user}@#{from_uri.host}:#{from_uri.port || 22}/#{from.path}/#{path}"
67
-
68
- to_uri = URI.parse("ssh://#{to.ssh}")
69
- to_str = "scp://#{to_uri.user}@#{to_uri.host}:#{to_uri.port || 22}/#{to.path}/#{path}"
70
-
71
- command = "scp -o ForwardAgent=yes #{from_str} #{to_str}"
72
-
73
- run_crucial command, verbose: verbose
74
- end
75
-
76
- def rsync direction, server_name, path, verbose: false
77
- server = @config.servers.fetch(server_name.to_sym)
78
-
79
- uri = URI.parse("ssh://#{server.gateway}")
80
- port = uri.port ? "-p#{uri.port}" : ""
81
- gateway = server.gateway ? "-oProxyCommand=\"ssh #{port} #{uri.user}@#{uri.host} -W %h:%p\"" : ""
82
-
83
- ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
84
- uri = URI.parse("ssh://#{server.ssh}")
85
- port = uri.port ? "-p#{uri.port}" : ""
86
- ssh = "-e'ssh #{ssh_key} #{port} #{gateway}'"
87
-
88
- dest_path = path.dup
89
- dest_path = "./#{dest_path}"
90
- from_and_to = [dest_path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
91
- from_and_to.reverse! if direction == :from
92
- from_and_to[-1].sub! %r(/[^/]+$), '/'
93
-
94
- command = "rsync #{ssh} --delete --info=progress2 -az #{from_and_to.join(" ")}"
95
-
96
- run_crucial command, verbose: verbose
97
- end
98
-
99
- def rsync_remote from_name, to_name, path, verbose: false
100
- from = @config.servers.fetch(from_name.to_sym)
101
- to = @config.servers.fetch(to_name.to_sym)
102
- raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
103
-
104
- dest_path = path.dup
105
- dest_path = "./#{dest_path}"
106
-
107
- from_uri = URI.parse("ssh://#{from.ssh}")
108
- from_str = "-p#{from_uri.port || 22} #{from_uri.user}@#{from_uri.host}"
109
-
110
- to_uri = URI.parse("ssh://#{to.ssh}")
111
- to_str = "#{to_uri.user}@#{to_uri.host}:#{to.path}/#{path}"
112
- to_str.sub! %r(/[^/]+$), '/'
113
-
114
- 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}')
115
-
116
- run_crucial command, verbose: verbose
117
- end
118
- end
119
-
data/lib/bard/data.rb DELETED
@@ -1,96 +0,0 @@
1
- class Bard::CLI < Thor
2
- class Data < Struct.new(:bard, :from, :to)
3
- def call
4
- if to == "production"
5
- server = bard.instance_variable_get(:@config).servers[to.to_sym]
6
- url = server.ping.first
7
- puts bard.yellow("WARNING: You are about to push data to production, overwriting everything that is there!")
8
- answer = bard.ask("If you really want to do this, please type in the full HTTPS url of the production server:")
9
- if answer != url
10
- puts bard.red("!!! ") + "Failed! We expected #{url}. Is this really where you want to overwrite all the data?"
11
- exit 1
12
- end
13
- end
14
-
15
- if to == "local"
16
- data_pull_db from.to_sym
17
- data_pull_assets from.to_sym
18
- elsif from == "local"
19
- data_push_db to.to_sym
20
- data_push_assets to.to_sym
21
- else
22
- data_move_db from.to_sym, to.to_sym
23
- data_move_assets from.to_sym, to.to_sym
24
- end
25
- end
26
-
27
- private
28
-
29
- def data_pull_db server
30
- bard.instance_eval do
31
- puts "Dumping remote database to file..."
32
- run_crucial ssh_command(server, "bin/rake db:dump")
33
-
34
- puts "Downloading file..."
35
- copy :from, server, "db/data.sql.gz", verbose: true
36
-
37
- puts "Loading file into local database..."
38
- run_crucial "bin/rake db:load"
39
- end
40
- end
41
-
42
- def data_push_db server
43
- bard.instance_eval do
44
- puts "Dumping local database to file..."
45
- run_crucial "bin/rake db:dump"
46
-
47
- puts "Uploading file..."
48
- copy :to, server, "db/data.sql.gz", verbose: true
49
-
50
- puts "Loading file into remote database..."
51
- run_crucial ssh_command(server, "bin/rake db:load")
52
- end
53
- end
54
-
55
- def data_move_db from, to
56
- bard.instance_eval do
57
- puts "Dumping local database to file..."
58
- run_crucial ssh_command(from, "bin/rake db:load")
59
-
60
- puts "Uploading file..."
61
- move from, to, "db/data.sql.gz", verbose: true
62
-
63
- puts "Loading file into remote database..."
64
- run_crucial ssh_command(to, "bin/rake db:load")
65
- end
66
- end
67
-
68
- def data_pull_assets server
69
- bard.instance_eval do
70
- @config.data.each do |path|
71
- puts "Downloading files..."
72
- rsync :from, server, path, verbose: true
73
- end
74
- end
75
- end
76
-
77
- def data_push_assets server
78
- bard.instance_eval do
79
- @config.data.each do |path|
80
- puts "Uploading files..."
81
- rsync :to, server, path, verbose: true
82
- end
83
- end
84
- end
85
-
86
- def data_move_assets from, to
87
- bard.instance_eval do
88
- @config.data.each do |path|
89
- puts "Copying files..."
90
- rsync_remote from, to, path, verbose: true
91
- end
92
- end
93
- end
94
- end
95
- end
96
-
@@ -1,45 +0,0 @@
1
- module Bard
2
- class RemoteCommand < Struct.new(:server, :command, :home)
3
- def self.run! *args
4
- new(*args).run!
5
- end
6
-
7
- def local_command
8
- uri = URI.parse("ssh://#{server.ssh}")
9
- ssh_key = server.ssh_key ? "-i #{server.ssh_key} " : ""
10
- cmd = command
11
- if server.env
12
- cmd = "#{server.env} #{command}"
13
- end
14
- unless home
15
- cmd = "cd #{server.path} && #{cmd}"
16
- end
17
- cmd = "ssh -tt #{ssh_key}#{"-p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} '#{cmd}'"
18
- if server.gateway
19
- uri = URI.parse("ssh://#{server.gateway}")
20
- cmd = "ssh -tt #{" -p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} \"#{cmd}\""
21
- end
22
- cmd
23
- end
24
-
25
- def run! verbose: false
26
- failed = false
27
-
28
- if verbose
29
- failed = !(system local_command)
30
- else
31
- stdout, stderr, status = Open3.capture3(local_command)
32
- failed = status.to_i.nonzero?
33
- if failed
34
- $stdout.puts stdout
35
- $stderr.puts stderr
36
- end
37
- end
38
-
39
- if failed
40
- raise "Running command failed: #{local_command}"
41
- end
42
- end
43
- end
44
- end
45
-