capistrano-distribution 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ require 'capistrano/distribution/distributor/abstract'
2
+
3
+ module Capistrano
4
+ class Distribution
5
+ module Distributor
6
+
7
+ ##
8
+ # @abstract Subclass and override {#check} and {#distribute} to create a
9
+ # distributor that uses Git to distribute code from a Git repository.
10
+ #
11
+ # An abstract distributor that operates on Git repositories.
12
+ class AbstractGit < Abstract
13
+ ##
14
+ # The identifier for a Git commit that will be distributed.
15
+ attr_reader :revision
16
+
17
+ ##
18
+ # @param url [URI, String] a URL to be used for fetching the artifact to be
19
+ # distributed
20
+ # @param revision [String] a commit identifier (revision SHA or ref) to be
21
+ # distributed
22
+ def initialize(context, url, revision, opts = {})
23
+ super(context, url, opts)
24
+ @revision = revision
25
+ @subtree = opts[:subtree]
26
+ end
27
+
28
+ private
29
+
30
+ ##
31
+ # A subtree of the repository to distribute.
32
+ attr_reader :subtree
33
+
34
+ ##
35
+ # Extracts the content of the commit at {#revision} from the local mirror of
36
+ # the repository to {#release_path}.
37
+ #
38
+ # @return [nil]
39
+ def release
40
+ context.execute 'mkdir', '-p', release_path
41
+
42
+ if subtree
43
+ path = subtree.slice %r#^/?(.*?)/?$#, 1
44
+ components = path.split('/').size
45
+ context.execute :git, '--git-dir', repo_path, :archive, revision, path, "| tar -x --strip-components #{components} -f - -C", release_path
46
+ else
47
+ context.execute :git, '--git-dir', repo_path, :archive, revision, '| tar -x -f - -C', release_path
48
+ end
49
+
50
+ nil
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ require 'capistrano/distribution/distributor/abstract_curl'
2
+ require 'capistrano/distribution/distributor/tar_helper'
3
+
4
+ module Capistrano
5
+ class Distribution
6
+ module Distributor
7
+
8
+ ##
9
+ # Deploys Tar files downloadable using the +curl+ command.
10
+ class CurlTar < AbstractCurl
11
+ include TarHelper
12
+
13
+ ##
14
+ # Extracts the content rooted under {#subtree} within the Tar file indicated
15
+ # by {#url} to the location indicated by {#release_path}. The Tar file is
16
+ # extracted as it is downloaded, so no local copy of the Tar file itself is
17
+ # ever created.
18
+ #
19
+ # @return [nil]
20
+ #
21
+ # @see Archiver#distribute
22
+ def distribute
23
+ context.execute 'mkdir', '-p', release_path
24
+ context.execute 'curl', '--fail', '--location', '--silent', url, '|',
25
+ 'tar', '-x',
26
+ compression_opt,
27
+ strip_components_opt,
28
+ '-C', release_path,
29
+ subtree
30
+ nil
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ require 'capistrano/distribution/distributor/abstract_curl'
2
+
3
+ module Capistrano
4
+ class Distribution
5
+ module Distributor
6
+
7
+ ##
8
+ # Deploys ZIP files downloadable using the +curl+ command.
9
+ class CurlZip < AbstractCurl
10
+ ##
11
+ # Extracts the content rooted under {#subtree} within the ZIP file indicated
12
+ # by {#url} to the location indicated by {#release_path}. Because the +unzip+
13
+ # utility is unable to extract a ZIP file over a pipe, a local copy of the
14
+ # entire ZIP file is made during the extraction process and then removed.
15
+ #
16
+ # @return [nil]
17
+ #
18
+ # @see Archiver#distribute
19
+ def distribute
20
+ zip_path = repo_path.to_s + '.zip'
21
+
22
+ context.execute 'curl', '--fail', '--location', '--silent',
23
+ '--output', zip_path, url
24
+
25
+ context.execute 'mkdir', '-p', repo_path
26
+ context.execute 'unzip', '-q',
27
+ '-d', repo_path,
28
+ zip_path,
29
+ subtree.join('\\*')
30
+ context.execute 'mv', repo_path.join(subtree), release_path
31
+
32
+ context.execute 'rm', '-rf', zip_path, repo_path
33
+
34
+ nil
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,67 @@
1
+ require 'capistrano/distribution/distributor/abstract_git'
2
+
3
+ module Capistrano
4
+ class Distribution
5
+ module Distributor
6
+
7
+ ##
8
+ # Distributes by mirroring a remote Git repository on each target host and then
9
+ # extracting the content from a revision to the release location.
10
+ class GitPull < AbstractGit
11
+ ##
12
+ # Tests that the Git repository at {#url} is readable.
13
+ #
14
+ # @return [Boolean] +true+ if the repository is readable; otherwise, +false+
15
+ def check
16
+ context.test 'git', 'ls-remote', url
17
+ end
18
+
19
+ ##
20
+ # Creates a mirror of the Git repository at {#url} if necessary, pulls updates
21
+ # into it, and finally extracts the revision to the release area. The Git
22
+ # repository is left in place in order to speed up future deployments by
23
+ # avoiding the need to pull *all* revision history again.
24
+ #
25
+ # @return [nil]
26
+ #
27
+ # @raise [exception] when distribution fails.
28
+ def distribute
29
+ clone
30
+ update
31
+ release
32
+ end
33
+
34
+ private
35
+
36
+ ##
37
+ # Creates a mirror of the repository at {#url} in {#repo_path} if it does not
38
+ # already exist.
39
+ #
40
+ # @return [nil]
41
+ #
42
+ # @raise [exception] when mirror creation fails.
43
+ def clone
44
+ if context.test '[', '!', '-e', repo_path.join('HEAD'), ']'
45
+ context.execute 'rm', '-rf', repo_path
46
+ context.execute 'git', 'clone', '--mirror', url, repo_path
47
+ end
48
+ nil
49
+ end
50
+
51
+ ##
52
+ # Updates the Git mirror.
53
+ #
54
+ # @return [nil]
55
+ #
56
+ # @raise [exception] when updating the mirror fails.
57
+ def update
58
+ context.execute 'git', '--git-dir', repo_path,
59
+ 'remote', 'set-url', 'origin', url
60
+ context.execute 'git', '--git-dir', repo_path, 'remote', 'update', '--prune'
61
+ nil
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,73 @@
1
+ require 'capistrano/distribution/distributor/abstract_git'
2
+
3
+ module Capistrano
4
+ class Distribution
5
+ module Distributor
6
+
7
+ ##
8
+ # Distributes by pushing a local Git repository into a clone on each target host
9
+ # and then extracting the content from a revision to the release location.
10
+ class GitPush < AbstractGit
11
+ ##
12
+ # Always returns +true+ because this distributor does not pull content but
13
+ # rather relies on the distribute step to push the content up.
14
+ #
15
+ # @return [true]
16
+ def check
17
+ true
18
+ end
19
+
20
+ ##
21
+ # Prepares a Git repository if necessary, pushes the selected revision to the
22
+ # Git repository, and finally extracts the revision to the release area. The
23
+ # Git repository is left in place in order to speed up future deployments by
24
+ # avoiding the need to upload *all* revision history again.
25
+ #
26
+ # @return [nil]
27
+ #
28
+ # @raise [exception] when distribution fails.
29
+ def distribute
30
+ init
31
+ push
32
+ release
33
+ nil
34
+ end
35
+
36
+ private
37
+
38
+ ##
39
+ # Initializes a bare Git repository in {#repo_path} if a repository does not
40
+ # already exist there.
41
+ #
42
+ # @return [nil]
43
+ #
44
+ # @raise [exception] when repository creation fails.
45
+ def init
46
+ if context.test '[', '!', '-e', repo_path.join('HEAD'), ']'
47
+ context.execute 'rm', '-rf', repo_path
48
+ context.execute 'git', 'init', '--bare', repo_path
49
+ end
50
+ nil
51
+ end
52
+
53
+ ##
54
+ # Pushes {#revision} and all its history to the Git repository within
55
+ # {#repo_path}.
56
+ #
57
+ # @raise [exception] when the push to the repository fails.
58
+ def push
59
+ user = context.host.user
60
+ hostname = context.host.hostname
61
+ repo_path = self.repo_path
62
+ revision = self.revision
63
+ context.run_locally do
64
+ execute 'git', 'push',
65
+ "ssh://#{user}@#{hostname}/#{repo_path}",
66
+ "+#{revision}:refs/tags/deploy"
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,35 @@
1
+ require 'capistrano/distribution/distributor/abstract_archiver'
2
+ require 'capistrano/distribution/distributor/tar_helper'
3
+
4
+ module Capistrano
5
+ class Distribution
6
+ module Distributor
7
+
8
+ ##
9
+ # Deploys locally available Tar files.
10
+ class Tar < AbstractArchiver
11
+ include TarHelper
12
+
13
+ ##
14
+ # Extracts the content rooted under {#subtree} within the Tar file indicated
15
+ # by {#url} to the location indicated by {#release_path}.
16
+ #
17
+ # @return [nil]
18
+ #
19
+ # @see Abstract#distribute
20
+ def distribute
21
+ context.execute 'mkdir', '-p', release_path
22
+ context.execute 'tar', '-x',
23
+ compression_opt,
24
+ strip_components_opt,
25
+ '-C', release_path,
26
+ '-f', url.path,
27
+ subtree
28
+
29
+ nil
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ module Capistrano
2
+ class Distribution
3
+ module Distributor
4
+
5
+ ##
6
+ # A mixin containing methods useful for distributors that extract Tar files.
7
+ module TarHelper
8
+ private
9
+
10
+ ##
11
+ # @return [String] the tar compression option to use based on the
12
+ # {Abstract#url} attribute
13
+ #
14
+ # @raise [RuntimeError] if the compression option cannot be guessed.
15
+ def compression_opt
16
+ case url.path
17
+ when %r{\.(tar\.gz|tgz)$}
18
+ '-z'
19
+ when %r{\.(tar\.bz2|tbz2)$}
20
+ '-j'
21
+ when %r{\.tar$}
22
+ ''
23
+ else
24
+ raise "Unable to guess decompression option for URL: #{url}"
25
+ end
26
+ end
27
+
28
+ ##
29
+ # @return [String] the component stripping option based on the number of path
30
+ # elements in the {AbstractArchiver#subtree} attribute
31
+ def strip_components_opt
32
+ path = subtree.to_s.slice %r#^/?(.*?)/?$#, 1
33
+ components = path.split('/').size
34
+ "--strip-components #{components}"
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ require 'capistrano/distribution/distributor/abstract_archiver'
2
+
3
+ module Capistrano
4
+ class Distribution
5
+ module Distributor
6
+
7
+ ##
8
+ # Deploys locally available ZIP files.
9
+ class Zip < AbstractArchiver
10
+ ##
11
+ # Extracts the content rooted under {#subtree} within the ZIP file indicated
12
+ # by {#url} to the location indicated by {#release_path}.
13
+ #
14
+ # @return [nil]
15
+ #
16
+ # @see Abstract#distribute
17
+ def distribute
18
+ context.execute 'mkdir', '-p', repo_path
19
+ context.execute 'unzip', '-q',
20
+ '-d', repo_path,
21
+ url.path,
22
+ subtree.join('\\*')
23
+ context.execute 'mv', repo_path.join(subtree), release_path
24
+
25
+ nil
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ require 'digest'
2
+ require 'pathname'
3
+ require 'uri'
4
+
5
+ module Capistrano
6
+ class Distribution
7
+
8
+ ##
9
+ # This module is a factory for creating distributor objects that are able to
10
+ # fetch distributable artifacts from various sources and deploy them to a
11
+ # target host.
12
+ module Distributor
13
+ ##
14
+ # Create a concrete distributor object based on _definition_.
15
+ #
16
+ # When _definition_ is a String, it is assumed to be a URL that is used to
17
+ # guess about the type of distributor to create. For instance, URLs that
18
+ # end with `.git` will elicit the Git::Pull distributor. A URL without a
19
+ # scheme and ending with .tgz will elicit the Tar distributor.
20
+ #
21
+ # When _definition_ is an Array and the first element is a String, the
22
+ # distributor class is guessed using that String as in the case when
23
+ # _definition_ is simply a String itself. The remaining members of the
24
+ # array are passed to the selected class' initializer as arguments.
25
+ #
26
+ # When _definition_ is an Array and the first element is a Symbol, the
27
+ # Symbol is assumed to map to the class of the distributor to instantiate.
28
+ # The Symbol +:curl_tar+ maps to the Curl::Tar distributor for example. The
29
+ # remaining members of the array are passed to the selected class'
30
+ # initializer as arguments.
31
+ #
32
+ # When _definition_ is anything else, it is returned directly under the
33
+ # assumption that the configuration explicitly created a distributor.
34
+ #
35
+ # @param context [{#test, #execute}] a Capistrano context used to run
36
+ # commands.
37
+ # @param definition [String, Array, other] a distributor definition.
38
+ #
39
+ # @return a distributor
40
+ def self.create(context, definition)
41
+ case definition
42
+ when String
43
+ type = type_from_url(definition)
44
+ constantize(type).new(context, definition)
45
+ when Array
46
+ if Symbol === definition.first
47
+ type, *args = definition
48
+ else
49
+ type = type_from_url(definition.first)
50
+ args = definition
51
+ end
52
+ constantize(type).new(context, *args)
53
+ else
54
+ definition
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ ##
61
+ # Return a distributor type based on _url_.
62
+ #
63
+ # @param url [URI] a URL denoting the location of the artifact to be
64
+ # deployed.
65
+ #
66
+ # @return [Symbol] a distributor type
67
+ def self.type_from_url(url)
68
+ url = URI.parse(url.to_s)
69
+
70
+ type =
71
+ if url.scheme == 'git' || url.path =~ %r{\.git$}
72
+ :git_pull
73
+ elsif url.scheme.nil? || url.scheme == 'file'
74
+ if url.path =~ %r{\.(tar(\.(gz|bzip2))?|tgz|tbz2)$}i
75
+ :tar
76
+ elsif url.path =~ %r{\.zip$}i
77
+ :zip
78
+ end
79
+ elsif url.path =~ %r{\.(tar(\.(gz|bzip2))?|tgz|tbz2)$}i
80
+ :curl_tar
81
+ elsif url.path =~ %r{\.zip$}i
82
+ :curl_zip
83
+ end
84
+
85
+ raise "Unable to guess distributor type for URL: #{url}" if type.nil?
86
+ type
87
+ end
88
+
89
+ ##
90
+ # Finds the class to use for a distributor given a Symbol.
91
+ #
92
+ # @param type [Symbol] maps to a distributor class.
93
+ #
94
+ # @return a distributor class.
95
+ def self.constantize(type)
96
+ require "capistrano/distribution/distributor/#{type}"
97
+ const_get(type.to_s.split('_').map(&:capitalize).join)
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,9 @@
1
+ module Capistrano
2
+
3
+ class Distribution
4
+ ##
5
+ # The release version of this gem.
6
+ VERSION = '0.2.0'
7
+ end
8
+
9
+ end
@@ -0,0 +1,68 @@
1
+ require 'capistrano/distribution/distributor'
2
+ load File.expand_path('../tasks/distribution.cap', __FILE__)
3
+
4
+ module Capistrano
5
+
6
+ ##
7
+ # A list of distributors to run.
8
+ class Distribution
9
+ ##
10
+ # Creates the list of distributors to run based on the definition found in the
11
+ # +:distribution+ key of _context_. The value associated the +distribution+
12
+ # can be 1 of 3 types:
13
+ # * String
14
+ # * Array of Arrays of distributor initialization arguments
15
+ # * Array of distributor instances
16
+ #
17
+ # @param context [{#fetch, #repo_path, #release_path}] a Capistrano deployment
18
+ # context.
19
+ #
20
+ # @return [Distribution] an instance of this class.
21
+ def initialize(context)
22
+ @context = context
23
+ distributor_list = context.fetch(:distribution)
24
+
25
+ case distributor_list
26
+ when String
27
+ distributor_list = [[distributor_list]]
28
+ when Array
29
+ unless distributor_list.all? { |distributor| Array === distributor }
30
+ distributor_list = [distributor_list]
31
+ end
32
+ end
33
+
34
+ @distributors = distributor_list.map do |distributor|
35
+ Distributor.create(context, distributor)
36
+ end
37
+ end
38
+
39
+ ##
40
+ # An identifier for a release.
41
+ def release_id
42
+ context.fetch(:release_id)
43
+ end
44
+
45
+ ##
46
+ # Calls the #check method of each distributor in the list.
47
+ def check
48
+ distributors.all? { |distributor| distributor.check }
49
+ end
50
+
51
+ ##
52
+ # Calls the #distribute method of each distributor in the list.
53
+ def distribute
54
+ distributors.each { |distributor| distributor.distribute }
55
+ end
56
+
57
+ private
58
+
59
+ ##
60
+ # The list of distributors.
61
+ attr_reader :distributors
62
+
63
+ ##
64
+ # The Capistrano context object.
65
+ attr_reader :context
66
+ end
67
+
68
+ end
@@ -0,0 +1,33 @@
1
+ namespace :distribution do
2
+ def distribution
3
+ @distribution ||= Capistrano::Distribution.new(self)
4
+ end
5
+
6
+ desc 'Confirm that all required distributables are available'
7
+ task :check do
8
+ on release_roles(:all), fetch(:distribution_runner_opts) do
9
+ exit 1 unless distribution.check
10
+ end
11
+ end
12
+
13
+ desc 'Distribute the artifacts'
14
+ task :create_release do
15
+ on release_roles(:all), fetch(:distribution_runner_opts) do
16
+ within repo_path do
17
+ distribution.distribute
18
+ end
19
+ end
20
+ end
21
+
22
+ desc 'Set a revision identifier'
23
+ task :set_current_revision do
24
+ set(:current_revision, distribution.release_id)
25
+ end
26
+ end
27
+
28
+ namespace :load do
29
+ task :defaults do
30
+ puts 'foo'
31
+ set :distribution_runner_opts, {}
32
+ end
33
+ end