recap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +5 -0
- data/Rakefile +12 -0
- data/bin/tomafro-deploy +3 -0
- data/doc/index.html +216 -0
- data/doc/lib/recap/bundler.html +151 -0
- data/doc/lib/recap/capistrano_extensions.html +203 -0
- data/doc/lib/recap/cli.html +39 -0
- data/doc/lib/recap/compatibility.html +70 -0
- data/doc/lib/recap/deploy.html +373 -0
- data/doc/lib/recap/env.html +103 -0
- data/doc/lib/recap/foreman.html +39 -0
- data/doc/lib/recap/preflight.html +158 -0
- data/doc/lib/recap/rails.html +39 -0
- data/doc/lib/recap/version.html +39 -0
- data/index.rb +53 -0
- data/lib/recap/bundler.rb +45 -0
- data/lib/recap/capistrano_extensions.rb +72 -0
- data/lib/recap/cli.rb +32 -0
- data/lib/recap/compatibility.rb +14 -0
- data/lib/recap/deploy/templates/Capfile.erb +6 -0
- data/lib/recap/deploy.rb +133 -0
- data/lib/recap/env.rb +54 -0
- data/lib/recap/foreman.rb +45 -0
- data/lib/recap/preflight.rb +68 -0
- data/lib/recap/rails.rb +22 -0
- data/lib/recap/version.rb +3 -0
- data/recap.gemspec +25 -0
- metadata +126 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
5
|
+
<title>preflight.rb</title>
|
6
|
+
<link rel="stylesheet" href="http://jashkenas.github.com/docco/resources/docco.css">
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div id='container'>
|
10
|
+
<div id="background"></div>
|
11
|
+
<div id="jump_to">
|
12
|
+
Jump To …
|
13
|
+
<div id="jump_wrapper">
|
14
|
+
<div id="jump_page">
|
15
|
+
<a class="source" href="../../index.html">index.rb</a>
|
16
|
+
<a class="source" href="bundler.html">bundler.rb</a>
|
17
|
+
<a class="source" href="capistrano_extensions.html">capistrano_extensions.rb</a>
|
18
|
+
<a class="source" href="cli.html">cli.rb</a>
|
19
|
+
<a class="source" href="compatibility.html">compatibility.rb</a>
|
20
|
+
<a class="source" href="deploy.html">deploy.rb</a>
|
21
|
+
<a class="source" href="env.html">env.rb</a>
|
22
|
+
<a class="source" href="foreman.html">foreman.rb</a>
|
23
|
+
<a class="source" href="preflight.html">preflight.rb</a>
|
24
|
+
<a class="source" href="rails.html">rails.rb</a>
|
25
|
+
<a class="source" href="version.html">version.rb</a>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
<table cellspacing=0 cellpadding=0>
|
30
|
+
<thead>
|
31
|
+
<tr>
|
32
|
+
<th class=docs><h1>preflight.rb</h1></th>
|
33
|
+
<th class=code></th>
|
34
|
+
</tr>
|
35
|
+
</thead>
|
36
|
+
<tbody>
|
37
|
+
<tr id='section-1'>
|
38
|
+
<td class=docs>
|
39
|
+
<div class="pilwrap">
|
40
|
+
<a class="pilcrow" href="#section-1">¶</a>
|
41
|
+
</div>
|
42
|
+
<p>Before <code>recap</code> will work correctly, a small amount of setup work needs to be performed on
|
43
|
+
all target servers.</p>
|
44
|
+
|
45
|
+
<p>First, each user who can deploy the app needs to have an account on each server, and must be able
|
46
|
+
to ssh into the box. They’ll also each need to be sudoers.</p>
|
47
|
+
|
48
|
+
<p>Secondly, each deploying user should set their git <code>user.name</code> and <code>user.email</code>. This can easily
|
49
|
+
be done by running:</p>
|
50
|
+
|
51
|
+
<p><code>git config —global user.email “you@example.com”</code>
|
52
|
+
<code>git config —global user.name “Your Name”</code></p>
|
53
|
+
|
54
|
+
<p>Finally, a user and group representing the application (and usually with the same name) should be
|
55
|
+
created. Where possible, the application user will run application code, while the group will own
|
56
|
+
application specific files. Each deploying user should be added to the application group.</p>
|
57
|
+
|
58
|
+
<p>This preflight recipe checks each of these things in turn, and attempts to give helpful advice
|
59
|
+
should a check fail.</p>
|
60
|
+
</td>
|
61
|
+
<td class=code>
|
62
|
+
<div class='highlight'><pre><span class="no">Capistrano</span><span class="o">::</span><span class="no">Configuration</span><span class="o">.</span><span class="n">instance</span><span class="p">(</span><span class="ss">:must_exist</span><span class="p">)</span><span class="o">.</span><span class="n">load</span> <span class="k">do</span></pre></div>
|
63
|
+
</td>
|
64
|
+
</tr>
|
65
|
+
<tr id='section-2'>
|
66
|
+
<td class=docs>
|
67
|
+
<div class="pilwrap">
|
68
|
+
<a class="pilcrow" href="#section-2">¶</a>
|
69
|
+
</div>
|
70
|
+
<p>The preflight check is pretty quick, so run it before every <code>deploy:setup</code> and <code>deploy</code></p>
|
71
|
+
</td>
|
72
|
+
<td class=code>
|
73
|
+
<div class='highlight'><pre> <span class="n">before</span> <span class="s1">'deploy:setup'</span><span class="p">,</span> <span class="s1">'preflight:check'</span>
|
74
|
+
<span class="n">before</span> <span class="s1">'deploy'</span><span class="p">,</span> <span class="s1">'preflight:check'</span>
|
75
|
+
|
76
|
+
<span class="n">set</span><span class="p">(</span><span class="ss">:remote_username</span><span class="p">)</span> <span class="p">{</span> <span class="n">capture</span><span class="p">(</span><span class="s1">'whoami'</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span> <span class="p">}</span>
|
77
|
+
|
78
|
+
<span class="n">namespace</span> <span class="ss">:preflight</span> <span class="k">do</span>
|
79
|
+
<span class="n">task</span> <span class="ss">:check</span> <span class="k">do</span></pre></div>
|
80
|
+
</td>
|
81
|
+
</tr>
|
82
|
+
<tr id='section-3'>
|
83
|
+
<td class=docs>
|
84
|
+
<div class="pilwrap">
|
85
|
+
<a class="pilcrow" href="#section-3">¶</a>
|
86
|
+
</div>
|
87
|
+
<p>First check the <code>application_user</code> exists</p>
|
88
|
+
</td>
|
89
|
+
<td class=code>
|
90
|
+
<div class='highlight'><pre> <span class="k">if</span> <span class="n">capture</span><span class="p">(</span><span class="s2">"id </span><span class="si">#{</span><span class="n">application_user</span><span class="si">}</span><span class="s2"> > /dev/null 2>&1; echo $?"</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span> <span class="o">!=</span> <span class="s2">"0"</span>
|
91
|
+
<span class="nb">abort</span> <span class="sx">%{</span>
|
92
|
+
<span class="sx">The application user '</span><span class="si">#{</span><span class="n">application_user</span><span class="si">}</span><span class="sx">' doesn't exist. You can create this user by logging into the server and running:</span>
|
93
|
+
|
94
|
+
<span class="sx"> sudo useradd </span><span class="si">#{</span><span class="n">application_user</span><span class="si">}</span><span class="sx"></span>
|
95
|
+
<span class="se">\n</span><span class="sx">}</span>
|
96
|
+
<span class="k">end</span></pre></div>
|
97
|
+
</td>
|
98
|
+
</tr>
|
99
|
+
<tr id='section-4'>
|
100
|
+
<td class=docs>
|
101
|
+
<div class="pilwrap">
|
102
|
+
<a class="pilcrow" href="#section-4">¶</a>
|
103
|
+
</div>
|
104
|
+
<p>Then the <code>application_group</code></p>
|
105
|
+
</td>
|
106
|
+
<td class=code>
|
107
|
+
<div class='highlight'><pre> <span class="k">if</span> <span class="n">capture</span><span class="p">(</span><span class="s2">"id -g </span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="s2"> > /dev/null 2>&1; echo $?"</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span> <span class="o">!=</span> <span class="s2">"0"</span>
|
108
|
+
<span class="nb">abort</span> <span class="sx">%{</span>
|
109
|
+
<span class="sx">The application group '</span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx">' doesn't exist. You can create this group by logging into the server and running:</span>
|
110
|
+
|
111
|
+
<span class="sx"> sudo groupadd </span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx"></span>
|
112
|
+
<span class="sx"> sudo usermod --append -G </span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx"> </span><span class="si">#{</span><span class="n">application_user</span><span class="si">}</span><span class="sx"></span>
|
113
|
+
<span class="se">\n</span><span class="sx">}</span>
|
114
|
+
<span class="k">end</span></pre></div>
|
115
|
+
</td>
|
116
|
+
</tr>
|
117
|
+
<tr id='section-5'>
|
118
|
+
<td class=docs>
|
119
|
+
<div class="pilwrap">
|
120
|
+
<a class="pilcrow" href="#section-5">¶</a>
|
121
|
+
</div>
|
122
|
+
<p>Check the git configuration exists</p>
|
123
|
+
</td>
|
124
|
+
<td class=code>
|
125
|
+
<div class='highlight'><pre> <span class="k">if</span> <span class="n">capture</span><span class="p">(</span><span class="s1">'git config user.name || true'</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="o">.</span><span class="n">empty?</span> <span class="o">||</span> <span class="n">capture</span><span class="p">(</span><span class="s1">'git config user.email || true'</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="o">.</span><span class="n">empty?</span>
|
126
|
+
<span class="nb">abort</span> <span class="sx">%{</span>
|
127
|
+
<span class="sx">Your remote user must have a git user.name and user.email set. You can set these by logging into the server as </span><span class="si">#{</span><span class="n">remote_username</span><span class="si">}</span><span class="sx"> and running:</span>
|
128
|
+
|
129
|
+
<span class="sx"> git config --global user.email "you@example.com"</span>
|
130
|
+
<span class="sx"> git config --global user.name "Your Name"</span>
|
131
|
+
<span class="se">\n</span><span class="sx">}</span>
|
132
|
+
<span class="k">end</span></pre></div>
|
133
|
+
</td>
|
134
|
+
</tr>
|
135
|
+
<tr id='section-6'>
|
136
|
+
<td class=docs>
|
137
|
+
<div class="pilwrap">
|
138
|
+
<a class="pilcrow" href="#section-6">¶</a>
|
139
|
+
</div>
|
140
|
+
<p>And finally check the remote user is a member of the <code>application_group</code></p>
|
141
|
+
|
142
|
+
</td>
|
143
|
+
<td class=code>
|
144
|
+
<div class='highlight'><pre> <span class="k">unless</span> <span class="n">capture</span><span class="p">(</span><span class="s1">'groups'</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">)</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">application_group</span><span class="p">)</span>
|
145
|
+
<span class="nb">abort</span> <span class="sx">%{</span>
|
146
|
+
<span class="sx">Your remote user must be a member of the '</span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx">' group in order to perform deployments. You can add yourself to this group by logging into the server and running:</span>
|
147
|
+
|
148
|
+
<span class="sx"> sudo usermod --append -G </span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx"> </span><span class="si">#{</span><span class="n">remote_username</span><span class="si">}</span><span class="sx"></span>
|
149
|
+
<span class="se">\n</span><span class="sx">}</span>
|
150
|
+
<span class="k">end</span>
|
151
|
+
<span class="k">end</span>
|
152
|
+
<span class="k">end</span>
|
153
|
+
<span class="k">end</span></pre></div>
|
154
|
+
</td>
|
155
|
+
</tr>
|
156
|
+
</table>
|
157
|
+
</div>
|
158
|
+
</body>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
5
|
+
<title>rails.rb</title>
|
6
|
+
<link rel="stylesheet" href="http://jashkenas.github.com/docco/resources/docco.css">
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div id='container'>
|
10
|
+
<div id="background"></div>
|
11
|
+
<div id="jump_to">
|
12
|
+
Jump To …
|
13
|
+
<div id="jump_wrapper">
|
14
|
+
<div id="jump_page">
|
15
|
+
<a class="source" href="../../index.html">index.rb</a>
|
16
|
+
<a class="source" href="bundler.html">bundler.rb</a>
|
17
|
+
<a class="source" href="capistrano_extensions.html">capistrano_extensions.rb</a>
|
18
|
+
<a class="source" href="cli.html">cli.rb</a>
|
19
|
+
<a class="source" href="compatibility.html">compatibility.rb</a>
|
20
|
+
<a class="source" href="deploy.html">deploy.rb</a>
|
21
|
+
<a class="source" href="env.html">env.rb</a>
|
22
|
+
<a class="source" href="foreman.html">foreman.rb</a>
|
23
|
+
<a class="source" href="preflight.html">preflight.rb</a>
|
24
|
+
<a class="source" href="rails.html">rails.rb</a>
|
25
|
+
<a class="source" href="version.html">version.rb</a>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
<table cellspacing=0 cellpadding=0>
|
30
|
+
<thead>
|
31
|
+
<tr>
|
32
|
+
<th class=docs><h1>rails.rb</h1></th>
|
33
|
+
<th class=code></th>
|
34
|
+
</tr>
|
35
|
+
</thead>
|
36
|
+
<tbody>
|
37
|
+
</table>
|
38
|
+
</div>
|
39
|
+
</body>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
5
|
+
<title>version.rb</title>
|
6
|
+
<link rel="stylesheet" href="http://jashkenas.github.com/docco/resources/docco.css">
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div id='container'>
|
10
|
+
<div id="background"></div>
|
11
|
+
<div id="jump_to">
|
12
|
+
Jump To …
|
13
|
+
<div id="jump_wrapper">
|
14
|
+
<div id="jump_page">
|
15
|
+
<a class="source" href="../../index.html">index.rb</a>
|
16
|
+
<a class="source" href="bundler.html">bundler.rb</a>
|
17
|
+
<a class="source" href="capistrano_extensions.html">capistrano_extensions.rb</a>
|
18
|
+
<a class="source" href="cli.html">cli.rb</a>
|
19
|
+
<a class="source" href="compatibility.html">compatibility.rb</a>
|
20
|
+
<a class="source" href="deploy.html">deploy.rb</a>
|
21
|
+
<a class="source" href="env.html">env.rb</a>
|
22
|
+
<a class="source" href="foreman.html">foreman.rb</a>
|
23
|
+
<a class="source" href="preflight.html">preflight.rb</a>
|
24
|
+
<a class="source" href="rails.html">rails.rb</a>
|
25
|
+
<a class="source" href="version.html">version.rb</a>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
<table cellspacing=0 cellpadding=0>
|
30
|
+
<thead>
|
31
|
+
<tr>
|
32
|
+
<th class=docs><h1>version.rb</h1></th>
|
33
|
+
<th class=code></th>
|
34
|
+
</tr>
|
35
|
+
</thead>
|
36
|
+
<tbody>
|
37
|
+
</table>
|
38
|
+
</div>
|
39
|
+
</body>
|
data/index.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# This is the annotated source code and documentation for
|
2
|
+
# [recap](http://github.com/freerange/recap), a simple, opinionated set of capistrano
|
3
|
+
# deployment recipes. Inspired by
|
4
|
+
# [this blog post](https://github.com/blog/470-deployment-script-spring-cleaning), these recipes use
|
5
|
+
# git's strengths to deploy applications in a faster, simpler manner than a standard capistrano
|
6
|
+
# deployment. Using git to manage release versions means apps can be deployed to a single directory.
|
7
|
+
# There's no need for `releases`, `shared` or `current` folders, and no symlinking.
|
8
|
+
|
9
|
+
# ### Goals ###
|
10
|
+
|
11
|
+
# These deployment recipes try to do the following:
|
12
|
+
|
13
|
+
# Run all commands as the `application_user`, loading the full user environment. The only
|
14
|
+
# exceptions are `git` commands (which often rely on SSH agent forwarding for authentication), and anything
|
15
|
+
# that requires `sudo`.
|
16
|
+
#
|
17
|
+
|
18
|
+
# Use `git` to avoid unecessary work. If the `Gemfile.lock` hasn't changed, there's no need to run
|
19
|
+
# `bundle install`. Similarly if there are no new migrations, why do `rake db:migrate`. Faster deploys
|
20
|
+
# mean more frequent deploys, which in our experience leads to better applications.
|
21
|
+
#
|
22
|
+
|
23
|
+
# Avoid the use of `sudo` (other than to change to the `application_user`). As much as possible, `sudo`
|
24
|
+
# is only used to `su` to the `application_user` before running a command. To avoid typing a password
|
25
|
+
# to perform the majority of deployment tasks, this code can be added to
|
26
|
+
# `/etc/sudoers.d/application` (change `application` to the name of your app).
|
27
|
+
|
28
|
+
%application ALL=NOPASSWD: /sbin/start application*
|
29
|
+
%application ALL=NOPASSWD: /sbin/stop application*
|
30
|
+
%application ALL=NOPASSWD: /sbin/restart application*
|
31
|
+
%application ALL=NOPASSWD: /bin/su - application*
|
32
|
+
%application ALL=NOPASSWD: /bin/su application*
|
33
|
+
|
34
|
+
# ### Code layout ###
|
35
|
+
|
36
|
+
# The main deployment tasks are defined in [recap.rb](lib/recap.html). Automatic
|
37
|
+
# checks to ensure servers are correctly setup are in
|
38
|
+
# [recap/preflight.rb](lib/recap/preflight.html).
|
39
|
+
|
40
|
+
# In addition, there are extensions for [bundler](lib/recap/bundler.html) and
|
41
|
+
# [foreman](lib/recap/foreman.html).
|
42
|
+
|
43
|
+
# For (limited) compatability with other existing recipes, see
|
44
|
+
# [compatibility](lib/recap/compatibility.html)
|
45
|
+
|
46
|
+
# ### Deployment target ###
|
47
|
+
|
48
|
+
# These recipes have been run successful against Ubuntu.
|
49
|
+
|
50
|
+
# The application should be run as the application user; if using Apache and Passenger, you should set the `PassengerDefaultUser` directive to be the same as the `application_user`.
|
51
|
+
|
52
|
+
# The code is available [on github](http://github.com/freerange/recap) and released under the
|
53
|
+
# [MIT License](https://github.com/freerange/recap/blob/master/LICENSE)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# The bundler recipe ensures that the application bundle is installed whenever the code is updated.
|
2
|
+
|
3
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
4
|
+
# Each bundle is declared in a `Gemfile`, by default in the root of the application directory
|
5
|
+
set(:bundle_gemfile) { "#{deploy_to}/Gemfile" }
|
6
|
+
|
7
|
+
# As well as a `Gemfile`, application repositories should also contain a `Gemfile.lock`.
|
8
|
+
set(:bundle_gemfile_lock) { "#{bundle_gemfile}.lock" }
|
9
|
+
|
10
|
+
# An application's gems are installed within the application directory. By default they are
|
11
|
+
# places under `.bundle/gems`.
|
12
|
+
set(:bundle_dir) { "#{deploy_to}/.bundle/gems" }
|
13
|
+
|
14
|
+
# Not all gems are needed for production environments, so by default the `development`, `test` and
|
15
|
+
# `assets` groups are skipped.
|
16
|
+
set(:bundle_without) { "development test assets" }
|
17
|
+
|
18
|
+
namespace :bundle do
|
19
|
+
namespace :install do
|
20
|
+
# After cloning or updating the code, we only install the bundle if the `Gemfile` has changed.
|
21
|
+
desc "Install the latest gem bundle only if Gemfile.lock has changed"
|
22
|
+
task :if_changed do
|
23
|
+
if deployed_file_changed?(bundle_gemfile_lock)
|
24
|
+
top.bundle.install.default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Occassionally it's useful to force an install (such as if something has gone wrong in
|
29
|
+
# a previous deployment)
|
30
|
+
desc "Install the latest gem bundle"
|
31
|
+
task :default do
|
32
|
+
if deployed_file_exists?(bundle_gemfile)
|
33
|
+
bundler "install --gemfile #{bundle_gemfile} --path #{bundle_dir} --deployment --quiet --binstubs --without #{bundle_without}"
|
34
|
+
else
|
35
|
+
puts "Skipping bundle:install as no Gemfile found"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# To install the bundle automatically each time the code is updated or cloned, hooks are added to
|
42
|
+
# the `deploy:clone_code` and `deploy:update_code` tasks.
|
43
|
+
after 'deploy:clone_code', 'bundle:install:if_changed'
|
44
|
+
after 'deploy:update_code', 'bundle:install:if_changed'
|
45
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Recap
|
4
|
+
module CapistranoExtensions
|
5
|
+
# Run a command as the given user
|
6
|
+
def as_user(user, command, pwd = deploy_to)
|
7
|
+
sudo "su - #{user} -c 'cd #{pwd} && #{command}'"
|
8
|
+
end
|
9
|
+
|
10
|
+
# Run a command as root
|
11
|
+
def as_root(command, pwd = deploy_to)
|
12
|
+
as_user 'root', command, pwd
|
13
|
+
end
|
14
|
+
|
15
|
+
# Run a command as the application user
|
16
|
+
def as_app(command, pwd = deploy_to)
|
17
|
+
as_user application_user, command, pwd
|
18
|
+
end
|
19
|
+
|
20
|
+
# Put a string into a file as the application user
|
21
|
+
def put_as_app(string, path)
|
22
|
+
as_app "touch #{path} && chmod g+rw #{path}"
|
23
|
+
put string, path
|
24
|
+
end
|
25
|
+
|
26
|
+
def edit_file(path)
|
27
|
+
if editor = ENV['DEPLOY_EDITOR'] || ENV['EDITOR']
|
28
|
+
as_app "touch #{path} && chmod g+rw #{path}"
|
29
|
+
local_path = Tempfile.new('deploy-edit').path
|
30
|
+
get(path, local_path)
|
31
|
+
`#{editor} #{local_path}`
|
32
|
+
upload(local_path, path)
|
33
|
+
else
|
34
|
+
abort "To edit a remote file, either the EDITOR or DEPLOY_EDITOR environment variables must be set"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Run a git command in the `deploy_to` directory
|
39
|
+
def git(command)
|
40
|
+
run "cd #{deploy_to} && git #{command}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Capture the result of a git command run within the `deploy_to` directory
|
44
|
+
def capture_git(command)
|
45
|
+
capture "cd #{deploy_to} && git #{command}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Run a bundle command in the `deploy_to` directory
|
49
|
+
def bundler(command)
|
50
|
+
as_app "bundle #{command}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Find the latest tag from the repository. As `git tag` returns tags in order, and our release
|
54
|
+
# tags are timestamps, the latest tag will always be the last in the list.
|
55
|
+
def latest_tag_from_repository
|
56
|
+
result = capture_git("tag | tail -n1").strip
|
57
|
+
result.empty? ? nil : result
|
58
|
+
end
|
59
|
+
|
60
|
+
# Does the given file exist within the deployment directory?
|
61
|
+
def deployed_file_exists?(path)
|
62
|
+
capture("cd #{deploy_to} && [ -f #{path} ]; echo $?").strip == "0"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Has the given path been created or changed since the previous deployment? During the first
|
66
|
+
# successful deployment this will always return true.
|
67
|
+
def deployed_file_changed?(path)
|
68
|
+
return true unless latest_tag
|
69
|
+
capture_git("diff --exit-code #{latest_tag} origin/#{branch} #{path} > /dev/null; echo $?").strip == "1"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/recap/cli.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Recap
|
4
|
+
class CLI < Thor
|
5
|
+
include Thor::Actions
|
6
|
+
|
7
|
+
attr_accessor :name, :repository
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
File.expand_path("../templates", __FILE__)
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'setup', 'Setup basic capistrano recipes, e.g: recap setup'
|
14
|
+
method_option :name, :aliases => "-n"
|
15
|
+
method_option :repository, :aliases => "-r"
|
16
|
+
def setup
|
17
|
+
self.name = options["name"] || guess_name
|
18
|
+
self.repository = options["repo"] || guess_repository
|
19
|
+
template 'Capfile.erb', 'Capfile'
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def guess_name
|
25
|
+
Dir.pwd.split(File::SEPARATOR).last
|
26
|
+
end
|
27
|
+
|
28
|
+
def guess_repository
|
29
|
+
`git remote -v`.split[1]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# `recap` isn't intended to be compatible with tasks (such as those within the `bundler`
|
2
|
+
# or `whenever` projects) that are built on the original capistrano deployment recipes. At times
|
3
|
+
# though there are tasks that would work, but for some missing (and redundant) settings.
|
4
|
+
#
|
5
|
+
# Including this recipe adds these legacy settings, but provides no guarantee that original tasks
|
6
|
+
# will work. Many are based on assumptions about the deployment layout that no longer hold true.
|
7
|
+
|
8
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
9
|
+
extend Recap::CapistranoExtensions
|
10
|
+
|
11
|
+
# As `git` to manages releases, all deployments are placed directly in the `deploy_to` folder. The
|
12
|
+
# `current_path` is always this directory (no symlinking required).
|
13
|
+
set(:current_path) { deploy_to }
|
14
|
+
end
|
data/lib/recap/deploy.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'recap/capistrano_extensions'
|
2
|
+
require 'recap/bundler'
|
3
|
+
require 'recap/preflight'
|
4
|
+
|
5
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
6
|
+
extend Recap::CapistranoExtensions
|
7
|
+
|
8
|
+
# To use this recipe, both the application's name and its git repository are required.
|
9
|
+
set(:application) { abort "You must set the name of your application in your Capfile, e.g.: set :application, 'tomafro.net'" }
|
10
|
+
set(:repository) { abort "You must set the git respository location in your Capfile, e.g.: set :respository, 'git@github.com/tomafro/tomafro.net'"}
|
11
|
+
|
12
|
+
# The recipe assumes that the application code will be run as a dedicated user. Any any user who
|
13
|
+
# can deploy the application should be added as a member of the application's group. By default,
|
14
|
+
# both the application user and group take the same name as the application.
|
15
|
+
set(:application_user) { application }
|
16
|
+
set(:application_group) { application_user }
|
17
|
+
|
18
|
+
# Deployments can be made from any branch. `master` is used by default.
|
19
|
+
set(:branch, 'master')
|
20
|
+
|
21
|
+
# Unlike a standard capistrano deployment, all releases are stored directly in the `deploy_to`
|
22
|
+
# directory. The default is `/home/#{application_user}/apps/#{application}`.
|
23
|
+
set(:deploy_to) { "/home/#{application_user}/apps/#{application}" }
|
24
|
+
|
25
|
+
# Each release is marked by a unique tag, generated with the current timestamp. While this can be
|
26
|
+
# changed, it's not recommended, as the sort order of the tag names is important; later tags must
|
27
|
+
# be listed after earlier tags.
|
28
|
+
set(:release_tag) { "#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"}
|
29
|
+
|
30
|
+
# On tagging a release, a message is also recorded alongside the tag. This message can contain
|
31
|
+
# anything useful - its contents are not important for the recipe.
|
32
|
+
set(:release_message, "Deployed at #{Time.now}")
|
33
|
+
|
34
|
+
# Some tasks need to know the `latest_tag` - the most recent successful deployment. If no
|
35
|
+
# deployments have been made, this will be `nil`.
|
36
|
+
set(:latest_tag) { latest_tag_from_repository }
|
37
|
+
|
38
|
+
# To authenticate with github or other git servers, it is easier (and cleaner) to forward the
|
39
|
+
# deploying user's ssh key than manage keys on deployment servers.
|
40
|
+
ssh_options[:forward_agent] = true
|
41
|
+
|
42
|
+
# If key forwarding isn't possible, git may show a password prompt which stalls capistrano unless
|
43
|
+
# `:pty` is set to `true`.
|
44
|
+
default_run_options[:pty] = true
|
45
|
+
|
46
|
+
namespace :deploy do
|
47
|
+
# The `deploy:setup` task prepares all the servers for the deployment.
|
48
|
+
desc "Prepare servers for deployment"
|
49
|
+
task :setup, :except => {:no_release => true} do
|
50
|
+
transaction do
|
51
|
+
clone_code
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Clone the repository into the deployment directory.
|
56
|
+
task :clone_code, :except => {:no_release => true} do
|
57
|
+
# This is a slightly complicated process, as git doesn't allow us to clone into an existing
|
58
|
+
# directory. To get around this, using `sudo` we create the base deployment folder (if it
|
59
|
+
# doesn't already exist).
|
60
|
+
sudo "mkdir -p #{File.expand_path(deploy_to + "/..")}"
|
61
|
+
# Next, clone our code into a temporary location. This is necessary as our user might not have
|
62
|
+
# permission to write in the base deployment folder.
|
63
|
+
run "git clone #{repository} $HOME/#{application}.tmp"
|
64
|
+
# Again using `sudo`, move the temporary clone to its final destination.
|
65
|
+
sudo "mv $HOME/#{application}.tmp #{deploy_to}"
|
66
|
+
# Finally ensure that members of the `application_group` can read and write all files.
|
67
|
+
top.deploy.change_ownership
|
68
|
+
end
|
69
|
+
|
70
|
+
# Any files that have been created or updated by our user need to have thier permissions changed to
|
71
|
+
# ensure they can be read and written by and member of the `application_group` (deploying users and
|
72
|
+
# the application itself).
|
73
|
+
task :change_ownership, :except => {:no_release => true} do
|
74
|
+
run "find #{deploy_to} -user `whoami` ! -group #{application_group} -exec chown :#{application_group} {} \\;"
|
75
|
+
run "find #{deploy_to} -user `whoami` -exec chmod g+rw {} \\;"
|
76
|
+
end
|
77
|
+
|
78
|
+
# The main deployment task (called with `cap deploy`) deploys the latest application code to all
|
79
|
+
# servers, tags the release and restarts the application.
|
80
|
+
desc "Deploy the latest application code"
|
81
|
+
task :default do
|
82
|
+
transaction do
|
83
|
+
update_code
|
84
|
+
tag
|
85
|
+
end
|
86
|
+
restart
|
87
|
+
end
|
88
|
+
|
89
|
+
# Fetch the latest changes, then update `HEAD` to the deployment branch.
|
90
|
+
task :update_code, :except => {:no_release => true} do
|
91
|
+
on_rollback { git "reset --hard #{latest_tag}" if latest_tag }
|
92
|
+
git "fetch"
|
93
|
+
git "reset --hard origin/#{branch}"
|
94
|
+
# Finally ensure that the members of the `application_group` can read and write all files.
|
95
|
+
top.deploy.change_ownership
|
96
|
+
end
|
97
|
+
|
98
|
+
# Tag `HEAD` with the release tag and message
|
99
|
+
task :tag, :except => {:no_release => true} do
|
100
|
+
on_rollback { git "tag -d #{release_tag}" }
|
101
|
+
git "tag #{release_tag} -m '#{release_message}'"
|
102
|
+
top.deploy.change_ownership
|
103
|
+
end
|
104
|
+
|
105
|
+
# After a successful deployment, the app is restarted. In the most basic deployments this does
|
106
|
+
# nothing, but other recipes may override it, or attach tasks it's before or after hooks.
|
107
|
+
desc "Restart the application following a deploy"
|
108
|
+
task :restart do
|
109
|
+
end
|
110
|
+
|
111
|
+
# To rollback a release, the latest tag is deleted, and `HEAD` reset to the previous release
|
112
|
+
# (if one exists). Finally the application is restarted again.
|
113
|
+
desc "Rollback to the previous release"
|
114
|
+
namespace :rollback do
|
115
|
+
task :default do
|
116
|
+
if latest_tag
|
117
|
+
git "tag -d #{latest_tag}"
|
118
|
+
if previous_tag = latest_tag_from_repository
|
119
|
+
git "reset --hard #{previous_tag}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
restart
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# In case of emergency or when manually testing deployment, it can be useful to remove all
|
127
|
+
# previously deployed files before starting again.
|
128
|
+
desc "Remove all deployed files"
|
129
|
+
task :destroy do
|
130
|
+
sudo "rm -rf #{deploy_to}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/recap/env.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# N.B. To get the environment loaded on every shell invocation add the following to .profile:
|
2
|
+
#
|
3
|
+
# if [ -s "$HOME/.env" ]; then export $(cat $HOME/.env); fi
|
4
|
+
#
|
5
|
+
# This will eventually be done automatically
|
6
|
+
|
7
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
8
|
+
namespace :env do
|
9
|
+
set(:environment_file) { "/home/#{application_user}/.env" }
|
10
|
+
|
11
|
+
def extract_environment(declarations)
|
12
|
+
declarations.inject({}) do |env, line|
|
13
|
+
if line =~ /\A([A-Za-z_]+)=(.*)\z/
|
14
|
+
env[$1] = $2.strip
|
15
|
+
end
|
16
|
+
env
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_environment
|
21
|
+
@current_environment ||= begin
|
22
|
+
if deployed_file_exists?(environment_file)
|
23
|
+
extract_environment(capture("cat #{environment_file}").split("\n"))
|
24
|
+
else
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_environment(env)
|
31
|
+
env.keys.sort.collect do |v|
|
32
|
+
"#{v}=#{env[v]}" unless env[v].nil? || env[v].empty?
|
33
|
+
end.compact.join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default do
|
37
|
+
puts write_environment(current_environment)
|
38
|
+
end
|
39
|
+
|
40
|
+
task :set do
|
41
|
+
additions = extract_environment(ARGV[1..-1])
|
42
|
+
env = write_environment(current_environment.merge(additions))
|
43
|
+
if env.empty?
|
44
|
+
as_app "rm -f #{environment_file}"
|
45
|
+
else
|
46
|
+
put_as_app env, environment_file
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
task :edit do
|
51
|
+
edit_file environment_file
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|