bosh-workspace 0.9.2 → 0.9.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.
- checksums.yaml +4 -4
- data/bosh-workspace.gemspec +5 -4
- data/lib/bosh/cli/commands/deployment_patch.rb +0 -1
- data/lib/bosh/cli/commands/prepare.rb +12 -11
- data/lib/bosh/workspace.rb +4 -3
- data/lib/bosh/workspace/credentials.rb +12 -5
- data/lib/bosh/workspace/git_credentials_provider.rb +80 -0
- data/lib/bosh/workspace/{git_remote_url.rb → helpers/git_protocol_helper.rb} +4 -8
- data/lib/bosh/workspace/helpers/project_deployment_helper.rb +10 -2
- data/lib/bosh/workspace/helpers/release_helper.rb +33 -7
- data/lib/bosh/workspace/manifest_builder.rb +6 -8
- data/lib/bosh/workspace/merge_tool.rb +73 -0
- data/lib/bosh/workspace/project_deployment.rb +20 -5
- data/lib/bosh/workspace/release.rb +103 -67
- data/lib/bosh/workspace/schemas/credentials.rb +22 -0
- data/lib/bosh/workspace/schemas/project_deployment.rb +14 -1
- data/lib/bosh/workspace/version.rb +1 -1
- data/spec/assets/bin/spruce +0 -0
- data/spec/assets/manifests-repo/deployments/foo.yml +1 -0
- data/spec/assets/manifests-repo/stubs/foo.yml +4 -0
- data/spec/commands/prepare_spec.rb +39 -12
- data/spec/credentials_spec.rb +8 -0
- data/spec/git_credentials_provider_spec.rb +82 -0
- data/spec/{git_remote_url_spec.rb → helpers/git_protocol_helper_spec.rb} +10 -11
- data/spec/helpers/project_deployment_helper_spec.rb +12 -1
- data/spec/helpers/release_helper_spec.rb +157 -73
- data/spec/manifest_builder_spec.rb +6 -5
- data/spec/merge_tool_spec.rb +98 -0
- data/spec/project_deployment_spec.rb +43 -1
- data/spec/release_spec.rb +369 -354
- data/spec/rugged_spec.rb +64 -0
- data/spec/schemas/credentials_spec.rb +22 -5
- data/spec/spec_helper.rb +1 -0
- metadata +35 -15
- data/lib/bosh/workspace/helpers/git_credentials_helper.rb +0 -111
- data/lib/bosh/workspace/helpers/spiff_helper.rb +0 -34
- data/spec/helpers/git_credentials_helper_spec.rb +0 -190
- data/spec/helpers/spiff_helper_spec.rb +0 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 095b4cec01561a415d908bc83c6a86e5a6861bac
|
4
|
+
data.tar.gz: 3038bc5ecd5756cf3672bbdc15405eee574a39c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 835a7af3d529d8264b273157ce4c2f80e5cdbe73ed3c8b817f469edebd874e730ff64110942b2f2aeb962930ac4fb0a3c39ef62445d9e0bb4c5ebbc04f32876c
|
7
|
+
data.tar.gz: 75e12938c90cea7260a8c2d16a6098d6c189bf9569431bf889e5cce7b23c70b57fafe297aa396b205e70cc54dc40adba7f47ec755007a9408cb60e3cd78bbd7d
|
data/bosh-workspace.gemspec
CHANGED
@@ -20,16 +20,17 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.required_ruby_version = '>= 2.0.0'
|
22
22
|
|
23
|
-
spec.add_runtime_dependency "bosh_cli",
|
24
|
-
spec.add_runtime_dependency "bosh_common",
|
23
|
+
spec.add_runtime_dependency "bosh_cli", ">= 1.2905.0"
|
24
|
+
spec.add_runtime_dependency "bosh_common", ">= 1.2905.0"
|
25
|
+
spec.add_runtime_dependency "bosh-template", ">= 1.2905.0"
|
25
26
|
spec.add_runtime_dependency "semi_semantic", "~> 1.1.0"
|
26
27
|
spec.add_runtime_dependency "membrane", "~> 1.1.0"
|
27
28
|
spec.add_runtime_dependency "hashdiff", "~> 0.2.1"
|
28
29
|
spec.add_runtime_dependency "rugged", "~> 0.23.0b3"
|
29
30
|
|
30
31
|
spec.add_development_dependency "bundler", "~> 1.6"
|
31
|
-
spec.add_development_dependency "rspec", "~> 3.
|
32
|
-
spec.add_development_dependency "rspec-its", '~> 1.
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.3.0"
|
33
|
+
spec.add_development_dependency "rspec-its", '~> 1.2.0'
|
33
34
|
spec.add_development_dependency "rake"
|
34
35
|
spec.add_development_dependency "archive-zip", "~> 0.7.0"
|
35
36
|
end
|
@@ -3,7 +3,6 @@ require "bosh/workspace"
|
|
3
3
|
module Bosh::Cli::Command
|
4
4
|
class DeploymentPatch < Base
|
5
5
|
include Bosh::Workspace::ProjectDeploymentHelper
|
6
|
-
include Bosh::Workspace::GitCredentialsHelper
|
7
6
|
|
8
7
|
usage "create deployment patch"
|
9
8
|
desc "Extract patch from the current directory and optionally writes to file"
|
@@ -5,15 +5,16 @@ module Bosh::Cli::Command
|
|
5
5
|
include Bosh::Cli::Validation
|
6
6
|
include Bosh::Workspace
|
7
7
|
include ProjectDeploymentHelper
|
8
|
-
include GitCredentialsHelper
|
9
8
|
include ReleaseHelper
|
10
9
|
include StemcellHelper
|
11
10
|
|
12
11
|
usage "prepare deployment"
|
13
12
|
desc "Resolve deployment requirements"
|
13
|
+
option "--local", "only perform local git operations (don't fetch remote)"
|
14
14
|
def prepare
|
15
15
|
require_project_deployment
|
16
16
|
auth_required
|
17
|
+
offline! if options[:local]
|
17
18
|
nl
|
18
19
|
prepare_release_repos
|
19
20
|
nl
|
@@ -28,19 +29,18 @@ module Bosh::Cli::Command
|
|
28
29
|
project_deployment_releases.each do |release|
|
29
30
|
require_git_url_error if release.git_url.nil?
|
30
31
|
say "Fetching release '#{release.name.make_green}' to satisfy template references"
|
31
|
-
fetch_or_clone_repo(release.repo_dir, release.git_url)
|
32
32
|
release.update_repo
|
33
|
-
release
|
34
|
-
fetch_or_clone_repo(File.join(release.repo_dir, submodule.path), submodule.url)
|
35
|
-
release.update_submodule(submodule)
|
36
|
-
end
|
37
|
-
msg = "Version '#{release.version.to_s.make_green}'"
|
38
|
-
msg = "Ref '#{release.ref.make_green}'" if release.ref
|
39
|
-
say "#{msg} has been checkout into:"
|
40
|
-
say "- #{release.repo_dir}"
|
33
|
+
print_prepare_release_repo_message(release)
|
41
34
|
end
|
42
35
|
end
|
43
36
|
|
37
|
+
def print_prepare_release_repo_message(release)
|
38
|
+
msg = "Version '#{release.version.to_s.make_green}'"
|
39
|
+
msg = "Ref '#{release.ref.make_green}'" if release.ref
|
40
|
+
say "#{msg} has been checkout into:"
|
41
|
+
say "- #{release.repo_dir}"
|
42
|
+
end
|
43
|
+
|
44
44
|
def require_git_url_error
|
45
45
|
say "`bosh prepare deployment' can not be used:"
|
46
46
|
err("`git:' is missing from `release:'".make_red)
|
@@ -58,7 +58,7 @@ module Bosh::Cli::Command
|
|
58
58
|
say "Skipping upload"
|
59
59
|
elsif release.url
|
60
60
|
say "Uploading '#{release.url}'"
|
61
|
-
|
61
|
+
release_upload_from_url(release.url)
|
62
62
|
else
|
63
63
|
say "Uploading '#{release.name_version.make_green}'"
|
64
64
|
release_upload(release.manifest_file, release.release_dir)
|
@@ -82,6 +82,7 @@ module Bosh::Cli::Command
|
|
82
82
|
|
83
83
|
def cached_stemcell_upload(stemcell)
|
84
84
|
unless stemcell.downloaded?
|
85
|
+
err "Stemcell not available offline: #{stemcell.file_name}" if offline?
|
85
86
|
say "Downloading '#{stemcell.name_version.make_green}'"
|
86
87
|
stemcell_download(stemcell.file_name)
|
87
88
|
end
|
data/lib/bosh/workspace.rb
CHANGED
@@ -6,14 +6,15 @@ require "rugged"
|
|
6
6
|
require "hashdiff"
|
7
7
|
require "cli/core_ext"
|
8
8
|
require "cli/validation"
|
9
|
+
require "bosh/template/renderer"
|
9
10
|
|
10
|
-
require "bosh/workspace/helpers/spiff_helper"
|
11
11
|
require "bosh/workspace/helpers/project_deployment_helper"
|
12
|
-
require "bosh/workspace/helpers/git_credentials_helper"
|
13
12
|
require "bosh/workspace/helpers/release_helper"
|
14
13
|
require "bosh/workspace/helpers/stemcell_helper"
|
15
14
|
require "bosh/workspace/helpers/dns_helper"
|
15
|
+
require "bosh/workspace/helpers/git_protocol_helper"
|
16
16
|
|
17
|
+
require "bosh/workspace/merge_tool"
|
17
18
|
require "bosh/workspace/schemas/project_deployment"
|
18
19
|
require "bosh/workspace/schemas/deployment_patch"
|
19
20
|
require "bosh/workspace/schemas/releases"
|
@@ -28,5 +29,5 @@ require "bosh/workspace/project_deployment"
|
|
28
29
|
require "bosh/workspace/stub_file"
|
29
30
|
require "bosh/workspace/deployment_patch"
|
30
31
|
require "bosh/workspace/credentials"
|
31
|
-
require "bosh/workspace/
|
32
|
+
require "bosh/workspace/git_credentials_provider"
|
32
33
|
require "bosh/workspace/version"
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module Bosh::Workspace
|
2
2
|
class Credentials
|
3
3
|
include Bosh::Cli::Validation
|
4
|
+
include GitProtocolHelper
|
5
|
+
attr_reader :raw_credentials
|
4
6
|
|
5
7
|
def initialize(file)
|
6
8
|
@raw_credentials = YAML.load_file file
|
7
9
|
end
|
8
10
|
|
9
11
|
def perform_validation(options = {})
|
10
|
-
Schemas::Credentials.new.validate
|
12
|
+
Schemas::Credentials.new.validate raw_credentials
|
11
13
|
rescue Membrane::SchemaValidationError => e
|
12
14
|
errors << e.message
|
13
15
|
end
|
@@ -16,15 +18,20 @@ module Bosh::Workspace
|
|
16
18
|
credentials[url]
|
17
19
|
end
|
18
20
|
|
21
|
+
def url_protocols
|
22
|
+
Hash[raw_credentials.map { |c| [c['url'], git_protocol_from_url(c['url'])] }]
|
23
|
+
end
|
24
|
+
|
19
25
|
private
|
20
26
|
|
21
27
|
def credentials
|
22
28
|
@credentials ||= begin
|
23
|
-
Hash[
|
24
|
-
[c.delete('url'),
|
25
|
-
c.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }]
|
26
|
-
end]
|
29
|
+
Hash[raw_credentials.map { |c| [c.delete('url'), symbolize_keys(c)] }]
|
27
30
|
end
|
28
31
|
end
|
32
|
+
|
33
|
+
def symbolize_keys(hash)
|
34
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
35
|
+
end
|
29
36
|
end
|
30
37
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Bosh::Workspace
|
2
|
+
class GitCredentialsProvider
|
3
|
+
attr_reader :credentials_file
|
4
|
+
|
5
|
+
def initialize(credentials_file)
|
6
|
+
@credentials_file = credentials_file
|
7
|
+
end
|
8
|
+
|
9
|
+
def callback
|
10
|
+
proc do |url, user, allowed_types|
|
11
|
+
require_credentials_file_for!(url)
|
12
|
+
validate_credentials!
|
13
|
+
validate_url_protocol_support!
|
14
|
+
credentials_for(url, user, allowed_types)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def credentials
|
21
|
+
@credentials ||= Credentials.new(@credentials_file)
|
22
|
+
end
|
23
|
+
|
24
|
+
def require_credentials_file_for!(url)
|
25
|
+
return if File.exist? @credentials_file
|
26
|
+
say("Authentication is required for: #{url}".make_red)
|
27
|
+
err("Credentials file does not exist: #{@credentials_file}".make_red)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_credentials!
|
31
|
+
return if credentials.valid?
|
32
|
+
say("Validation errors:".make_red)
|
33
|
+
credentials.errors.each { |error| say("- #{error}") }
|
34
|
+
err("'#{credentials_file}' is not valid".make_red)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_url_protocol_support!
|
38
|
+
credentials.url_protocols.each do |url, protocol|
|
39
|
+
next if Rugged.features.include? protocol
|
40
|
+
say("Please reinstall Rugged gem with #{protocol} support: http://git.io/veiyJ")
|
41
|
+
err("Rugged requires #{protocol} support for: #{url}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def credentials_for(url, user, allowed_types)
|
46
|
+
if creds = credentials.find_by_url(url)
|
47
|
+
load_git_credentials(creds, user, allowed_types)
|
48
|
+
else
|
49
|
+
say("Credential look up failed in: #{credentials_file}")
|
50
|
+
err("No credentials found for: #{url}".make_red)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_git_credentials(credentials, user, allowed_types)
|
55
|
+
case allowed_types
|
56
|
+
when %i(ssh_key)
|
57
|
+
key_file = temp_key_file(credentials[:private_key])
|
58
|
+
Rugged::Credentials::SshKey.new username: user, privatekey: key_file
|
59
|
+
when %i(plain_text)
|
60
|
+
Rugged::Credentials::UserPassword.new(credentials)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def temp_key_file(key)
|
65
|
+
file = Tempfile.new('sshkey')
|
66
|
+
file.write key
|
67
|
+
file.close
|
68
|
+
File.chmod(0600, file.path)
|
69
|
+
file.path
|
70
|
+
end
|
71
|
+
|
72
|
+
def say(*args)
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
def err(*args)
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
module Bosh::Workspace
|
2
|
-
|
3
|
-
def
|
4
|
-
|
5
|
-
end
|
6
|
-
|
7
|
-
def protocol()
|
8
|
-
case @url
|
2
|
+
module GitProtocolHelper
|
3
|
+
def git_protocol_from_url(url)
|
4
|
+
case url
|
9
5
|
when /^git:/
|
10
6
|
return :git
|
11
7
|
when /^https:/
|
@@ -15,7 +11,7 @@ module Bosh::Workspace
|
|
15
11
|
when /(@.+:|^ssh:)/
|
16
12
|
return :ssh
|
17
13
|
else
|
18
|
-
|
14
|
+
return nil
|
19
15
|
end
|
20
16
|
end
|
21
17
|
end
|
@@ -15,7 +15,7 @@ module Bosh::Workspace
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def project_deployment_file?(deployment)
|
18
|
-
|
18
|
+
ProjectDeployment.new(deployment).manifest.has_key?("templates")
|
19
19
|
end
|
20
20
|
|
21
21
|
def require_project_deployment
|
@@ -46,7 +46,7 @@ module Bosh::Workspace
|
|
46
46
|
|
47
47
|
say("Generating deployment manifest")
|
48
48
|
ManifestBuilder.build(project_deployment, work_dir)
|
49
|
-
|
49
|
+
|
50
50
|
if domain_name = project_deployment.domain_name
|
51
51
|
say("Transforming to dynamic networking (dns)")
|
52
52
|
DnsHelper.transform(project_deployment.merged_file, domain_name)
|
@@ -57,6 +57,14 @@ module Bosh::Workspace
|
|
57
57
|
use_targeted_director_uuid if director_uuid_current?
|
58
58
|
end
|
59
59
|
|
60
|
+
def offline!
|
61
|
+
@offline = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def offline?
|
65
|
+
@offline
|
66
|
+
end
|
67
|
+
|
60
68
|
private
|
61
69
|
|
62
70
|
def use_targeted_director_uuid
|
@@ -7,10 +7,13 @@ module Bosh::Workspace
|
|
7
7
|
remote_release && remote_release["versions"].include?(version.to_s)
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def release_upload_from_url(release_url)
|
11
|
+
upload_release_cmd.upload(release_url)
|
12
|
+
end
|
13
|
+
|
14
|
+
def release_upload(manifest_file, release_dir)
|
15
|
+
release_tarball = create_release(manifest_file, release_dir)
|
16
|
+
upload_release_cmd.upload(release_tarball)
|
14
17
|
end
|
15
18
|
|
16
19
|
def releases_dir
|
@@ -20,15 +23,38 @@ module Bosh::Workspace
|
|
20
23
|
end
|
21
24
|
|
22
25
|
def project_deployment_releases
|
23
|
-
|
24
|
-
|
26
|
+
project_deployment.releases.map do |r|
|
27
|
+
Release.new(r, releases_dir, credentials_callback, offline: offline?)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
28
31
|
private
|
29
32
|
|
30
|
-
def
|
33
|
+
def create_release(release_manifest, release_dir)
|
34
|
+
release_tarball = release_manifest.sub('yml', 'tgz')
|
35
|
+
return release_tarball if File.exist?(release_tarball)
|
36
|
+
err "Final release tarball missing: #{release_tarball}" if offline?
|
37
|
+
create_release_cmd(release_dir).create(release_manifest)
|
38
|
+
release_tarball
|
39
|
+
end
|
40
|
+
|
41
|
+
def credentials_callback
|
42
|
+
@callback ||= GitCredentialsProvider.new(credentials_file).callback
|
43
|
+
end
|
44
|
+
|
45
|
+
def credentials_file
|
46
|
+
File.join(work_dir, '.credentials.yml')
|
47
|
+
end
|
48
|
+
|
49
|
+
def upload_release_cmd
|
31
50
|
Bosh::Cli::Command::Release::UploadRelease.new
|
32
51
|
end
|
52
|
+
|
53
|
+
def create_release_cmd(release_dir)
|
54
|
+
Bosh::Cli::Command::Release::CreateRelease.new.tap do |r|
|
55
|
+
r.add_option(:with_tarball, true)
|
56
|
+
r.add_option(:dir, release_dir)
|
57
|
+
end
|
58
|
+
end
|
33
59
|
end
|
34
60
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Bosh::Workspace
|
2
2
|
class ManifestBuilder
|
3
|
-
|
3
|
+
attr_reader :merge_tool
|
4
4
|
|
5
5
|
def self.build(project_deployment, work_dir)
|
6
6
|
manifest_builder = ManifestBuilder.new(project_deployment, work_dir)
|
@@ -9,22 +9,20 @@ module Bosh::Workspace
|
|
9
9
|
|
10
10
|
def initialize(project_deployment, work_dir)
|
11
11
|
@project_deployment = project_deployment
|
12
|
+
@merge_tool = @project_deployment.merge_tool
|
12
13
|
@work_dir = work_dir
|
13
14
|
end
|
14
15
|
|
15
16
|
def merge_templates
|
16
|
-
|
17
|
+
@merge_tool.merge(template_paths, @project_deployment.merged_file)
|
17
18
|
end
|
18
19
|
|
19
20
|
private
|
20
21
|
|
21
|
-
def spiff_template_paths
|
22
|
-
spiff_templates = template_paths
|
23
|
-
spiff_templates << stub_file_path
|
24
|
-
end
|
25
|
-
|
26
22
|
def template_paths
|
27
|
-
@project_deployment.templates.map
|
23
|
+
@template_paths ||= @project_deployment.templates.map do |t|
|
24
|
+
template_path(t)
|
25
|
+
end.push(stub_file_path)
|
28
26
|
end
|
29
27
|
|
30
28
|
def stub_file_path
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Bosh::Workspace
|
2
|
+
class MergeTool
|
3
|
+
include Bosh::Exec
|
4
|
+
|
5
|
+
attr_accessor :name, :version
|
6
|
+
|
7
|
+
def initialize(merge_tool = nil)
|
8
|
+
@name, @version = case merge_tool
|
9
|
+
when Hash
|
10
|
+
[merge_tool['name'], merge_tool['version']]
|
11
|
+
when String
|
12
|
+
[merge_tool, 'current']
|
13
|
+
else
|
14
|
+
['spiff', 'current']
|
15
|
+
end
|
16
|
+
|
17
|
+
unless available_tool_names.include?(@name)
|
18
|
+
say("#{@name} is not supported, please specify spiff or spruce instead.".make_red)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def available_tool_names
|
23
|
+
%w(spiff spruce)
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(templates, target_file)
|
27
|
+
check_tool_version if version != 'current'
|
28
|
+
run_merge_tool(:merge, templates) do |output|
|
29
|
+
File.open(target_file, 'w') { |file| file.write(output) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def run_merge_tool(verb, params = [])
|
36
|
+
params.map!(&:shellescape)
|
37
|
+
cmd = [name, verb.to_s] + params + ['2>&1']
|
38
|
+
sh(cmd.join(" "), :yield => :on_false) do |result|
|
39
|
+
command_not_found if result.not_found?
|
40
|
+
command_failed(result.command, result.output) if result.failed?
|
41
|
+
yield result.output
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_tool_version
|
46
|
+
run_merge_tool('-v') do |output|
|
47
|
+
actual_version = output.match(/(\d+\.\d+\.\d+)/).to_a.first
|
48
|
+
if actual_version.nil? || actual_version != version
|
49
|
+
warning "Deployment requires #{name} to have version #{version}. " +
|
50
|
+
"Your actual #{name} version is #{actual_version}."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def command_not_found(command)
|
56
|
+
say("Can't find #{name} in $PATH".make_red)
|
57
|
+
say("Go to #{installation_instructions_url} for installation instructions")
|
58
|
+
err("Please make sure #{name} is installed")
|
59
|
+
end
|
60
|
+
|
61
|
+
def installation_instructions_url
|
62
|
+
case name
|
63
|
+
when 'spiff' then 'spiff.cfapps.io'
|
64
|
+
when 'spruce' then 'https://github.com/geofffranks/spruce#installation'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def command_failed(command, output)
|
69
|
+
say("Command failed: '#{command}'")
|
70
|
+
err(output)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|