Skip to content

Commit 0dbeb56

Browse files
committed
Merge branch 'release/0.5.0' into stable
2 parents 990656c + f7b4d6b commit 0dbeb56

20 files changed

+2364
-83
lines changed

README.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ add additional support.
1515
```elixir
1616
def deps do
1717
[{:distillery, "~> 1.3",
18-
{:bootleg, "~> 0.4"}]
18+
{:bootleg, "~> 0.5"}]
1919
end
2020
```
2121

@@ -136,7 +136,7 @@ by Bootleg:
136136
* `workspace` - remote path specifying where to perform a build or push a deploy (default `.`)
137137
* `user` - ssh username (default to local user)
138138
* `password` - ssh password
139-
* `identity` - file path of an SSH private key identify file
139+
* `identity` - unencrypted private key file path (passphrases are not supported at this time)
140140
* `port` - ssh port (default `22`)
141141

142142
#### Examples
@@ -180,7 +180,7 @@ Bootleg extensions may impose restrictions on certain roles, such as restricting
180180
### Roles provided by Bootleg
181181

182182
* `build` - Takes only one host. If a list is given, only the first hosts is
183-
used and a warning may result. If this role isn't set the release packaging will be done locally.
183+
used and a warning may result.
184184
* `app` - Takes a list of hosts, or a string with one host.
185185

186186
## Building and deploying a release
@@ -382,9 +382,15 @@ remote :app do
382382
end
383383

384384
# filtering - only runs on app hosts with an option of primary set to true
385-
remote :app, primary: true do
385+
remote :app, filter: [primary: true] do
386386
"mix ecto.migrate"
387387
end
388+
389+
# change working directory - creates a file `/tmp/foo`, regardless of the role
390+
# workspace configuration
391+
remote :app, cd: "/tmp" do
392+
"touch ./foo"
393+
end
388394
```
389395

390396
## Phoenix Support
@@ -399,7 +405,7 @@ for building phoenix releases.
399405
# mix.exs
400406
def deps do
401407
[{:distillery, "~> 1.3"},
402-
{:bootleg, "~> 0.4"},
408+
{:bootleg, "~> 0.5"},
403409
{:bootleg_phoenix, "~> 0.1"}]
404410
end
405411
```

lib/bootleg/config.ex

+41-13
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ defmodule Bootleg.Config do
122122
line = caller.line()
123123
quote do
124124
hook_number = Bootleg.Config.Agent.increment(:next_hook_number)
125-
module_name = String.to_atom("Elixir.Bootleg.Tasks.DynamicCallbacks." <>
125+
module_name = String.to_atom("Elixir.Bootleg.DynamicCallbacks." <>
126126
String.capitalize("#{unquote(position)}") <> String.capitalize("#{unquote(task)}") <>
127127
"#{hook_number}")
128128
defmodule module_name do
@@ -338,18 +338,18 @@ defmodule Bootleg.Config do
338338
@doc """
339339
Executes commands on all remote hosts within a role.
340340
341-
This is equivalent to calling `remote/3` with a `filter` of `[]`.
341+
This is equivalent to calling `remote/3` with an `options` of `[]`.
342342
"""
343343
defmacro remote(role, lines) do
344344
quote do: remote(unquote(role), [], unquote(lines))
345345
end
346346

347-
defmacro remote(role, filter, do: {:__block__, _, lines}) do
348-
quote do: remote(unquote(role), unquote(filter), unquote(lines))
347+
defmacro remote(role, options, do: {:__block__, _, lines}) do
348+
quote do: remote(unquote(role), unquote(options), unquote(lines))
349349
end
350350

351-
defmacro remote(role, filter, do: lines) do
352-
quote do: remote(unquote(role), unquote(filter), unquote(lines))
351+
defmacro remote(role, options, do: lines) do
352+
quote do: remote(unquote(role), unquote(options), unquote(lines))
353353
end
354354

355355
@doc """
@@ -363,9 +363,16 @@ defmodule Bootleg.Config do
363363
used as a command. Each command will be simulataneously executed on all hosts in the role. Once
364364
all hosts have finished executing the command, the next command in the list will be sent.
365365
366-
`filter` is an optional `Keyword` list of host options to filter with. Any host whose options match
366+
`options` is an optional `Keyword` list of options to customize the remote invocation. Currently two
367+
keys are supported:
368+
369+
* `filter` takes a `Keyword` list of host options to filter with. Any host whose options match
367370
the filter will be included in the remote execution. A host matches if it has all of the filtering
368371
options defined and the values match (via `==/2`) the filter.
372+
* `cd` changes the working directory of the remote shell prior to executing the remote
373+
commands. The options takes either an absolute or relative path, with relative paths being
374+
defined relative to the workspace configured for the role, or the default working directory
375+
of the shell if no workspace is defined.
369376
370377
`role` can be a single role, a list of roles, or the special role `:all` (all roles). If the same host
371378
exists in multiple roles, the commands will be run once for each role where the host shows up. In the
@@ -394,19 +401,24 @@ defmodule Bootleg.Config do
394401
395402
# only runs on `host1.example.com`
396403
role :build, "host2.example.com"
397-
role :build, "host1.example.com", primary: true, another_attr: :cat
404+
role :build, "host1.example.com", filter: [primary: true, another_attr: :cat]
398405
399-
remote :build, primary: true do
406+
remote :build, filter: [primary: true] do
407+
"hostname"
408+
end
409+
410+
# runs on `host1.example.com` inside the `tmp` directory found in the workspace
411+
remote :build, filter: [primary: true], cd: "tmp/" do
400412
"hostname"
401413
end
402414
```
403415
"""
404-
defmacro remote(role, filter, lines) do
416+
defmacro remote(role, options, lines) do
405417
roles = unpack_role(role)
406418
quote bind_quoted: binding() do
407419
Enum.reduce(roles, [], fn role, outputs ->
408420
role
409-
|> SSH.init([], filter)
421+
|> SSH.init([cd: options[:cd]], Keyword.get(options, :filter, []))
410422
|> SSH.run!(lines)
411423
|> SSH.merge_run_results(outputs)
412424
end)
@@ -488,13 +500,29 @@ defmodule Bootleg.Config do
488500
@doc false
489501
@spec app() :: any
490502
def app do
491-
get_config(:app, Project.config[:app])
503+
:config
504+
|> Bootleg.Config.Agent.get()
505+
|> Keyword.get_lazy(:app, fn -> cache_project_config(:app) end)
492506
end
493507

494508
@doc false
495509
@spec version() :: any
496510
def version do
497-
get_config(:version, Project.config[:version])
511+
:config
512+
|> Bootleg.Config.Agent.get()
513+
|> Keyword.get_lazy(:version, fn -> cache_project_config(:version) end)
514+
end
515+
516+
@doc false
517+
@spec cache_project_config(atom) :: any
518+
def cache_project_config(prop) do
519+
unless Project.umbrella? do
520+
val = Project.config[prop]
521+
Bootleg.Config.Agent.merge(:config, prop, val)
522+
val
523+
else
524+
nil
525+
end
498526
end
499527

500528
@doc false

lib/bootleg/config/agent.ex

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ defmodule Bootleg.Config.Agent do
6060
receive do
6161
{:DOWN, ^ref, :process, _pid, _reason} ->
6262
Enum.each(:code.all_loaded(), fn {module, _file} ->
63-
if String.starts_with?(Atom.to_string(module), "Elixir.Bootleg.DynamicTasks.") do
63+
if String.starts_with?(Atom.to_string(module), "Elixir.Bootleg.DynamicTasks.") ||
64+
String.starts_with?(Atom.to_string(module), "Elixir.Bootleg.DynamicCallbacks.")do
6465
unload_code(module)
6566
end
6667
end)

lib/bootleg/ssh.ex

+15-3
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ defmodule Bootleg.SSH do
3737
def init(hosts, options) do
3838
workspace = Keyword.get(options, :workspace, ".")
3939
create_workspace = Keyword.get(options, :create_workspace, true)
40+
working_directory = Keyword.get(options, :cd)
4041
UI.puts "Creating remote context at '#{workspace}'"
4142

4243
:ssh.start()
4344

4445
hosts
45-
|> List.wrap
46+
|> List.wrap()
4647
|> Enum.map(&ssh_host_options/1)
47-
|> SSHKit.context
48+
|> SSHKit.context()
4849
|> validate_workspace(workspace, create_workspace)
50+
|> working_directory(working_directory)
4951
end
5052

5153
def ssh_host_options(%Host{} = host) do
@@ -84,6 +86,16 @@ defmodule Bootleg.SSH do
8486
SSHKit.path context, workspace
8587
end
8688

89+
defp working_directory(context, path) when path == "." or path == false or is_nil(path) do
90+
context
91+
end
92+
defp working_directory(context, path) do
93+
case Path.type(path) do
94+
:absolute -> %Context{context | path: path}
95+
_ -> %Context{context | path: Path.join(context.path, path)}
96+
end
97+
end
98+
8799
defp capture(message, {buffer, status} = state, host) do
88100
next = case message do
89101
{:data, _, 0, data} ->
@@ -147,7 +159,7 @@ defmodule Bootleg.SSH do
147159
def ssh_opt(option), do: option
148160

149161
@ssh_options ~w(user password port key_cb auth_methods connection_timeout id_string
150-
idle_time silently_accept_hosts user_dir timeout connection_timeout identity)a
162+
idle_time silently_accept_hosts user_dir timeout connection_timeout identity quiet_mode)a
151163
def supported_options do
152164
@ssh_options
153165
end

lib/bootleg/tasks/build.exs

+12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
alias Bootleg.{UI, Config}
22
use Bootleg.Config
33

4+
task :verify_config do
5+
if Config.app() == nil || Config.version() == nil do
6+
raise "Error: app or version to deploy is not set.\n"
7+
<> "Usually these are automatically picked up from Mix.Project.\n"
8+
<> "If this is an umbrella app, you must set these in your deploy.exs, e.g.:\n"
9+
<> "# config(:app, :myapp)\n"
10+
<> "# config(:version, \"0.0.1\")"
11+
end
12+
end
13+
414
task :build do
515
Bootleg.Strategies.Build.Distillery.build()
616
end
717

18+
before_task :build, :verify_config
19+
820
task :generate_release do
921
UI.info "Generating release"
1022
mix_env = Keyword.get(Config.config(), :mix_env, "prod")

lib/mix/tasks/init.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ defmodule Mix.Tasks.Bootleg.Init do
4242
# # mix.exs
4343
# def deps do
4444
# [{:distillery, "~> 1.3"},
45-
# {:bootleg, "~> 0.4"},
45+
# {:bootleg, "~> 0.5"},
4646
# {:bootleg_phoenix, "~> 0.1"}]
4747
# end
4848
# ```

lib/ui.ex

+24-15
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,15 @@ defmodule Bootleg.UI do
7575
"""
7676
def puts_upload(%SSHKit.Context{} = context, local_path, remote_path) do
7777
Enum.each(context.hosts, fn(host) ->
78-
[:bright, :green]
78+
[:reset, :bright, :green]
7979
++ ["[" <> String.pad_trailing(host.name, 10) <> "] "]
8080
++ [:reset, :yellow, "UPLOAD", " "]
8181
++ [:reset, Path.relative_to_cwd(local_path)]
8282
++ [:reset, :yellow, " -> "]
8383
++ [:reset, Path.join(context.path, remote_path)]
84-
|> Bunt.puts()
84+
++ ["\n"]
85+
|> IO.ANSI.format(output_coloring())
86+
|> IO.binwrite()
8587
end)
8688
end
8789

@@ -90,13 +92,15 @@ defmodule Bootleg.UI do
9092
"""
9193
def puts_download(%SSHKit.Context{} = context, remote_path, local_path) do
9294
Enum.each(context.hosts, fn(host) ->
93-
[:bright, :green]
95+
[:reset, :bright, :green]
9496
++ ["[" <> String.pad_trailing(host.name, 10) <> "] "]
9597
++ [:reset, :yellow, "DOWNLOAD", " "]
9698
++ [:reset, Path.join(context.path, remote_path)]
9799
++ [:reset, :yellow, " -> "]
98100
++ [:reset, Path.relative_to_cwd(local_path)]
99-
|> Bunt.puts()
101+
++ ["\n"]
102+
|> IO.ANSI.format(output_coloring())
103+
|> IO.binwrite()
100104
end)
101105
end
102106

@@ -114,7 +118,9 @@ defmodule Bootleg.UI do
114118
"""
115119
def puts_send(%SSHKit.Host{} = host, command) do
116120
prefix = "[" <> String.pad_trailing(host.name, 10) <> "] "
117-
Bunt.puts [:bright, :green, prefix, :reset, command]
121+
[:reset, :bright, :green, prefix, :reset, command, "\n"]
122+
|> IO.ANSI.format(output_coloring())
123+
|> IO.binwrite()
118124
end
119125

120126
@doc """
@@ -154,17 +160,20 @@ defmodule Bootleg.UI do
154160
prefix = "[" <> String.pad_trailing(host.name, 10) <> "] "
155161
text
156162
|> String.split(["\r\n", "\n"])
157-
|> Enum.map(&String.trim_trailing/1)
158-
|> Enum.map(&([:reset, :bright, :blue, prefix, :reset, &1]))
159-
|> drop_last_line()
160-
|> Enum.intersperse("\n")
161-
|> Bunt.puts
163+
|> Enum.map(&format_line(&1, prefix))
164+
|> IO.ANSI.format(output_coloring())
165+
|> IO.binwrite()
162166
end
163167

164-
defp drop_last_line(lines) do
165-
case length(lines) < 2 do
166-
true -> lines
167-
false -> Enum.drop(lines, -1)
168-
end
168+
defp format_line(line, prefix) do
169+
[:reset, :bright, :blue, prefix, :reset, String.trim_trailing(line), "\n"]
170+
end
171+
172+
@doc """
173+
Get configured output coloring enabled
174+
Defaults to true
175+
"""
176+
def output_coloring do
177+
Application.get_env(:bootleg, :output_coloring, true)
169178
end
170179
end

mix.exs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Bootleg.Mixfile do
22
use Mix.Project
33

4-
@version "0.4.0"
4+
@version "0.5.0"
55
@source "https://github.com/labzero/bootleg"
66

77
def project do
@@ -45,7 +45,6 @@ defmodule Bootleg.Mixfile do
4545
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
4646
{:ex_doc, "~> 0.16", only: :dev, runtime: false},
4747
{:excoveralls, "~> 0.6", only: :test},
48-
{:bunt, "~> 0.2.0"},
4948
{:mock, "~> 0.2.0", only: :test},
5049
{:junit_formatter, "~> 1.3", only: :test},
5150
{:temp, "~> 0.4.3", only: :test}

test/bootleg/config_functional_test.exs

+9-6
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,22 @@ defmodule Bootleg.ConfigFunctionalTest do
136136
end
137137

138138
@tag boot: 3
139-
test "remote/3 filtering" do
139+
test "remote/3 options" do
140140
capture_io(fn ->
141141
use Bootleg.Config
142142

143-
assert [{:ok, out_0, 0, _}] = remote :build, [foo: 0], "hostname"
144-
assert [{:ok, out_1, 0, _}] = remote :build, [foo: 1], do: "hostname"
143+
assert [{:ok, out_0, 0, _}] = remote :build, [filter: [foo: 0]], "hostname"
144+
assert [{:ok, out_1, 0, _}] = remote :build, [filter: [foo: 1]], do: "hostname"
145145
assert out_1 != out_0
146146

147-
assert [] = remote :build, [foo: :bar], "hostname"
148-
assert [{:ok, out_all, 0, _}] = remote :all, [foo: :bar], "hostname"
147+
assert [] = remote :build, [filter: [foo: :bar]], "hostname"
148+
assert [{:ok, out_all, 0, _}] = remote :all, [filter: [foo: :bar]], "hostname"
149149
assert out_1 != out_0 != out_all
150150

151-
remote :all, [foo: :bar] do "hostname" end
151+
remote :all, filter: [foo: :bar] do "hostname" end
152+
153+
[{:ok, [stdout: "/tmp\n"], 0, _}] = remote :app, cd: "/tmp" do "pwd" end
154+
[{:ok, [stdout: "/home\n"], 0, _}] = remote :app, cd: "../.." do "pwd" end
152155
end)
153156
end
154157

0 commit comments

Comments
 (0)