recap 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|