capistrano-distribution 0.2.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,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