hell 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +5 -2
- data/README.markdown +0 -1
- data/VERSION +1 -1
- data/bin/hell +28 -10
- data/config.ru +7 -0
- data/hell.gemspec +107 -0
- data/lib/hell.rb +4 -0
- data/lib/hell/app.rb +108 -32
- data/lib/hell/lib/capistrano/cli.rb +57 -0
- data/lib/hell/lib/capistrano/configuration.rb +14 -0
- data/lib/hell/lib/capistrano/task_definition.rb +18 -0
- data/lib/hell/lib/cli.rb +167 -0
- data/lib/hell/lib/helpers.rb +32 -13
- data/lib/hell/lib/monkey_patch.rb +4 -70
- data/lib/hell/public/assets/js/bootstrap.growl.js +85 -1
- data/lib/hell/public/assets/js/bootstrap.js +7 -2025
- data/lib/hell/public/assets/js/hashchange.js +300 -0
- data/lib/hell/public/assets/js/hell.js +63 -21
- data/lib/hell/public/assets/js/jquery.js +9555 -0
- data/lib/hell/public/assets/js/{underscore.min.js → underscore.js} +1 -1
- data/lib/hell/views/index.erb +14 -25
- data/unicorn +2 -0
- metadata +63 -12
- data/lib/hell/config.ru +0 -4
- data/lib/hell/hell.rb +0 -0
- data/lib/hell/public/assets/js/backbone.min.js +0 -38
- data/lib/hell/public/assets/js/bootstrap.min.js +0 -6
- data/lib/hell/public/assets/js/hashchange.min.js +0 -9
- data/lib/hell/public/assets/js/jquery.min.js +0 -4
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -64,7 +64,6 @@ Note that the current response is subject to change, and as such is not document
|
|
64
64
|
The following environment variables are available for your use:
|
65
65
|
|
66
66
|
- `HELL_APP_ROOT`: Path from which capistrano should execute. Defaults to `Dir.pwd`.
|
67
|
-
- `HELL_ENVIRONMENTS`: Comma-separated list of environments, Default: `production,staging`.
|
68
67
|
- `HELL_REQUIRE_ENV`: Whether or not to require specifying an environment. Default: `1`.
|
69
68
|
- `HELL_LOG_PATH`: Path to which logs should be written to. Defaults to `Dir.pwd + '/log'`.
|
70
69
|
- `HELL_BASE_DIR`: Base directory to use in web ui. Useful for subdirectories. Defaults to `/`.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
data/bin/hell
CHANGED
@@ -1,12 +1,30 @@
|
|
1
|
-
#!/
|
2
|
-
#
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
#!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
|
2
|
+
# -*- encoding: binary -*-
|
3
|
+
require 'unicorn/launcher'
|
4
|
+
require 'hell/lib/cli'
|
5
|
+
|
6
|
+
ENV["RACK_ENV"] ||= "development"
|
7
|
+
|
8
|
+
unicorn_path = File.expand_path('../../unicorn', __FILE__)
|
9
|
+
options, op = Hell::CLI.option_parser(ARGV, unicorn_path)
|
10
|
+
|
11
|
+
unless File.directory? options[:log_path]
|
12
|
+
abort('Missing hell log path %s' % options[:log_path])
|
13
|
+
end
|
14
|
+
|
15
|
+
options.delete(:log_path)
|
16
|
+
|
17
|
+
app = Unicorn.builder(File.expand_path('../../config.ru', __FILE__), op)
|
18
|
+
op = nil
|
19
|
+
|
20
|
+
if $DEBUG
|
21
|
+
require 'pp'
|
22
|
+
pp({
|
23
|
+
:unicorn_options => options,
|
24
|
+
:app => app,
|
25
|
+
:daemonize => options[:daemonize],
|
26
|
+
})
|
10
27
|
end
|
11
28
|
|
12
|
-
|
29
|
+
Unicorn::Launcher.daemonize!(options) if options[:daemonize]
|
30
|
+
Unicorn::HttpServer.new(app, options).start.join
|
data/config.ru
ADDED
data/hell.gemspec
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "hell"
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jose Diaz-Gonzalez"]
|
12
|
+
s.date = "2013-02-21"
|
13
|
+
s.description = "Hell is an open source web interface that exposes a set of capistrano recipes as a json api, for usage within large teams"
|
14
|
+
s.email = "jose@seatgeek.com"
|
15
|
+
s.executables = ["hell"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE.txt",
|
18
|
+
"README.markdown"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".document",
|
22
|
+
"Gemfile",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.markdown",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"bin/hell",
|
28
|
+
"config.ru",
|
29
|
+
"hell.gemspec",
|
30
|
+
"lib/hell.rb",
|
31
|
+
"lib/hell/app.rb",
|
32
|
+
"lib/hell/lib/capistrano/cli.rb",
|
33
|
+
"lib/hell/lib/capistrano/configuration.rb",
|
34
|
+
"lib/hell/lib/capistrano/task_definition.rb",
|
35
|
+
"lib/hell/lib/cli.rb",
|
36
|
+
"lib/hell/lib/helpers.rb",
|
37
|
+
"lib/hell/lib/monkey_patch.rb",
|
38
|
+
"lib/hell/public/assets/css/bootstrap-responsive.css",
|
39
|
+
"lib/hell/public/assets/css/bootstrap-responsive.min.css",
|
40
|
+
"lib/hell/public/assets/css/bootstrap.css",
|
41
|
+
"lib/hell/public/assets/css/bootstrap.min.css",
|
42
|
+
"lib/hell/public/assets/css/hell.css",
|
43
|
+
"lib/hell/public/assets/ico/favicon.ico",
|
44
|
+
"lib/hell/public/assets/ico/favicon.png",
|
45
|
+
"lib/hell/public/assets/img/glyphicons-halflings-white.png",
|
46
|
+
"lib/hell/public/assets/img/glyphicons-halflings.png",
|
47
|
+
"lib/hell/public/assets/js/backbone-localstorage.js",
|
48
|
+
"lib/hell/public/assets/js/backbone.js",
|
49
|
+
"lib/hell/public/assets/js/bankersbox.js",
|
50
|
+
"lib/hell/public/assets/js/bootstrap.growl.js",
|
51
|
+
"lib/hell/public/assets/js/bootstrap.js",
|
52
|
+
"lib/hell/public/assets/js/hashchange.js",
|
53
|
+
"lib/hell/public/assets/js/hell.js",
|
54
|
+
"lib/hell/public/assets/js/jquery.js",
|
55
|
+
"lib/hell/public/assets/js/timeago.js",
|
56
|
+
"lib/hell/public/assets/js/underscore.js",
|
57
|
+
"lib/hell/views/index.erb",
|
58
|
+
"test/helper.rb",
|
59
|
+
"test/test_hell.rb",
|
60
|
+
"unicorn"
|
61
|
+
]
|
62
|
+
s.homepage = "http://github.com/seatgeek/hell"
|
63
|
+
s.licenses = ["MIT"]
|
64
|
+
s.require_paths = ["lib"]
|
65
|
+
s.rubygems_version = "1.8.23"
|
66
|
+
s.summary = "A web interface and api wrapper around Capistrano"
|
67
|
+
|
68
|
+
if s.respond_to? :specification_version then
|
69
|
+
s.specification_version = 3
|
70
|
+
|
71
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
72
|
+
s.add_runtime_dependency(%q<capistrano>, [">= 0"])
|
73
|
+
s.add_runtime_dependency(%q<multi_json>, [">= 0"])
|
74
|
+
s.add_runtime_dependency(%q<pusher>, [">= 0"])
|
75
|
+
s.add_runtime_dependency(%q<sinatra>, [">= 0"])
|
76
|
+
s.add_runtime_dependency(%q<sinatra-contrib>, [">= 0"])
|
77
|
+
s.add_runtime_dependency(%q<sinatra-assetpack>, [">= 0"])
|
78
|
+
s.add_runtime_dependency(%q<sass>, [">= 0"])
|
79
|
+
s.add_runtime_dependency(%q<thin>, [">= 0"])
|
80
|
+
s.add_runtime_dependency(%q<unicorn>, [">= 0"])
|
81
|
+
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
82
|
+
else
|
83
|
+
s.add_dependency(%q<capistrano>, [">= 0"])
|
84
|
+
s.add_dependency(%q<multi_json>, [">= 0"])
|
85
|
+
s.add_dependency(%q<pusher>, [">= 0"])
|
86
|
+
s.add_dependency(%q<sinatra>, [">= 0"])
|
87
|
+
s.add_dependency(%q<sinatra-contrib>, [">= 0"])
|
88
|
+
s.add_dependency(%q<sinatra-assetpack>, [">= 0"])
|
89
|
+
s.add_dependency(%q<sass>, [">= 0"])
|
90
|
+
s.add_dependency(%q<thin>, [">= 0"])
|
91
|
+
s.add_dependency(%q<unicorn>, [">= 0"])
|
92
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
93
|
+
end
|
94
|
+
else
|
95
|
+
s.add_dependency(%q<capistrano>, [">= 0"])
|
96
|
+
s.add_dependency(%q<multi_json>, [">= 0"])
|
97
|
+
s.add_dependency(%q<pusher>, [">= 0"])
|
98
|
+
s.add_dependency(%q<sinatra>, [">= 0"])
|
99
|
+
s.add_dependency(%q<sinatra-contrib>, [">= 0"])
|
100
|
+
s.add_dependency(%q<sinatra-assetpack>, [">= 0"])
|
101
|
+
s.add_dependency(%q<sass>, [">= 0"])
|
102
|
+
s.add_dependency(%q<thin>, [">= 0"])
|
103
|
+
s.add_dependency(%q<unicorn>, [">= 0"])
|
104
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
data/lib/hell.rb
CHANGED
data/lib/hell/app.rb
CHANGED
@@ -4,23 +4,36 @@ require 'sinatra'
|
|
4
4
|
require 'sinatra/base'
|
5
5
|
require 'sinatra/json'
|
6
6
|
require 'sinatra/streaming'
|
7
|
+
require 'sinatra/assetpack'
|
7
8
|
|
8
|
-
require '
|
9
|
+
require 'pusher'
|
10
|
+
require 'multi_json'
|
9
11
|
require 'securerandom'
|
10
|
-
require 'websocket'
|
11
|
-
|
12
|
-
HELL_DIR = Dir.pwd
|
13
|
-
APP_ROOT = ENV.fetch('HELL_APP_ROOT', HELL_DIR)
|
14
|
-
ENVIRONMENTS = ENV.fetch('HELL_ENVIRONMENTS', 'production,staging').split(',')
|
15
|
-
BLACKLIST = ['invoke', 'shell', 'internal:ensure_env', 'internal:setup_env']
|
16
|
-
REQUIRE_ENV = ENV.fetch('HELL_REQUIRE_ENV', '1') == '1'
|
17
|
-
HELL_LOG_PATH = ENV.fetch('HELL_LOG_PATH', File.join(HELL_DIR, 'log'))
|
18
|
-
HELL_BASE_DIR = ENV.fetch('HELL_BASE_DIR', '/')
|
19
|
-
SENTINEL_STRINGS = ENV.fetch('HELL_SENTINEL_STRINGS', 'Hellish Task Completed').split(',')
|
20
12
|
|
13
|
+
require 'hell/lib/cli'
|
21
14
|
require 'hell/lib/monkey_patch'
|
22
15
|
require 'hell/lib/helpers'
|
23
16
|
|
17
|
+
options, op = Hell::CLI.option_parser(ARGV, nil, true)
|
18
|
+
op = nil
|
19
|
+
|
20
|
+
HELL_DIR = Dir.pwd
|
21
|
+
HELL_APP_ROOT = options[:app_root]
|
22
|
+
HELL_BLACKLIST = ['invoke', 'shell', 'internal:ensure_env', 'internal:setup_env']
|
23
|
+
HELL_REQUIRE_ENV = !!options[:require_env]
|
24
|
+
HELL_LOG_PATH = options[:log_path]
|
25
|
+
HELL_BASE_PATH = options[:base_path]
|
26
|
+
HELL_SENTINEL_STRINGS = options[:sentinel]
|
27
|
+
|
28
|
+
USE_PUSHER = !!(options[:pusher_app_id] && options[:pusher_key] && options[:pusher_secret])
|
29
|
+
|
30
|
+
PUSHER_APP_ID = options[:pusher_app_id]
|
31
|
+
PUSHER_KEY = options[:pusher_key]
|
32
|
+
PUSHER_SECRET = options[:pusher_secret]
|
33
|
+
|
34
|
+
Pusher.app_id = PUSHER_APP_ID
|
35
|
+
Pusher.key = PUSHER_KEY
|
36
|
+
Pusher.secret = PUSHER_SECRET
|
24
37
|
|
25
38
|
module Hell
|
26
39
|
class App < Sinatra::Base
|
@@ -28,7 +41,7 @@ module Hell
|
|
28
41
|
helpers Sinatra::Streaming
|
29
42
|
helpers Hell::Helpers
|
30
43
|
|
31
|
-
|
44
|
+
register Sinatra::AssetPack
|
32
45
|
|
33
46
|
set :public_folder, File.join(File.expand_path('..', __FILE__), 'public')
|
34
47
|
set :root, HELL_DIR
|
@@ -36,38 +49,67 @@ module Hell
|
|
36
49
|
set :static, true
|
37
50
|
set :views, File.join(File.expand_path('..', __FILE__), 'views')
|
38
51
|
|
52
|
+
assets do
|
53
|
+
css_compression :sass
|
54
|
+
|
55
|
+
js :jquery, [
|
56
|
+
'/assets/js/jquery.js',
|
57
|
+
]
|
58
|
+
|
59
|
+
js :main, [
|
60
|
+
'/assets/js/backbone-localstorage.js',
|
61
|
+
'/assets/js/bootstrap.growl.js',
|
62
|
+
'/assets/js/timeago.js',
|
63
|
+
'/assets/js/hashchange.js',
|
64
|
+
'/assets/js/hell.js',
|
65
|
+
]
|
66
|
+
|
67
|
+
css :bootstrap, [
|
68
|
+
'/assets/css/bootstrap.min.css',
|
69
|
+
'/assets/css/bootstrap-responsive.css',
|
70
|
+
'/assets/css/hell.css',
|
71
|
+
]
|
72
|
+
|
73
|
+
prebuild true
|
74
|
+
end
|
75
|
+
|
39
76
|
configure :production, :development do
|
40
77
|
enable :logging
|
41
78
|
end
|
42
79
|
|
43
80
|
get '/' do
|
44
|
-
@tasks = cap.
|
45
|
-
@require_env =
|
46
|
-
@www_base_dir =
|
47
|
-
@environments =
|
81
|
+
@tasks = cap.tasks.keys
|
82
|
+
@require_env = HELL_REQUIRE_ENV
|
83
|
+
@www_base_dir = HELL_BASE_PATH
|
84
|
+
@environments = cap.environments
|
85
|
+
|
86
|
+
@use_pusher = USE_PUSHER
|
87
|
+
@pusher_app_id = PUSHER_APP_ID
|
88
|
+
@pusher_key = PUSHER_KEY
|
89
|
+
@pusher_secret = PUSHER_SECRET
|
48
90
|
erb :index
|
49
91
|
end
|
50
92
|
|
51
93
|
get '/tasks' do
|
52
|
-
tasks = cap.
|
94
|
+
tasks = cap.tasks
|
53
95
|
json tasks
|
54
96
|
end
|
55
97
|
|
56
98
|
get '/tasks/search/:pattern' do
|
57
|
-
tasks = cap.
|
99
|
+
tasks = cap.tasks(params[:pattern])
|
58
100
|
json tasks
|
59
101
|
end
|
60
102
|
|
61
103
|
get '/tasks/:name/exists' do
|
62
|
-
tasks = cap.
|
104
|
+
tasks = cap.tasks(params[:name], {:exact => true})
|
63
105
|
response = { :exists => !tasks.empty?, :task => params[:name]}
|
64
106
|
json response
|
65
107
|
end
|
66
108
|
|
67
|
-
|
109
|
+
put '/tasks/:name/background' do
|
68
110
|
tasks, original_cmd = verify_task(cap, params[:name])
|
69
111
|
verbose = ""
|
70
|
-
verbose = "LOGGING=debug" if params[:verbose] ==
|
112
|
+
verbose = "LOGGING=debug" if params[:verbose] == "on"
|
71
113
|
|
72
114
|
task_id = run_in_background!("bundle exec cap -l STDOUT %s %s" % [original_cmd, verbose]) unless tasks.empty?
|
73
115
|
response = {}
|
@@ -80,9 +122,9 @@ module Hell
|
|
80
122
|
get '/logs/:id/tail' do
|
81
123
|
content_type "text/event-stream"
|
82
124
|
if valid_log params[:id]
|
83
|
-
|
125
|
+
send_success(params[:id], "tail -f %s" % File.join(HELL_LOG_PATH, params[:id] + ".log"))
|
84
126
|
else
|
85
|
-
|
127
|
+
send_error(params[:id], "log file '#{params[:id]}' not found")
|
86
128
|
end
|
87
129
|
end
|
88
130
|
|
@@ -94,35 +136,69 @@ module Hell
|
|
94
136
|
|
95
137
|
get '/tasks/:name/execute' do
|
96
138
|
tasks, original_cmd = verify_task(cap, params[:name])
|
139
|
+
task_id = random_id
|
97
140
|
content_type "text/event-stream"
|
98
141
|
if tasks.empty?
|
99
|
-
|
142
|
+
send_error(task_id, "cap task '#{original_cmd}' not found")
|
100
143
|
else
|
101
|
-
|
144
|
+
send_success(task_id, "bundle exec cap -l STDOUT #{original_cmd} LOGGING=debug 2>&1", {:prepend => true})
|
102
145
|
end
|
103
146
|
end
|
104
147
|
|
105
|
-
def
|
148
|
+
def self.init_send
|
149
|
+
if USE_PUSHER
|
150
|
+
alias_method :send_error, :_pusher_error
|
151
|
+
alias_method :send_success, :_pusher_success
|
152
|
+
else
|
153
|
+
alias_method :send_error, :_stream_error
|
154
|
+
alias_method :send_success, :_stream_success
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def cap
|
159
|
+
FileUtils.chdir HELL_APP_ROOT do
|
160
|
+
@cap ||= Capistrano::CLI.parse(["-T"])
|
161
|
+
end
|
162
|
+
return @cap
|
163
|
+
end
|
164
|
+
|
165
|
+
def _pusher_error(task_id, message)
|
166
|
+
Pusher[task_id].trigger('start', {:data => ''})
|
167
|
+
Pusher[task_id].trigger('message', ws_message("<p>#{message}</p>"))
|
168
|
+
Pusher[task_id].trigger('end', {:data => ''})
|
169
|
+
end
|
170
|
+
|
171
|
+
def _pusher_success(task_id, command, opts = {})
|
172
|
+
opts = {:prepend => false}.merge(opts)
|
173
|
+
Pusher[task_id].trigger('start', {:data => ''})
|
174
|
+
Pusher[task_id].trigger('message', ws_message("<p>#{command}</p>")) unless opts[:prepend] == false
|
175
|
+
IO.popen(command, 'rb') do |io|
|
176
|
+
io.each {|line| push_line(task_id, line, out, io)}
|
177
|
+
end
|
178
|
+
Pusher[task_id].trigger('end', {:data => ''})
|
179
|
+
end
|
180
|
+
|
181
|
+
def _stream_error(task_id, message)
|
106
182
|
stream do |out|
|
107
183
|
out << "event: start\ndata:\n\n" unless out.closed?
|
108
|
-
out << "data: " + ws_message("<p>#{message}</p>") unless out.closed?
|
184
|
+
out << "data: " + ws_message("<p>#{message}</p>") + "\n\n" unless out.closed?
|
109
185
|
out << "event: end\ndata:\n\n" unless out.closed?
|
110
186
|
out.close
|
111
187
|
end
|
112
188
|
end
|
113
189
|
|
114
|
-
def _stream_success(command, opts = {})
|
190
|
+
def _stream_success(task_id, command, opts = {})
|
115
191
|
opts = {:prepend => false}.merge(opts)
|
116
192
|
stream do |out|
|
117
193
|
out << "event: start\ndata:\n\n" unless out.closed?
|
118
|
-
out << "data: " + ws_message("<p>#{command}</p>") unless out.closed? or opts[:prepend] == false
|
194
|
+
out << "data: " + ws_message("<p>#{command}</p>") + "\n\n" unless out.closed? or opts[:prepend] == false
|
119
195
|
IO.popen(command, 'rb') do |io|
|
120
|
-
io.each
|
121
|
-
process_line(line, out, io)
|
122
|
-
end
|
196
|
+
io.each {|line| stream_line(task_id, line, out, io)}
|
123
197
|
end
|
124
198
|
close_stream(out)
|
125
199
|
end
|
126
200
|
end
|
201
|
+
|
202
|
+
init_send
|
127
203
|
end
|
128
204
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'capistrano/cli'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Capistrano
|
5
|
+
class CLI
|
6
|
+
module JsonTaskList
|
7
|
+
|
8
|
+
def task_list
|
9
|
+
config = instantiate_configuration(options)
|
10
|
+
config.debug = options[:debug]
|
11
|
+
config.dry_run = options[:dry_run]
|
12
|
+
config.preserve_roles = options[:preserve_roles]
|
13
|
+
config.logger.level = options[:verbose]
|
14
|
+
|
15
|
+
set_pre_vars(config)
|
16
|
+
load_recipes(config)
|
17
|
+
|
18
|
+
config.trigger(:load)
|
19
|
+
[config, options]
|
20
|
+
|
21
|
+
tasks = config.task_list(:all)
|
22
|
+
tasks.reject! {|t| HELL_BLACKLIST.include?(t.fully_qualified_name)}
|
23
|
+
tasks = Hash[tasks.map {|task| [task.fully_qualified_name, task.to_hash]}]
|
24
|
+
|
25
|
+
config.trigger(:exit)
|
26
|
+
|
27
|
+
tasks
|
28
|
+
end
|
29
|
+
|
30
|
+
def tasks(pattern = nil, opts = {})
|
31
|
+
@available_tasks ||= task_list
|
32
|
+
tasks = @available_tasks.reject {|name, task| is_environment?(task)}
|
33
|
+
|
34
|
+
if pattern.is_a?(String)
|
35
|
+
if opts.fetch(:exact, false)
|
36
|
+
tasks.select! {|name| name == pattern}
|
37
|
+
else
|
38
|
+
tasks.select! {|name| name =~ /#{pattern}/}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
tasks
|
43
|
+
end
|
44
|
+
|
45
|
+
def environments
|
46
|
+
@available_tasks ||= task_list
|
47
|
+
environments = @available_tasks.select {|name, task| is_environment?(task)}
|
48
|
+
environments.keys
|
49
|
+
end
|
50
|
+
|
51
|
+
def is_environment?(task)
|
52
|
+
return false unless task[:options].include?(:hell_tags)
|
53
|
+
Array(task[:options][:hell_tags]).include?(:environment)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|