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.
@@ -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 &hellip;
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">&#182;</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&rsquo;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 &mdash;global user.email &ldquo;you@example.com&rdquo;</code>
52
+ <code>git config &mdash;global user.name &ldquo;Your Name&rdquo;</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">&#182;</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">&#39;deploy:setup&#39;</span><span class="p">,</span> <span class="s1">&#39;preflight:check&#39;</span>
74
+ <span class="n">before</span> <span class="s1">&#39;deploy&#39;</span><span class="p">,</span> <span class="s1">&#39;preflight:check&#39;</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">&#39;whoami&#39;</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">&#182;</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">&quot;id </span><span class="si">#{</span><span class="n">application_user</span><span class="si">}</span><span class="s2"> &gt; /dev/null 2&gt;&amp;1; echo $?&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span> <span class="o">!=</span> <span class="s2">&quot;0&quot;</span>
91
+ <span class="nb">abort</span> <span class="sx">%{</span>
92
+ <span class="sx">The application user &#39;</span><span class="si">#{</span><span class="n">application_user</span><span class="si">}</span><span class="sx">&#39; doesn&#39;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">&#182;</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">&quot;id -g </span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="s2"> &gt; /dev/null 2&gt;&amp;1; echo $?&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span> <span class="o">!=</span> <span class="s2">&quot;0&quot;</span>
108
+ <span class="nb">abort</span> <span class="sx">%{</span>
109
+ <span class="sx">The application group &#39;</span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx">&#39; doesn&#39;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">&#182;</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">&#39;git config user.name || true&#39;</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">&#39;git config user.email || true&#39;</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 &quot;you@example.com&quot;</span>
130
+ <span class="sx"> git config --global user.name &quot;Your Name&quot;</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">&#182;</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">&#39;groups&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot; &quot;</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 &#39;</span><span class="si">#{</span><span class="n">application_group</span><span class="si">}</span><span class="sx">&#39; 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 &hellip;
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 &hellip;
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
@@ -0,0 +1,6 @@
1
+ require "recap/deploy"
2
+
3
+ set :application, "<%= name %>"
4
+
5
+ set :repository, "<%= repository %>"
6
+ set :branch, "master"
@@ -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