luban 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ffaea169e73bcbe91d121057e872f25c28f9aa91
4
- data.tar.gz: 0b0cfbe6d7b224f87f62ec78a2801f0fe970e491
3
+ metadata.gz: 8f634550b34b8380b8a9bb38189ab450e3c50a34
4
+ data.tar.gz: d507cdf9924ca36905c4e8a8585a7835071e1168
5
5
  SHA512:
6
- metadata.gz: 59807ad43e6829f55c512d1d865402710de337424982863330faaa4a86acb16b36921aa3ec5fe438d55399d431988b75b33abdd461e1de85cce15be94596fcc8
7
- data.tar.gz: c2085acfba4d4476242a85904f009afe9c7cbf4ca149f09260cd1972b2b9be3db5f15602ec588fa1cc6089b6ad4dd8d3f02a2e937d14504ffce9128f6d9a55d7
6
+ metadata.gz: 75e17a9e3a96df84cea850e4313060c5cb2a4ca09c2e4274ff75d09704b6deb55a49f6f1e8656c471e7ed51aa883d3a77b179c3a7bdb39a7d1d42b315ceb948b
7
+ data.tar.gz: 246833cd50c99ef0d7d0d847a2a3aa44efec01ef0832df7868ff89ce06f94a7c829fd240a4252c2c22bc458384567d4f8e4967c1366c5f66630fb3542a161b9e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Change log
2
2
 
3
+ ## Version 0.4.2 (Jun 07, 2016)
4
+
5
+ Minor enhancements:
6
+ * Enhanced process management in controller
7
+ * Added parameters #process_monitor to setup process monitor in Luban
8
+ * Added #process_monitor_via convenient method to setup process monitor in Luban
9
+ * Removed #monitor_process and #unmonitor_process from common control tasks
10
+ * Added option #configure_opts for package installation
11
+ * Optimized format result output message
12
+ * Added convenient methods #env_name and #service_name to standardize naming
13
+ * Minor code refactoring
14
+
15
+ Bug fixes:
16
+ * Fixed SSHKit output format to handle :airbrussh properly
17
+
3
18
  ## Version 0.4.1 (May 13, 2016)
4
19
 
5
20
  Minor enhancements:
@@ -120,7 +120,7 @@ module Luban
120
120
 
121
121
  def set_parameters
122
122
  super
123
- copy_parameters_from_parent(:stage, :project)
123
+ copy_parameters_from_parent(:stage, :project, :process_monitor)
124
124
  @packages = {}
125
125
  @services = {}
126
126
  @source = {}
@@ -29,7 +29,7 @@ module Luban
29
29
 
30
30
  def destroy_app
31
31
  rm(etc_path.join('*', "#{stage}.#{project}.#{application}.*"))
32
- rm(app_path)
32
+ rmdir(app_path)
33
33
  update_result "The application environment is destroyed."
34
34
  end
35
35
 
@@ -66,4 +66,4 @@ module Luban
66
66
  end
67
67
  end
68
68
  end
69
- end
69
+ end
@@ -112,7 +112,7 @@ module Luban
112
112
  create_symlinks
113
113
  update_releases_log
114
114
  else
115
- rm(release_path)
115
+ rmdir(release_path)
116
116
  end
117
117
  ensure
118
118
  rm(upload_to)
@@ -176,7 +176,7 @@ module Luban
176
176
  end
177
177
  releases_to_remove = releases - releases_to_keep
178
178
  releases_to_remove.each do |release|
179
- rm(releases_path.join(release))
179
+ rmdir(releases_path.join(release))
180
180
  end
181
181
  info "Removed #{releases_to_remove.count} old releases."
182
182
  else
@@ -36,7 +36,7 @@ module Luban
36
36
  assure_dirs(release_tag)
37
37
  execute(:tar, "-C #{clone_path} -cf - . | tar -C #{release_tag} -xf -")
38
38
  execute(:tar, "-czf", release_package_path, release_tag)
39
- rm(release_tag)
39
+ rmdir(release_tag)
40
40
  end
41
41
  end
42
42
  end
@@ -91,8 +91,7 @@ module Luban
91
91
 
92
92
  module Control
93
93
  Actions = %i(start_process stop_process kill_process
94
- restart_process check_process
95
- monitor_process unmonitor_process)
94
+ restart_process check_process)
96
95
  Actions.each do |action|
97
96
  define_method(action) do |args:, opts:|
98
97
  raise NotImplementedError, "#{self.class.name}##{__method__} is an abstract method."
@@ -101,6 +100,12 @@ module Luban
101
100
 
102
101
  def controllable?; true; end
103
102
 
103
+ def process_monitor_via(monitor, env: "uber/lubmon")
104
+ monitor = monitor.to_s.downcase
105
+ env = env.to_s.downcase
106
+ process_monitor name: monitor, env: env
107
+ end
108
+
104
109
  protected
105
110
 
106
111
  def setup_control_tasks
@@ -128,16 +133,6 @@ module Luban
128
133
  desc "Check process status"
129
134
  action! :check_process
130
135
  end
131
-
132
- task :monitor do
133
- desc "Turn on process monitor"
134
- action! :monitor_process
135
- end
136
-
137
- task :unmonitor do
138
- desc "Turn off process monitor"
139
- action! :unmonitor_process
140
- end
141
136
  end
142
137
  end
143
138
  end
@@ -325,7 +320,9 @@ module Luban
325
320
  def print_task_result(result)
326
321
  result.each do |entry|
327
322
  next if entry[:message].to_s.empty?
328
- puts " [#{entry[:hostname]}] #{entry[:message]}"
323
+ entry[:message].split("\n").each do |msg|
324
+ puts " [#{entry[:hostname]}] #{msg}"
325
+ end
329
326
  end
330
327
  end
331
328
 
@@ -336,7 +333,7 @@ module Luban
336
333
  enable_dry_run if dry_run
337
334
 
338
335
  SSHKit.configure do |sshkit|
339
- sshkit.format = format
336
+ sshkit.format = format unless format == :airbrussh
340
337
  sshkit.output_verbosity = verbosity
341
338
  sshkit.default_env = default_env
342
339
  sshkit.backend = sshkit_backend
@@ -368,4 +365,4 @@ module Luban
368
365
  end
369
366
  end
370
367
  end
371
- end
368
+ end
@@ -68,7 +68,9 @@ module Luban
68
68
  end
69
69
 
70
70
  def configure_build_options
71
- @configure_opts = default_configure_opts | (task.opts.__remaining__ || [])
71
+ @configure_opts = default_configure_opts
72
+ @configure_opts |= task.opts.configure_opts unless task.opts.configure_opts.nil?
73
+ @configure_opts |= task.opts.__remaining__ unless task.opts.__remaining__.nil?
72
74
  @configure_opts.unshift(install_prefix)
73
75
  end
74
76
 
@@ -283,8 +283,7 @@ module Luban
283
283
  end
284
284
 
285
285
  def remove_symlinks!
286
- return unless symlink?(current_path)
287
- execute "[ -h #{current_path} ] && rm -f #{current_path}; true"
286
+ symlink?(current_path) and rm(current_path)
288
287
  end
289
288
 
290
289
  def remove_binstubs!
@@ -292,7 +291,7 @@ module Luban
292
291
  find_cmd = "find #{current_bin_path}/* -type f -print && find #{current_bin_path}/* -type l -print"
293
292
  capture(find_cmd).split("\n").each do |bin|
294
293
  bin_symlink = app_bin_path.join(File.basename(bin))
295
- execute "[ -h #{bin_symlink} ] && rm -f #{bin_symlink}; true"
294
+ symlink?(bin_symlink) and rm(bin_symlink)
296
295
  end
297
296
  end
298
297
 
@@ -20,7 +20,7 @@ module Luban
20
20
  end
21
21
  end
22
22
 
23
- def package_name; task.opts.name; end
23
+ def package_name; File.basename(task.opts.name); end
24
24
  def package_full_name; "#{package_name}-#{package_version}"; end
25
25
 
26
26
  def package_version; task.opts.version; end
@@ -5,18 +5,24 @@ module Luban
5
5
  include Luban::Deployment::Command::Tasks::Deploy
6
6
  include Luban::Deployment::Command::Tasks::Control
7
7
 
8
- ControlActions = Luban::Deployment::Command::Tasks::Control::Actions
9
- ProfileActions = %i(update_profile)
10
-
11
- (ControlActions | ProfileActions).each do |m|
12
- define_method(m) do |args:, opts:|
8
+ def self.service_action(name, dispatch_to: nil, locally: false, &blk)
9
+ define_method(name) do |args:, opts:|
13
10
  if current_version
14
11
  send("#{__method__}!", args: args, opts: opts.merge(version: current_version))
15
12
  else
16
13
  abort "Aborted! No current version of #{display_name} is specified."
17
14
  end
18
15
  end
16
+ unless dispatch_to.nil?
17
+ dispatch_task "#{name}!", to: dispatch_to, as: name, locally: locally, &blk
18
+ protected "#{name}!"
19
+ end
20
+ end
21
+
22
+ Luban::Deployment::Command::Tasks::Control::Actions.each do |action|
23
+ service_action action, dispatch_to: :controller
19
24
  end
25
+ service_action :update_profile, dispatch_to: :configurator, locally: true
20
26
 
21
27
  protected
22
28
 
@@ -37,12 +43,6 @@ module Luban
37
43
  super
38
44
  linked_dirs.push('log', 'pids')
39
45
  end
40
-
41
- ControlActions.each do |task|
42
- dispatch_task "#{task}!", to: :controller, as: task
43
- end
44
-
45
- dispatch_task :update_profile!, to: :configurator, as: :update_profile, locally: true
46
46
  end
47
47
  end
48
48
  end
@@ -2,11 +2,32 @@ module Luban
2
2
  module Deployment
3
3
  module Service
4
4
  class Controller < Worker
5
- %i(process_stopped? process_started?
6
- monitor_process unmonitor_process).each do |m|
7
- define_method(m) do
8
- raise NotImplementedError, "#{self.class.name}##{__method__} is an abstract method."
9
- end
5
+ def pid
6
+ capture(:cat, "#{pid_file_path} 2>/dev/null")
7
+ end
8
+
9
+ def process_started?
10
+ !!process_grep.keys.first
11
+ end
12
+
13
+ def process_stopped?
14
+ !process_started?
15
+ end
16
+
17
+ def pid_file_orphaned?
18
+ process_stopped? and pid_file_exists?
19
+ end
20
+
21
+ def pid_file_missing?
22
+ process_started? and !pid_file_exists?
23
+ end
24
+
25
+ def pid_file_exists?
26
+ file?(pid_file_path, "-s") # file is NOT zero size
27
+ end
28
+
29
+ def process_pattern
30
+ raise NotImplementedError, "#{self.class.name}##{__method__} is an abstract method."
10
31
  end
11
32
 
12
33
  def start_process
@@ -17,9 +38,11 @@ module Luban
17
38
 
18
39
  output = start_process!
19
40
  if check_until { process_started? }
20
- update_result "Successfully started #{package_full_name}: #{output}"
41
+ monitor_process
42
+ update_result "Start #{package_full_name}: [OK] #{output}"
21
43
  else
22
- update_result "Failed to start #{package_full_name}: #{output}",
44
+ remove_orphaned_pid_file
45
+ update_result "Start #{package_full_name}: [FAILED] #{output}",
23
46
  status: :failed, level: :error
24
47
  end
25
48
  end
@@ -30,11 +53,13 @@ module Luban
30
53
  return
31
54
  end
32
55
 
33
- output = stop_process!
56
+ output = stop_process! || 'OK'
34
57
  if check_until { process_stopped? }
35
- update_result "Successfully stopped #{package_full_name}: #{output}"
58
+ unmonitor_process
59
+ update_result "Stop #{package_full_name}: [OK] #{output}"
36
60
  else
37
- update_result "Failed to stop #{package_full_name}: #{output}",
61
+ remove_orphaned_pid_file
62
+ update_result "Stop #{package_full_name}: [FAILED] #{output}",
38
63
  status: :failed, level: :error
39
64
  end
40
65
  end
@@ -42,8 +67,12 @@ module Luban
42
67
  def restart_process
43
68
  if process_started?
44
69
  output = stop_process!
45
- unless check_until { process_stopped? }
46
- update_result "Failed to stop #{package_full_name}: #{output}",
70
+ if check_until { process_stopped? }
71
+ info "Stop #{package_full_name}: [OK] #{output}"
72
+ unmonitor_process
73
+ else
74
+ remove_orphaned_pid_file
75
+ update_result "Stop #{package_full_name}: [FAILED] #{output}",
47
76
  status: :failed, level: :error
48
77
  return
49
78
  end
@@ -51,9 +80,11 @@ module Luban
51
80
 
52
81
  output = start_process!
53
82
  if check_until { process_started? }
54
- update_result "Successfully restarted #{package_full_name}: #{output}"
83
+ update_result "Restart #{package_full_name}: [OK] #{output}"
84
+ monitor_process
55
85
  else
56
- update_result "Failed to restart #{package_full_name}: #{output}",
86
+ remove_orphaned_pid_file
87
+ update_result "Restart #{package_full_name}: [FAILED] #{output}",
57
88
  status: :failed, level: :error
58
89
  end
59
90
  end
@@ -65,9 +96,43 @@ module Luban
65
96
  def kill_process
66
97
  output = kill_process!
67
98
  if check_until { process_stopped? }
68
- update_result "Successfully kill processs (#{output})."
99
+ unmonitor_process
100
+ remove_orphaned_pid_file
101
+ update_result "Kill #{package_full_name}: [OK] #{output}"
69
102
  else
70
- update_result "Failed to kill process: #{output}"
103
+ update_result "Kill #{package_full_name}: [FAILED] #{output}"
104
+ end
105
+ end
106
+
107
+ def process_monitor_defined?
108
+ !process_monitor[:name].nil?
109
+ end
110
+
111
+ def monitor_process
112
+ if process_monitor_defined?
113
+ if monitor_process!
114
+ info "Turned on process monitor for #{service_entry}"
115
+ else
116
+ info "Failed to turn on process monitor for #{service_entry}"
117
+ end
118
+ end
119
+ end
120
+
121
+ def unmonitor_process
122
+ if process_monitor_defined?
123
+ if unmonitor_process!
124
+ info "Turned off process monitor for #{service_entry}"
125
+ else
126
+ info "Failed to turn off process monitor for #{service_entry}"
127
+ end
128
+ end
129
+ end
130
+
131
+ protected
132
+
133
+ %i(start_process! stop_process!).each do |m|
134
+ define_method(m) do
135
+ raise NotImplementedError, "#{self.class.name}##{__method__} is an abstract method."
71
136
  end
72
137
  end
73
138
 
@@ -80,18 +145,53 @@ module Luban
80
145
  succeeded
81
146
  end
82
147
 
83
- protected
148
+ def check_process!
149
+ if pid_file_missing?
150
+ "#{package_full_name}: started but PID file does NOT exist - #{pid_file_path}"
151
+ elsif process_started?
152
+ "#{package_full_name}: started (PID #{pid})"
153
+ elsif pid_file_orphaned?
154
+ "#{package_full_name}: stopped but PID file exists - #{pid_file_path}"
155
+ else
156
+ "#{package_full_name}: stopped"
157
+ end
158
+ end
84
159
 
85
- %i(start_process! stop_process! check_process!).each do |m|
86
- define_method(m) do
87
- raise NotImplementedError, "#{self.class.name}##{__method__} is an abstract method."
160
+ def kill_process!(pattern = process_pattern)
161
+ capture(:pkill, "-9 -f \"#{pattern}\"")
162
+ end
163
+
164
+ def process_grep(pattern = process_pattern)
165
+ capture(:pgrep, "-l -f \"#{pattern}\" 2>/dev/null").split.inject({}) do |h, p|
166
+ pid, pname = p.split(' ', 2)
167
+ h[pid] = pname
168
+ h
88
169
  end
89
170
  end
90
171
 
91
- def kill_process!
92
- pid = capture(pid_file_path)
93
- output = capture(:kill, "-9 #{pid}")
94
- output.empty? ? pid : output
172
+ def remove_orphaned_pid_file
173
+ rm(pid_file_path) if pid_file_orphaned?
174
+ end
175
+
176
+ def monitor_process!
177
+ test(process_monitor_command)
178
+ end
179
+
180
+ def unmonitor_process!
181
+ test(process_unmonitor_command)
182
+ end
183
+
184
+ def process_monitor_executable
185
+ @process_monitor_executable ||= env_path.join("#{stage}.#{process_monitor[:env]}").
186
+ join('bin').join(process_monitor[:name])
187
+ end
188
+
189
+ def process_monitor_command
190
+ @process_monitor_command ||= "#{process_monitor_executable} monitor #{service_entry}"
191
+ end
192
+
193
+ def process_unmonitor_command
194
+ @process_unmonitor_command ||= "#{process_monitor_executable} unmonitor #{service_entry}"
95
195
  end
96
196
  end
97
197
  end
@@ -5,7 +5,11 @@ module Luban
5
5
  include Luban::Deployment::Worker::Paths::Remote::Service
6
6
 
7
7
  def service_name
8
- @service_name = package_name.downcase
8
+ @service_name ||= package_name.downcase
9
+ end
10
+
11
+ def service_entry
12
+ @service_entry ||= "#{env_name.gsub('/', '.')}.#{service_name}"
9
13
  end
10
14
 
11
15
  def profile_path
@@ -34,4 +38,4 @@ module Luban
34
38
  end
35
39
  end
36
40
  end
37
- end
41
+ end
@@ -14,8 +14,8 @@ module Luban
14
14
  test "[ -d #{path} ]"
15
15
  end
16
16
 
17
- def file?(path)
18
- test "[ -f #{path} ]"
17
+ def file?(path, test_op = "-f")
18
+ test "[ #{test_op} #{path} ]"
19
19
  end
20
20
 
21
21
  def symlink?(path)
@@ -53,9 +53,12 @@ module Luban
53
53
  end
54
54
 
55
55
  def rm(*opts, path)
56
+ execute(:rm, '-f', *opts, path)
57
+ end
58
+
59
+ def rmdir(*opts, path)
56
60
  execute(:rm, '-fr', *opts, path)
57
61
  end
58
- alias_method :rmdir, :rm
59
62
 
60
63
  def chmod(*opts, path)
61
64
  execute(:chmod, '-R', *opts, path)
@@ -101,35 +104,37 @@ module Luban
101
104
  @user_home ||= capture("eval echo ~")
102
105
  end
103
106
 
104
- def url_exists?(download_url)
107
+ def url_exists?(url)
105
108
  # Sent HEAD request to avoid downloading the file contents
106
- test("curl -s -L -I -o /dev/null -f #{download_url}")
109
+ test("curl -s -L -I -o /dev/null -f #{url}")
107
110
 
108
111
  # Other effective ways to check url existence with curl
109
112
 
110
113
  # In case HEAD request is refused,
111
114
  # only the first byte of the file is requested
112
- # test("curl -s -L -o /dev/null -f -r 0-0 #{download_url}")
115
+ # test("curl -s -L -o /dev/null -f -r 0-0 #{url}")
113
116
 
114
117
  # Alternatively, http code (200) can be validated
115
- # capture("curl -s -L -I -o /dev/null -w '%{http_code}' #{download_url}") == '200'
118
+ # capture("curl -s -L -I -o /dev/null -w '%{http_code}' #{url}") == '200'
116
119
  end
117
120
 
118
121
  def upload_by_template(file_to_upload:, template_file:, auto_revision: false, **opts)
119
- template = File.read(template_file)
120
-
121
122
  if auto_revision
122
123
  require 'digest/md5'
123
- revision = Digest::MD5.hexdigest(template)
124
+ revision = Digest::MD5.file(template_file).hexdigest
124
125
  return if revision_match?(file_to_upload, revision)
125
126
  end
126
127
 
127
- require 'erb'
128
- context = opts[:binding] || binding
129
- upload!(StringIO.new(ERB.new(template, nil, '<>').result(context)), file_to_upload)
128
+ upload!(StringIO.new(render_template(template_file, context: binding)), file_to_upload)
130
129
  yield file_to_upload if block_given?
131
130
  end
132
131
 
132
+ def render_template(template_file, context: binding)
133
+ require 'erb'
134
+ template = File.read(template_file)
135
+ ERB.new(template, nil, '<>').result(context)
136
+ end
137
+
133
138
  def revision_match?(file_to_upload, revision)
134
139
  file?(file_to_upload) and match?("grep \"Revision: \" #{file_to_upload}; true", revision)
135
140
  end
@@ -170,6 +175,10 @@ module Luban
170
175
  @hostname ||= host.hostname
171
176
  end
172
177
 
178
+ def now
179
+ Time.now().strftime("%d/%m/%Y %H:%M:%S")
180
+ end
181
+
173
182
  def method_missing(sym, *args, &blk)
174
183
  backend.respond_to?(sym) ? backend.send(sym, *args, &blk) : super
175
184
  end
@@ -64,8 +64,6 @@ module Luban
64
64
  parameter :stage
65
65
 
66
66
  parameter :process_monitor
67
- parameter :process_monitor_env
68
-
69
67
  parameter :sshkit_backend
70
68
  parameter :authen_key_type
71
69
  parameter :default_env
@@ -79,6 +77,7 @@ module Luban
79
77
  protected
80
78
 
81
79
  def set_default_project_parameters
80
+ set_default :process_monitor, {}
82
81
  set_default :sshkit_backend, SSHKit::Backend::Netssh
83
82
  set_default :authen_key_type, 'rsa'
84
83
  set_default :default_env, { path: '$PATH:/usr/local/bin' }
@@ -97,7 +96,16 @@ module Luban
97
96
  Luban::Deployment::Helpers::Configuration::Finder.project(self)
98
97
  end
99
98
 
100
- def validate_project_parameters; end
99
+ def validate_project_parameters
100
+ unless process_monitor.empty?
101
+ if process_monitor[:name].nil?
102
+ abort "Aborted! Please specify the process monitor."
103
+ end
104
+ if process_monitor[:env].nil?
105
+ abort "Aborted! Please specify the process monitor environment."
106
+ end
107
+ end
108
+ end
101
109
  end
102
110
 
103
111
  module Application
@@ -1,7 +1,7 @@
1
1
  # Luban environment activation resource file
2
- # Environment: <%= env_name = "#{stage}.#{project}/#{application}" %>
2
+ # Environment: <%= env_name %>
3
3
  # Revision: <%= revision %>
4
- # Created at <%= Time.now().strftime("%d/%m/%Y %H:%M:%S") %>
4
+ # Created at <%= now %>
5
5
 
6
6
  echo_line() {
7
7
  if [ "$PS1" ]; then
@@ -1,7 +1,7 @@
1
1
  # Environment de-activation resource file
2
- # Environment: <%= env_name = "#{stage}.#{project}/#{application}" %>
2
+ # Environment: <%= env_name %>
3
3
  # Revision: <%= revision %>
4
- # Created at <%= Time.now().strftime("%d/%m/%Y %H:%M:%S") %>
4
+ # Created at <%= now %>
5
5
 
6
6
  echo_line() {
7
7
  if [ "$PS1" ]; then
@@ -1,5 +1,5 @@
1
1
  module Luban
2
2
  module Deployment
3
- VERSION = "0.4.1"
3
+ VERSION = "0.4.2"
4
4
  end
5
5
  end
@@ -25,6 +25,10 @@ module Luban
25
25
  def osx?; os_name == 'Darwin'; end
26
26
  def linux?; os_name == 'Linux'; end
27
27
 
28
+ def env_name
29
+ @env_name ||= "#{stage}.#{project}/#{application}"
30
+ end
31
+
28
32
  def run
29
33
  update_result(__return__: @run_blk ? run_with_block : run_with_command).to_h
30
34
  end
@@ -62,7 +66,9 @@ module Luban
62
66
  r.level = level
63
67
  r.message = message unless message.nil? or !r.message.nil?
64
68
  attrs.each_pair { |k, v| r.send("#{k}=", v) }
65
- send(level, message) unless message.nil? or message.empty?
69
+ unless message.nil? or message.empty?
70
+ message.split("\n").each { |msg| send(level, msg) }
71
+ end
66
72
  end
67
73
  end
68
74
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: luban
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rubyist Lei
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-05-13 00:00:00.000000000 Z
11
+ date: 2016-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: luban-cli