minestrone 0.0.1

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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +32 -0
  3. data/.gitignore +5 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +35 -0
  7. data/Rakefile +10 -0
  8. data/bin/capify +89 -0
  9. data/bin/min +5 -0
  10. data/docs/lib-codebase-map.md +162 -0
  11. data/docs/lib-dependency-graph.svg +129 -0
  12. data/lib/minestrone/callback.rb +45 -0
  13. data/lib/minestrone/cli/help.rb +131 -0
  14. data/lib/minestrone/cli/help.txt +72 -0
  15. data/lib/minestrone/cli/options.rb +232 -0
  16. data/lib/minestrone/cli.rb +159 -0
  17. data/lib/minestrone/command.rb +177 -0
  18. data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
  19. data/lib/minestrone/configuration/actions/inspect.rb +46 -0
  20. data/lib/minestrone/configuration/actions/invocation.rb +202 -0
  21. data/lib/minestrone/configuration/alias_task.rb +29 -0
  22. data/lib/minestrone/configuration/callbacks.rb +129 -0
  23. data/lib/minestrone/configuration/connections.rb +66 -0
  24. data/lib/minestrone/configuration/execution.rb +139 -0
  25. data/lib/minestrone/configuration/loading.rb +207 -0
  26. data/lib/minestrone/configuration/log_formatters.rb +75 -0
  27. data/lib/minestrone/configuration/namespaces.rb +225 -0
  28. data/lib/minestrone/configuration/servers.rb +70 -0
  29. data/lib/minestrone/configuration/variables.rb +115 -0
  30. data/lib/minestrone/configuration.rb +69 -0
  31. data/lib/minestrone/errors.rb +17 -0
  32. data/lib/minestrone/ext/string.rb +7 -0
  33. data/lib/minestrone/extensions.rb +56 -0
  34. data/lib/minestrone/logger.rb +171 -0
  35. data/lib/minestrone/processable.rb +50 -0
  36. data/lib/minestrone/recipes/deploy/assets.rb +194 -0
  37. data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
  38. data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
  39. data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
  40. data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
  41. data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
  42. data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
  43. data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
  44. data/lib/minestrone/recipes/deploy/scm.rb +22 -0
  45. data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
  46. data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
  47. data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
  48. data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
  49. data/lib/minestrone/recipes/deploy.rb +639 -0
  50. data/lib/minestrone/recipes/standard.rb +23 -0
  51. data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
  52. data/lib/minestrone/server_definition.rb +56 -0
  53. data/lib/minestrone/ssh.rb +81 -0
  54. data/lib/minestrone/task_definition.rb +82 -0
  55. data/lib/minestrone/transfer.rb +205 -0
  56. data/lib/minestrone/version.rb +11 -0
  57. data/lib/minestrone.rb +3 -0
  58. data/minestrone.gemspec +32 -0
  59. data/test/cli/execute_test.rb +130 -0
  60. data/test/cli/help_test.rb +178 -0
  61. data/test/cli/options_test.rb +315 -0
  62. data/test/cli/ui_test.rb +26 -0
  63. data/test/cli_test.rb +17 -0
  64. data/test/command_test.rb +305 -0
  65. data/test/configuration/actions/file_transfer_test.rb +61 -0
  66. data/test/configuration/actions/inspect_test.rb +76 -0
  67. data/test/configuration/actions/invocation_test.rb +258 -0
  68. data/test/configuration/alias_task_test.rb +110 -0
  69. data/test/configuration/callbacks_test.rb +201 -0
  70. data/test/configuration/connections_test.rb +192 -0
  71. data/test/configuration/execution_test.rb +176 -0
  72. data/test/configuration/loading_test.rb +149 -0
  73. data/test/configuration/namespace_dsl_test.rb +325 -0
  74. data/test/configuration/servers_test.rb +100 -0
  75. data/test/configuration/variables_test.rb +191 -0
  76. data/test/configuration_test.rb +77 -0
  77. data/test/deploy/local_dependency_test.rb +61 -0
  78. data/test/deploy/remote_dependency_test.rb +146 -0
  79. data/test/deploy/scm/base_test.rb +55 -0
  80. data/test/deploy/scm/git_test.rb +260 -0
  81. data/test/deploy/scm/none_test.rb +26 -0
  82. data/test/deploy/strategy/copy_test.rb +360 -0
  83. data/test/extensions_test.rb +69 -0
  84. data/test/fixtures/cli_integration.rb +5 -0
  85. data/test/fixtures/config.rb +4 -0
  86. data/test/fixtures/custom.rb +3 -0
  87. data/test/logger_formatting_test.rb +149 -0
  88. data/test/logger_test.rb +134 -0
  89. data/test/recipes_test.rb +26 -0
  90. data/test/server_definition_test.rb +121 -0
  91. data/test/ssh_test.rb +99 -0
  92. data/test/task_definition_test.rb +117 -0
  93. data/test/transfer_test.rb +172 -0
  94. data/test/utils.rb +28 -0
  95. data/test/version_test.rb +11 -0
  96. metadata +258 -0
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minestrone/recipes/deploy/scm/base'
4
+
5
+ module Minestrone
6
+ module Deploy
7
+ module SCM
8
+
9
+ # An SCM module for using Git as your source control tool.
10
+ #
11
+ # Assumes you are using a shared Git repository.
12
+ #
13
+ # Parts of this plugin borrowed from Scott Chacon's version, which I
14
+ # found on the Minestrone mailing list but failed to be able to get
15
+ # working.
16
+ #
17
+ # FEATURES:
18
+ #
19
+ # * Very simple, only requiring 2 lines in your deploy.rb.
20
+ # * Can deploy different branches, tags, or any SHA1 easily.
21
+ # * Supports prompting for password / passphrase upon checkout.
22
+ # (I am amazed at how some plugins don't do this)
23
+ # * Supports :scm_command, :scm_passphrase Minestrone directives.
24
+ #
25
+ # CONFIGURATION
26
+ # -------------
27
+ #
28
+ # Use this plugin by adding the following line in your config/deploy.rb:
29
+ #
30
+ # set :scm, :git
31
+ #
32
+ # Set <tt>:repository</tt> to the path of your Git repo:
33
+ #
34
+ # set :repository, "someuser@somehost:/home/myproject"
35
+ #
36
+ # The above two options are required to be set, the ones below are
37
+ # optional.
38
+ #
39
+ # You may set <tt>:branch</tt>, which is the reference to the branch, tag,
40
+ # or any SHA1 you are deploying, for example:
41
+ #
42
+ # set :branch, "master"
43
+ #
44
+ # Otherwise, HEAD is assumed. I strongly suggest you set this. HEAD is
45
+ # not always the best assumption.
46
+ #
47
+ # You may also set <tt>:remote</tt>, which will be used as a name for remote
48
+ # tracking of repositories. This option is intended for use with the
49
+ # <tt>:remote_cache</tt> strategy in a distributed git environment.
50
+ #
51
+ # For example in the projects <tt>config/deploy.rb</tt>:
52
+ #
53
+ # set :repository, "#{scm_user}@somehost:~/projects/project.git"
54
+ # set :remote, "#{scm_user}"
55
+ #
56
+ # Then each person with deploy priveledges can add the following to their
57
+ # local <tt>~/.caprc</tt> file:
58
+ #
59
+ # set :scm_user, 'someuser'
60
+ #
61
+ # Now any time a person deploys the project, their repository will be
62
+ # setup as a remote git repository within the cached repository.
63
+ #
64
+ # The <tt>:scm_command</tt> configuration variable, if specified, will
65
+ # be used as the full path to the git executable on the *remote* machine:
66
+ #
67
+ # set :scm_command, "/opt/local/bin/git"
68
+ #
69
+ # <tt>:scm_passphrase</tt> is supported for the public key passphrase.
70
+ #
71
+ # The remote cache strategy is also supported.
72
+ #
73
+ # set :repository_cache, "git_master"
74
+ # set :deploy_via, :remote_cache
75
+ #
76
+ # For faster clone, you can also use shallow cloning. This will set the
77
+ # '--depth' flag using the depth specified. This *cannot* be used
78
+ # together with the :remote_cache strategy
79
+ #
80
+ # set :git_shallow_clone, 1
81
+ #
82
+ # To deploy from a local repository:
83
+ #
84
+ # set :repository, "file://."
85
+ # set :deploy_via, :copy
86
+ #
87
+ # AUTHORS
88
+ # -------
89
+ #
90
+ # Garry Dolley http://scie.nti.st
91
+ # Contributions by Geoffrey Grosenbach http://topfunky.com
92
+ # Scott Chacon http://jointheconversation.org
93
+ # Alex Arnell http://twologic.com
94
+ # and Phillip Goldenburg
95
+
96
+ class Git < Base
97
+ # Sets the default command name for this SCM on your *local* machine.
98
+ # Users may override this by setting the :scm_command variable.
99
+ default_command "git"
100
+
101
+ # When referencing "head", use the branch we want to deploy or, by
102
+ # default, Git's reference of HEAD (the latest changeset in the default
103
+ # branch, usually called "master").
104
+ def head
105
+ variable(:branch) || 'HEAD'
106
+ end
107
+
108
+ def origin
109
+ variable(:remote) || 'origin'
110
+ end
111
+
112
+ # Performs a clone on the remote machine, then checkout on the branch
113
+ # you want to deploy.
114
+ def checkout(revision, destination)
115
+ git = command
116
+ remote = origin
117
+
118
+ args = []
119
+
120
+ # Add an option for the branch name so :git_shallow_clone works with branches
121
+ args << "-b #{variable(:branch)}" unless variable(:branch).nil? || variable(:branch) == revision
122
+ args << "-o #{remote}" unless remote == 'origin'
123
+
124
+ if depth = variable(:git_shallow_clone)
125
+ args << "--depth #{depth}"
126
+ end
127
+
128
+ execute = []
129
+ execute << "#{git} clone #{verbose} #{args.join(' ')} #{variable(:repository)} #{destination}"
130
+
131
+ # checkout into a local branch rather than a detached HEAD
132
+ execute << "cd #{destination} && #{git} checkout #{verbose} -b deploy #{revision}"
133
+
134
+ if variable(:git_enable_submodules)
135
+ execute << "#{git} submodule #{verbose} init"
136
+ execute << "#{git} submodule #{verbose} sync"
137
+
138
+ if variable(:git_submodules_recursive) == false
139
+ execute << "#{git} submodule #{verbose} update --init"
140
+ else
141
+ execute << %Q(export GIT_RECURSIVE=$([ ! "`#{git} --version`" \\< "git version 1.6.5" ] && echo --recursive))
142
+ execute << "#{git} submodule #{verbose} update --init $GIT_RECURSIVE"
143
+ end
144
+ end
145
+
146
+ execute.compact.join(" && ").gsub(/\s+/, ' ')
147
+ end
148
+
149
+ # An expensive export. Performs a checkout as above, then
150
+ # removes the repo.
151
+ def export(revision, destination)
152
+ checkout(revision, destination) << " && rm -Rf #{destination}/.git"
153
+ end
154
+
155
+ # Merges the changes to 'head' since the last fetch, for remote_cache
156
+ # deployment strategy
157
+ def sync(revision, destination)
158
+ git = command
159
+ remote = origin
160
+
161
+ execute = []
162
+ execute << "cd #{destination}"
163
+
164
+ # Use git-config to setup a remote tracking branches. Could use
165
+ # git-remote but it complains when a remote of the same name already
166
+ # exists, git-config will just silenty overwrite the setting every
167
+ # time. This could cause wierd-ness in the remote cache if the url
168
+ # changes between calls, but as long as the repositories are all
169
+ # based from each other it should still work fine.
170
+
171
+ if remote != 'origin'
172
+ execute << "#{git} config remote.#{remote}.url #{variable(:repository)}"
173
+ execute << "#{git} config remote.#{remote}.fetch +refs/heads/*:refs/remotes/#{remote}/*"
174
+ end
175
+
176
+ # since we're in a local branch already, just reset to specified revision rather than merge
177
+ execute << "#{git} fetch #{verbose} #{remote} && #{git} fetch --tags #{verbose} #{remote} && #{git} reset #{verbose} --hard #{revision}"
178
+
179
+ if variable(:git_enable_submodules)
180
+ execute << "#{git} submodule #{verbose} init"
181
+ execute << "#{git} submodule #{verbose} sync"
182
+
183
+ if variable(:git_submodules_recursive) == false
184
+ execute << "#{git} submodule #{verbose} update --init"
185
+ else
186
+ execute << %Q(export GIT_RECURSIVE=$([ ! "`#{git} --version`" \\< "git version 1.6.5" ] && echo --recursive))
187
+ execute << "#{git} submodule #{verbose} update --init $GIT_RECURSIVE"
188
+ end
189
+ end
190
+
191
+ # Make sure there's nothing else lying around in the repository (for
192
+ # example, a submodule that has subsequently been removed).
193
+ execute << "#{git} clean #{verbose} -d -x -f"
194
+
195
+ execute.join(" && ")
196
+ end
197
+
198
+ # Returns a string of diffs between two revisions
199
+ def diff(from, to = nil)
200
+ if to
201
+ scm :diff, "#{from}..#{to}"
202
+ else
203
+ scm :diff, from
204
+ end
205
+ end
206
+
207
+ # Returns a log of changes between the two revisions (inclusive).
208
+ def log(from, to = nil)
209
+ scm :log, "#{from}..#{to}"
210
+ end
211
+
212
+ # Getting the actual commit id, in case we were passed a tag
213
+ # or partial sha or something - it will return the sha if you pass a sha, too
214
+ def query_revision(revision)
215
+ raise ArgumentError, "Deploying remote branches is no longer supported. Specify the remote branch as a local branch for the git repository you're deploying from (ie: '#{revision.gsub('origin/', '')}' rather than '#{revision}')." if revision =~ /^origin\//
216
+
217
+ return revision if revision =~ /^[0-9a-f]{40}$/
218
+
219
+ command = scm('ls-remote', repository, revision)
220
+ result = yield(command)
221
+ revdata = result.split(/[\t\n]/)
222
+ newrev = nil
223
+
224
+ revdata.each_slice(2) do |refs|
225
+ rev, ref = *refs
226
+ if ref.sub(/refs\/.*?\//, '').strip == revision.to_s
227
+ newrev = rev
228
+ break
229
+ end
230
+ end
231
+
232
+ return newrev if newrev =~ /^[0-9a-f]{40}$/
233
+
234
+ # If sha is not found on remote, try expanding from local repository
235
+ command = scm('rev-parse --revs-only', origin + '/' + revision)
236
+ newrev = yield(command).to_s.strip
237
+
238
+ # fallback for expected legacy default functionality
239
+ unless newrev =~ /^[0-9a-f]{40}$/
240
+ command = scm('rev-parse --revs-only', revision)
241
+ newrev = yield(command).to_s.strip
242
+ end
243
+
244
+ raise "Unable to resolve revision for '#{revision}' on repository '#{repository}'." unless newrev =~ /^[0-9a-f]{40}$/
245
+ return newrev
246
+ end
247
+
248
+ # Determines what the response should be for a particular bit of text
249
+ # from the SCM. Password prompts, connection requests, passphrases,
250
+ # etc. are handled here.
251
+ def handle_data(state, stream, text)
252
+ host = state[:channel][:host]
253
+ logger.info "[#{host} :: #{stream}] #{text}"
254
+ case text
255
+ when /\bpassword.*:/i
256
+ # git is prompting for a password
257
+ pass = Minestrone::CLI.password_prompt
258
+ %("#{pass}"\n)
259
+ when %r{\(yes/no\)}
260
+ # git is asking whether or not to connect
261
+ "yes\n"
262
+ when /passphrase/i
263
+ # git is asking for the passphrase for the user's key
264
+ unless pass = variable(:scm_passphrase)
265
+ pass = Minestrone::CLI.password_prompt
266
+ end
267
+ %("#{pass}"\n)
268
+ when /accept \(t\)emporarily/
269
+ # git is asking whether to accept the certificate
270
+ "t\n"
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ # If verbose output is requested, return nil, otherwise return the
277
+ # command-line switch for "quiet" ("-q").
278
+ def verbose
279
+ variable(:scm_verbose) ? nil : "-q"
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minestrone/recipes/deploy/scm/base'
4
+
5
+ module Minestrone
6
+ module Deploy
7
+ module SCM
8
+
9
+ # A trivial SCM wrapper for representing the current working directory
10
+ # as a repository. Obviously, not all operations are available for this
11
+ # SCM, but it works sufficiently for use with the "copy" deployment
12
+ # strategy.
13
+ #
14
+ # Use of this module is _not_ recommended; in general, it is good
15
+ # practice to use some kind of source code management even for anything
16
+ # you are wanting to deploy. However, this module is provided in
17
+ # acknowledgement of the cases where trivial deployment of your current
18
+ # working directory is desired.
19
+ #
20
+ # set :repository, "."
21
+ # set :scm, :none
22
+ # set :deploy_via, :copy
23
+ #
24
+ # Dereference symbolic links. Copy files instead. Handy when you
25
+ # reference files and directory outside of your deployment root.
26
+ # set :copy_dereference_symlink, true
27
+
28
+ class None < Base
29
+
30
+ # No versioning, thus, no head. Returns the empty string.
31
+ def head
32
+ ""
33
+ end
34
+
35
+ # Simply does a copy from the :repository directory to the :destination directory.
36
+ def checkout(revision, destination)
37
+ "cp -R#{configuration[:copy_dereference_symlink] ? 'L' : ''} #{repository} #{destination}"
38
+ end
39
+
40
+ alias_method :export, :checkout
41
+
42
+ # No versioning, so this just returns the argument, with no modification.
43
+ def query_revision(revision)
44
+ revision
45
+ end
46
+
47
+ # log: There's no log, so it just echos from and to.
48
+ def log(from = "", to = "")
49
+ "No SCM: #{from} - #{to}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minestrone
4
+ module Deploy
5
+ module SCM
6
+ def self.new(scm, config = {})
7
+ scm_file = "minestrone/recipes/deploy/scm/#{scm}"
8
+ require(scm_file)
9
+
10
+ scm_const = scm.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
11
+
12
+ if const_defined?(scm_const)
13
+ const_get(scm_const).new(config)
14
+ else
15
+ raise Minestrone::Error, "could not find `#{name}::#{scm_const}' in `#{scm_file}'"
16
+ end
17
+ rescue LoadError
18
+ raise Minestrone::Error, "could not find any SCM named `#{scm}'"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'minestrone/recipes/deploy/dependencies'
5
+
6
+ module Minestrone
7
+ module Deploy
8
+ module Strategy
9
+
10
+ # This class defines the abstract interface for all Minestrone
11
+ # deployment strategies. Subclasses must implement at least the
12
+ # #deploy! method.
13
+
14
+ class Base
15
+ attr_reader :configuration
16
+
17
+ # Instantiates a strategy with a reference to the given configuration.
18
+ def initialize(config = {})
19
+ @configuration = config
20
+ end
21
+
22
+ # Executes the necessary commands to deploy the revision of the source
23
+ # code identified by the +revision+ variable. Additionally, this
24
+ # should write the value of the +revision+ variable to a file called
25
+ # REVISION, in the base of the deployed revision. This file is used by
26
+ # other tasks, to perform diffs and such.
27
+
28
+ def deploy!
29
+ raise NotImplementedError, "`deploy!' is not implemented by #{self.class.name}"
30
+ end
31
+
32
+ # Performs a check on the remote hosts to determine whether everything
33
+ # is setup such that a deploy could succeed.
34
+
35
+ def check!
36
+ Dependencies.new(configuration) do |d|
37
+ d.remote.directory(configuration[:releases_path])
38
+ .or("`#{configuration[:releases_path]}' does not exist. Please run `min deploy:setup'.")
39
+ d.remote.writable(configuration[:deploy_to])
40
+ .or("You do not have permissions to write to `#{configuration[:deploy_to]}'.")
41
+ d.remote.writable(configuration[:releases_path])
42
+ .or("You do not have permissions to write to `#{configuration[:releases_path]}'.")
43
+ end
44
+ end
45
+
46
+
47
+ protected
48
+
49
+ # This is to allow helper methods like "run" and "put" to be more
50
+ # easily accessible to strategy implementations.
51
+ def method_missing(sym, *args, &block)
52
+ if configuration.respond_to?(sym)
53
+ configuration.send(sym, *args, &block)
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ # A wrapper for Kernel#system that logs the command being executed.
60
+ def system(*args)
61
+ cmd = args.join(' ')
62
+ result = nil
63
+ logger.trace "executing locally: #{cmd}"
64
+
65
+ elapsed = Benchmark.realtime do
66
+ result = super
67
+ end
68
+
69
+ logger.trace "command finished in #{(elapsed * 1000).round}ms"
70
+ result
71
+ end
72
+
73
+
74
+ private
75
+
76
+ def logger
77
+ @logger ||= configuration.logger || Minestrone::Logger.new(:output => STDOUT)
78
+ end
79
+
80
+ # The revision to deploy. Must return a real revision identifier, and not a pseudo-id.
81
+ def revision
82
+ configuration[:real_revision]
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end