recap 0.1.0

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