mamiya 0.0.1.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +43 -0
- data/Rakefile +6 -0
- data/bin/mamiya +17 -0
- data/config.example.yml +11 -0
- data/docs/sequences/deploy.png +0 -0
- data/docs/sequences/deploy.uml +58 -0
- data/example.rb +74 -0
- data/lib/mamiya.rb +5 -0
- data/lib/mamiya/agent.rb +181 -0
- data/lib/mamiya/agent/actions.rb +12 -0
- data/lib/mamiya/agent/fetcher.rb +137 -0
- data/lib/mamiya/agent/handlers/abstract.rb +20 -0
- data/lib/mamiya/agent/handlers/fetch.rb +68 -0
- data/lib/mamiya/cli.rb +322 -0
- data/lib/mamiya/cli/client.rb +172 -0
- data/lib/mamiya/config.rb +57 -0
- data/lib/mamiya/dsl.rb +192 -0
- data/lib/mamiya/helpers/git.rb +75 -0
- data/lib/mamiya/logger.rb +190 -0
- data/lib/mamiya/master.rb +118 -0
- data/lib/mamiya/master/agent_monitor.rb +146 -0
- data/lib/mamiya/master/agent_monitor_handlers.rb +44 -0
- data/lib/mamiya/master/web.rb +148 -0
- data/lib/mamiya/package.rb +122 -0
- data/lib/mamiya/script.rb +117 -0
- data/lib/mamiya/steps/abstract.rb +19 -0
- data/lib/mamiya/steps/build.rb +72 -0
- data/lib/mamiya/steps/extract.rb +26 -0
- data/lib/mamiya/steps/fetch.rb +24 -0
- data/lib/mamiya/steps/push.rb +34 -0
- data/lib/mamiya/storages.rb +17 -0
- data/lib/mamiya/storages/abstract.rb +48 -0
- data/lib/mamiya/storages/mock.rb +61 -0
- data/lib/mamiya/storages/s3.rb +127 -0
- data/lib/mamiya/util/label_matcher.rb +38 -0
- data/lib/mamiya/version.rb +3 -0
- data/mamiya.gemspec +35 -0
- data/misc/logger_test.rb +12 -0
- data/spec/agent/actions_spec.rb +37 -0
- data/spec/agent/fetcher_spec.rb +199 -0
- data/spec/agent/handlers/fetch_spec.rb +121 -0
- data/spec/agent_spec.rb +255 -0
- data/spec/config_spec.rb +50 -0
- data/spec/dsl_spec.rb +291 -0
- data/spec/fixtures/dsl_test_load.rb +1 -0
- data/spec/fixtures/dsl_test_use.rb +1 -0
- data/spec/fixtures/helpers/foo.rb +1 -0
- data/spec/fixtures/test-package-source/.mamiya.meta.json +1 -0
- data/spec/fixtures/test-package-source/greeting +1 -0
- data/spec/fixtures/test-package.tar.gz +0 -0
- data/spec/fixtures/test.yml +4 -0
- data/spec/logger_spec.rb +68 -0
- data/spec/master/agent_monitor_spec.rb +269 -0
- data/spec/master/web_spec.rb +121 -0
- data/spec/master_spec.rb +94 -0
- data/spec/package_spec.rb +394 -0
- data/spec/script_spec.rb +78 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/steps/build_spec.rb +261 -0
- data/spec/steps/extract_spec.rb +68 -0
- data/spec/steps/fetch_spec.rb +96 -0
- data/spec/steps/push_spec.rb +73 -0
- data/spec/storages/abstract_spec.rb +22 -0
- data/spec/storages/s3_spec.rb +342 -0
- data/spec/storages_spec.rb +33 -0
- data/spec/support/dummy_serf.rb +70 -0
- data/spec/util/label_matcher_spec.rb +85 -0
- metadata +272 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'mamiya/cli'
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
require 'uri'
|
6
|
+
require 'json'
|
7
|
+
require 'thor'
|
8
|
+
|
9
|
+
module Mamiya
|
10
|
+
class CLI < Thor
|
11
|
+
class Client < Thor
|
12
|
+
class_option :master, aliases: '-u', type: :string
|
13
|
+
class_option :application, aliases: %w(-a --app), type: :string
|
14
|
+
|
15
|
+
desc "list-applications", "list applications"
|
16
|
+
def list_applications
|
17
|
+
puts master_get('/packages')["applications"]
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "list-packages", "list-packages"
|
21
|
+
def list_packages
|
22
|
+
puts master_get("/packages/#{application}")["packages"]
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "show-package", "show package meta data"
|
26
|
+
method_option :format, aliases: %w(-f), type: :string, default: 'pp'
|
27
|
+
def show_package(package)
|
28
|
+
meta = master_get("/packages/#{application}/#{package}")
|
29
|
+
|
30
|
+
case options[:format]
|
31
|
+
when 'pp'
|
32
|
+
require 'pp'
|
33
|
+
pp meta
|
34
|
+
when 'json'
|
35
|
+
require 'json'
|
36
|
+
puts meta.to_json
|
37
|
+
when 'yaml'
|
38
|
+
require 'yaml'
|
39
|
+
puts meta.to_yaml
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "list-agents", 'list agents'
|
44
|
+
def list_agents
|
45
|
+
payload = master_get("/agents")
|
46
|
+
|
47
|
+
agents = payload["agents"].keys
|
48
|
+
|
49
|
+
agents.each do |agent|
|
50
|
+
puts "#{agent}\talive"
|
51
|
+
end
|
52
|
+
payload["failed_agents"].each do |agent|
|
53
|
+
puts "#{agent}\tfailed"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "show-agent AGENT", 'Show agent'
|
58
|
+
method_option :format, aliases: %w(-f), type: :string, default: 'pp'
|
59
|
+
def show_agent(agent)
|
60
|
+
agent = master_get("/agents/#{agent}")
|
61
|
+
|
62
|
+
case options[:format]
|
63
|
+
when 'pp'
|
64
|
+
require 'pp'
|
65
|
+
pp agent
|
66
|
+
when 'json'
|
67
|
+
require 'json'
|
68
|
+
puts agent.to_json
|
69
|
+
when 'yaml'
|
70
|
+
require 'yaml'
|
71
|
+
puts agent.to_yaml
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "show-distribution package", "Show package distribution status"
|
76
|
+
method_option :format, aliases: %w(-f), type: :string, default: 'text'
|
77
|
+
method_option :verbose, aliases: %w(-v), type: :boolean
|
78
|
+
def show_distribution(package)
|
79
|
+
dist = master_get("/packages/#{application}/#{package}/distribution")
|
80
|
+
|
81
|
+
case options[:format]
|
82
|
+
when 'json'
|
83
|
+
require 'json'
|
84
|
+
puts dist.to_json
|
85
|
+
return
|
86
|
+
|
87
|
+
when 'yaml'
|
88
|
+
require 'yaml'
|
89
|
+
puts dist.to_yaml
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
total = dist['distributed_count'] + dist['not_distributed_count']
|
94
|
+
puts <<-EOF
|
95
|
+
Distribution status of #{application}/#{package}
|
96
|
+
|
97
|
+
ratio (distributed:total-agents): #{"%.1f" % ((dist['distributed_count']/total.to_f)*100)}%
|
98
|
+
number of agents have package: #{dist['distributed_count']}
|
99
|
+
number of agents don't have package: #{dist['not_distributed_count']}
|
100
|
+
EOF
|
101
|
+
|
102
|
+
if options[:verbose]
|
103
|
+
puts ""
|
104
|
+
dist['not_distributed'].each do |name|
|
105
|
+
puts "#{name}\tnot_distributed"
|
106
|
+
end
|
107
|
+
dist['distributed'].each do |name|
|
108
|
+
puts "#{name}\tdistributed"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
desc "distribute package", "order distributing package to agents"
|
114
|
+
def distribute(package)
|
115
|
+
p master_post("/packages/#{application}/#{package}/distribute")
|
116
|
+
end
|
117
|
+
|
118
|
+
desc "refresh", "order refreshing agent status"
|
119
|
+
def refresh
|
120
|
+
p master_post('/agents/refresh')
|
121
|
+
end
|
122
|
+
|
123
|
+
desc "deploy PACKAGE", "Run distribute->prepare->finalize"
|
124
|
+
def deploy
|
125
|
+
end
|
126
|
+
|
127
|
+
desc "rollback", "Switch back to previous release then finalize"
|
128
|
+
def rollback
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def fatal!(msg)
|
134
|
+
$stderr.puts msg
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
|
138
|
+
def application
|
139
|
+
options[:application] or fatal!('specify application')
|
140
|
+
end
|
141
|
+
|
142
|
+
def master_get(path)
|
143
|
+
master_http.start do |http|
|
144
|
+
JSON.parse http.get(path).tap(&:value).body
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def master_post(path, data='')
|
149
|
+
master_http.start do |http|
|
150
|
+
response = http.post(path, data).tap(&:value)
|
151
|
+
response.code == '204' ? true : JSON.parse(response.tap(&:value).body)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def master_http
|
156
|
+
url = master_url
|
157
|
+
Net::HTTP.new(url.host, url.port).tap do |http|
|
158
|
+
http.use_ssl = true if url.scheme == 'https'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def master_url
|
163
|
+
url = ENV["MAMIYA_MASTER_URL"] || options[:master]
|
164
|
+
fatal! 'specify master URL via --master(-u) option or $MAMIYA_MASTER_URL' unless url
|
165
|
+
URI.parse(url)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
desc "client", "client for master"
|
170
|
+
subcommand "client", Client
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'mamiya/storages'
|
3
|
+
|
4
|
+
module Mamiya
|
5
|
+
class Config
|
6
|
+
def self.load(file)
|
7
|
+
self.new YAML.load_file(file)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(config_hash = {})
|
11
|
+
@config = symbolize_keys_in(config_hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
@config[key]
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(key, value)
|
19
|
+
@config[key] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def storage_class
|
23
|
+
self[:storage] && Storages.find(self[:storage][:type])
|
24
|
+
end
|
25
|
+
|
26
|
+
def deploy_to_for_app(app)
|
27
|
+
# TODO: test
|
28
|
+
app = app.to_sym
|
29
|
+
|
30
|
+
if self[:apps] && self[:applications][app]
|
31
|
+
Pathname.new(self[:applications][app][:deploy_to])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def releases_path_for_app(app)
|
36
|
+
# TODO: test
|
37
|
+
deploy_to_for_app(app).join('releases')
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def symbolize_keys_in(hash)
|
43
|
+
Hash[hash.map { |k, v|
|
44
|
+
case v
|
45
|
+
when Hash
|
46
|
+
v = symbolize_keys_in(v)
|
47
|
+
when Array
|
48
|
+
if v.find { |_| _.kind_of?(Hash) }
|
49
|
+
v = v.map { |_| _.kind_of?(Hash) ? symbolize_keys_in(_) : _ }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
[k.to_sym, v]
|
54
|
+
}]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/mamiya/dsl.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'mamiya/util/label_matcher'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Mamiya
|
5
|
+
class DSL
|
6
|
+
class TaskNotDefinedError < Exception; end
|
7
|
+
class HelperNotFound < Exception; end
|
8
|
+
|
9
|
+
##
|
10
|
+
# Creates new DSL environment.
|
11
|
+
def initialize
|
12
|
+
@variables = {}
|
13
|
+
@tasks = {}
|
14
|
+
@hooks = {}
|
15
|
+
@eval_lock = Mutex.new
|
16
|
+
@use_lock = Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :hooks, :tasks
|
20
|
+
|
21
|
+
##
|
22
|
+
# Returns Hash of default setting variables.
|
23
|
+
def self.defaults
|
24
|
+
@defaults ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.define_variable_accessor(name) # :nodoc:
|
28
|
+
k = name.to_sym
|
29
|
+
return if self.instance_methods.include?(k)
|
30
|
+
|
31
|
+
define_method(k) { @variables[k] || self.class.defaults[k] }
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Sets default value +value+ for variable name +key+.
|
36
|
+
# Values set by this method will available for all instances of same class.
|
37
|
+
def self.set_default(key, value)
|
38
|
+
k = key.to_sym
|
39
|
+
defaults[k] = value
|
40
|
+
self.define_variable_accessor(k)
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Add hook point with name +name+.
|
45
|
+
# This defines method with same name in class to call and define hooks.
|
46
|
+
def self.add_hook(name, attributes={})
|
47
|
+
define_method(name) do |*args, &block|
|
48
|
+
@hooks[name] ||= []
|
49
|
+
|
50
|
+
if block
|
51
|
+
hook_name = args.shift if args.first.kind_of?(String)
|
52
|
+
options = args.pop if args.last.kind_of?(Hash)
|
53
|
+
|
54
|
+
hook = {block: block, options: options || {}, name: hook_name}
|
55
|
+
case args.first
|
56
|
+
when :overwrite
|
57
|
+
@hooks[name] = [hook]
|
58
|
+
when :prepend
|
59
|
+
@hooks[name][0,0] = [hook]
|
60
|
+
else
|
61
|
+
@hooks[name] << hook
|
62
|
+
end
|
63
|
+
|
64
|
+
else
|
65
|
+
matcher = Mamiya::Util::LabelMatcher::Simple.new(args)
|
66
|
+
Proc.new { |*args|
|
67
|
+
filtered_hooks = @hooks[name].reject { |hook|
|
68
|
+
options = hook[:options]
|
69
|
+
|
70
|
+
(options[:only] && !matcher.match?(*options[:only] )) ||
|
71
|
+
(options[:except] && matcher.match?(*options[:except]))
|
72
|
+
}
|
73
|
+
|
74
|
+
if attributes[:chain]
|
75
|
+
init = args.shift
|
76
|
+
filtered_hooks.inject(init) do |result, hook|
|
77
|
+
hook[:block].call(result, *args)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
filtered_hooks.each do |hook|
|
81
|
+
hook[:block].call *args
|
82
|
+
end
|
83
|
+
end
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# :call-seq:
|
91
|
+
# evaluate!(string [, filename [, lineno]])
|
92
|
+
# evaluate! { block }
|
93
|
+
#
|
94
|
+
# Evaluates given string or block in DSL environment.
|
95
|
+
def evaluate!(str = nil, filename = nil, lineno = nil, &block)
|
96
|
+
@eval_lock.synchronize {
|
97
|
+
begin
|
98
|
+
if block_given?
|
99
|
+
self.instance_eval(&block)
|
100
|
+
elsif str
|
101
|
+
@file = filename if filename
|
102
|
+
|
103
|
+
if str && filename && lineno
|
104
|
+
self.instance_eval(str, filename, lineno)
|
105
|
+
elsif str && filename
|
106
|
+
self.instance_eval(str, filename)
|
107
|
+
elsif str
|
108
|
+
self.instance_eval(str)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
ensure
|
112
|
+
@file = nil
|
113
|
+
end
|
114
|
+
}
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Evaluates specified file +file+ in DSL environment.
|
120
|
+
def load!(file)
|
121
|
+
evaluate! File.read(file), file, 1
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# (DSL) Find file using +name+ from current +load_path+ then load.
|
126
|
+
# +options+ will be available as variable +options+ in loaded file.
|
127
|
+
def use(name, options={})
|
128
|
+
helper_file = find_helper_file(name)
|
129
|
+
raise HelperNotFound unless helper_file
|
130
|
+
|
131
|
+
@use_lock.lock unless @use_lock.owned? # to avoid lock recursively
|
132
|
+
|
133
|
+
@_options = options
|
134
|
+
self.instance_eval File.read(helper_file).prepend("options = @_options; @_options = nil;\n"), helper_file, 1
|
135
|
+
|
136
|
+
ensure
|
137
|
+
@_options = nil
|
138
|
+
@use_lock.unlock if @use_lock.owned?
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# (DSL) Set value +value+ for variable named +key+.
|
143
|
+
def set(key, value)
|
144
|
+
k = key.to_sym
|
145
|
+
self.class.define_variable_accessor(key) unless self.methods.include?(k)
|
146
|
+
@variables[k] = value
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# (DSL) Set value +value+ for variable named +key+ unless value is present for the variable.
|
151
|
+
def set_default(key, value)
|
152
|
+
k = key.to_sym
|
153
|
+
return @variables[k] if @variables.key?(k)
|
154
|
+
set(k, value)
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# (DSL) Define task named +name+ with given block.
|
159
|
+
def task(name, &block)
|
160
|
+
@tasks[name] = block
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# (DSL) Invoke task named +name+.
|
165
|
+
def invoke(name)
|
166
|
+
raise TaskNotDefinedError unless @tasks[name]
|
167
|
+
self.instance_eval &@tasks[name]
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Returns current load path used by +use+ method.
|
172
|
+
def load_path
|
173
|
+
(@variables[:load_path] ||= []) +
|
174
|
+
[
|
175
|
+
"#{__dir__}/helpers",
|
176
|
+
*(@file ? ["#{File.dirname(@file)}/helpers"] : [])
|
177
|
+
]
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def find_helper_file(name) # :nodoc:
|
183
|
+
load_path.find do |_| # Using find to return nil when not found
|
184
|
+
path = File.join(_, "#{name}.rb")
|
185
|
+
break path if File.exists?(path)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# TODO: hook call context methods
|
190
|
+
#https://gist.github.com/sorah/9263951
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
set_default :git_remote, 'origin'
|
4
|
+
set_default :commit, "#{self.git_remote}/HEAD"
|
5
|
+
|
6
|
+
def git_ignored_files
|
7
|
+
git_clean_out = `git clean -ndx`.lines
|
8
|
+
prefix = /^Would (?:remove|skip repository) /
|
9
|
+
|
10
|
+
if git_clean_out.any? { |_| prefix !~ _ }
|
11
|
+
puts git_clean_out
|
12
|
+
raise "`git clean -ndx` doesn't return line starting with 'Would remove' or 'Would skip'"
|
13
|
+
end
|
14
|
+
|
15
|
+
excludes = git_clean_out.grep(prefix).map{ |_| _.sub(prefix, '').chomp }
|
16
|
+
if package_under
|
17
|
+
excludes.grep(/^#{Regexp.escape(package_under)}/).map{ |_| _.sub(/^#{Regexp.escape(package_under)}\/?/, '') }
|
18
|
+
else
|
19
|
+
excludes
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def git_head
|
24
|
+
git_show = `git show --pretty=fuller -s`
|
25
|
+
commit, comment = git_show.split(/\n\n/, 2)
|
26
|
+
|
27
|
+
{
|
28
|
+
commit: commit.match(/^commit (.+)$/)[1],
|
29
|
+
author: commit.match(/^Author:\s*(?<name>.+?) <(?<email>.+?)>$/).
|
30
|
+
tap {|match| break Hash[match.names.map {|name| [name.to_sym, match[name]] }] },
|
31
|
+
author_date: Time.parse(commit.match(/^AuthorDate:\s*(.+)$/)[1]),
|
32
|
+
committer: commit.match(/^Commit:\s*(?<name>.+?) <(?<email>.+?)>$/).
|
33
|
+
tap {|match| break Hash[match.names.map {|name| [name.to_sym, match[name]] }] },
|
34
|
+
commit_date: Time.parse(commit.match(/^CommitDate:\s*(.+)$/)[1]),
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
prepare_build do |update|
|
39
|
+
logger = self.logger['git']
|
40
|
+
|
41
|
+
if !update && !self.repository
|
42
|
+
logger.warn 'Skipping cloning repository because script.repository not set'
|
43
|
+
elsif !update
|
44
|
+
run "git", "clone", self.repository, self.build_from
|
45
|
+
end
|
46
|
+
|
47
|
+
Dir.chdir(self.build_from) do
|
48
|
+
logger.info Dir.pwd
|
49
|
+
run "git", "fetch", self.git_remote
|
50
|
+
run "git", "remote", "prune", self.git_remote, allow_failure: true
|
51
|
+
run "git", "fetch", "--tags", self.git_remote
|
52
|
+
|
53
|
+
run "git", "reset", "--hard", self.commit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if options[:exclude_git_clean_targets]
|
58
|
+
build(:prepend) do
|
59
|
+
set :exclude_from_package, exclude_from_package + git_ignored_files()
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
options[:add_commit_hash_to_package_name] = true unless options.key?(:add_commit_hash_to_package_name)
|
64
|
+
if options[:add_commit_hash_to_package_name]
|
65
|
+
package_name do |candidate|
|
66
|
+
candidate + [git_head[:commit]]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
options[:include_head_commit_to_meta] = true unless options.key?(:include_head_commit_to_meta)
|
71
|
+
if options[:include_head_commit_to_meta]
|
72
|
+
package_meta do |candidate|
|
73
|
+
candidate.merge(git: git_head())
|
74
|
+
end
|
75
|
+
end
|