a4tools 1.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/install.log +38 -0
  3. data/.gitignore +2 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +38 -0
  6. data/a4tools.gemspec +38 -0
  7. data/bin/deploy_latest_clients +32 -0
  8. data/bin/devsite_config_server +48 -0
  9. data/bin/netshell +23 -0
  10. data/bin/update_server +101 -0
  11. data/bin/usher +54 -0
  12. data/lib/a4tools.rb +61 -0
  13. data/lib/a4tools/version.rb +3 -0
  14. data/lib/acres_client.rb +376 -0
  15. data/lib/clients/caching_client.rb +151 -0
  16. data/lib/clients/deployment_client.rb +53 -0
  17. data/lib/clients/kai_config_client.rb +39 -0
  18. data/lib/clients/usher_client.rb +72 -0
  19. data/lib/clients/usher_mgmt_client.rb +201 -0
  20. data/lib/event_manager.rb +24 -0
  21. data/lib/events.json +1 -0
  22. data/lib/net_shell/builtin_command.rb +312 -0
  23. data/lib/net_shell/builtin_commands/build.rb +251 -0
  24. data/lib/net_shell/builtin_commands/cd.rb +12 -0
  25. data/lib/net_shell/builtin_commands/connect.rb +122 -0
  26. data/lib/net_shell/builtin_commands/deploy.rb +280 -0
  27. data/lib/net_shell/builtin_commands/disconnect.rb +15 -0
  28. data/lib/net_shell/builtin_commands/excerpt.rb +97 -0
  29. data/lib/net_shell/builtin_commands/exit.rb +7 -0
  30. data/lib/net_shell/builtin_commands/get.rb +38 -0
  31. data/lib/net_shell/builtin_commands/help.rb +40 -0
  32. data/lib/net_shell/builtin_commands/host.rb +126 -0
  33. data/lib/net_shell/builtin_commands/inject.rb +42 -0
  34. data/lib/net_shell/builtin_commands/jsoncache.rb +80 -0
  35. data/lib/net_shell/builtin_commands/kai_event.rb +151 -0
  36. data/lib/net_shell/builtin_commands/persist.rb +24 -0
  37. data/lib/net_shell/builtin_commands/pwd.rb +6 -0
  38. data/lib/net_shell/builtin_commands/recap.rb +188 -0
  39. data/lib/net_shell/builtin_commands/references.rb +63 -0
  40. data/lib/net_shell/builtin_commands/select.rb +36 -0
  41. data/lib/net_shell/builtin_commands/send.rb +74 -0
  42. data/lib/net_shell/builtin_commands/set.rb +29 -0
  43. data/lib/net_shell/builtin_commands/show.rb +183 -0
  44. data/lib/net_shell/builtin_commands/site.rb +122 -0
  45. data/lib/net_shell/builtin_commands/ssh.rb +62 -0
  46. data/lib/net_shell/builtin_commands/talk.rb +90 -0
  47. data/lib/net_shell/builtin_commands/translate.rb +45 -0
  48. data/lib/net_shell/builtin_commands/unset.rb +14 -0
  49. data/lib/net_shell/builtin_commands/usher.rb +55 -0
  50. data/lib/net_shell/builtin_commands/usher_device.rb +39 -0
  51. data/lib/net_shell/builtin_commands/usher_site.rb +245 -0
  52. data/lib/net_shell/builtin_commands/usherm_connect.rb +21 -0
  53. data/lib/net_shell/colors.rb +149 -0
  54. data/lib/net_shell/command.rb +97 -0
  55. data/lib/net_shell/io.rb +132 -0
  56. data/lib/net_shell/net_shell.rb +396 -0
  57. data/lib/net_shell/prompt.rb +335 -0
  58. data/lib/object_builder/definitions/app_info_for_script.rb +83 -0
  59. data/lib/object_builder/definitions/connection_request.rb +28 -0
  60. data/lib/object_builder/definitions/device_info_for_system.rb +37 -0
  61. data/lib/object_builder/object_builder.rb +145 -0
  62. data/lib/talk.json +1 -0
  63. data/lib/talk_consumer.rb +235 -0
  64. metadata +279 -0
@@ -0,0 +1,251 @@
1
+ category "Protocol manipulation"
2
+ description "Interactively build a JSON object of a given class"
3
+ usage "class_name"
4
+ help <<-EOS
5
+ Launches an interactive prompt to build the indicated Talk class. At the conclusion of this prompt, a JSON string will be printed to stdout.
6
+
7
+ This command is somewhat magical in that the interactive prompts do not appear on the command's standard output, nor does it gather prompt input from standard input; only the JSON appears on standard output.
8
+
9
+ Therefore, you can pipe the output of #{command.style(:command)} directly to #{"send".style(:command)} or some other command.
10
+ EOS
11
+
12
+
13
+ validate { args.length == 2 }
14
+ tab 0 do
15
+ talk.things_named_like(args[1], :class).map { |thing| talk.truncated_name thing[:name] }
16
+ end
17
+
18
+ run do
19
+ cls = talk.class_named(args[1])
20
+ return show_error("#{args[1]}: no such class defined") if cls.nil?
21
+
22
+ obj = build(cls)
23
+ obj[:__class] = talk.class_named(args[1])[:name] rescue args[1]
24
+ JSON.generate(obj)
25
+ end
26
+
27
+ ###
28
+
29
+ def prompt_for_path(path)
30
+ path.join(".").style(:class) + ") "
31
+ end
32
+
33
+ def prompt_for_value(path, type)
34
+ "#{path.join(".").style(:class)} #{type.style(:field_datatype)}) "
35
+ end
36
+
37
+ def get_action(allowed)
38
+ char = STDIN.gets.chomp rescue nil
39
+ char = nil unless char.nil? or allowed.include? char
40
+ char
41
+ end
42
+
43
+ def get_value_real(value)
44
+ case value
45
+ when nil, "null", ""
46
+ nil
47
+ else
48
+ value.to_f
49
+ end
50
+ end
51
+
52
+ def get_value_integer(value)
53
+ case value
54
+ when nil, "null", ""
55
+ nil
56
+ else
57
+ value.to_i
58
+ end
59
+ end
60
+
61
+ def get_value_boolean(value)
62
+ case value
63
+ when "0", "false"
64
+ false
65
+ when "1", "true"
66
+ true
67
+ when "", "null"
68
+ nil
69
+ else
70
+ true
71
+ end
72
+ end
73
+
74
+ def get_value_string(value)
75
+ case value
76
+ when nil, "null"
77
+ nil
78
+ else
79
+ value
80
+ end
81
+ end
82
+
83
+ def get_value(field, path)
84
+ type = field[:type].first
85
+ console_print prompt_for_value(path, type)
86
+
87
+ value = STDIN.gets
88
+ if value.nil? then
89
+ value = "null" # nil => CTRL+D. Take advantage that all get_value_* interpret null as nil-value to make CTRL+D -> null shortcut.
90
+ console_puts # need a newline since none was actually typed
91
+ end
92
+
93
+ value = value.chomp
94
+ if(value == "?") then
95
+ show_field_help(field)
96
+ return get_value(field, path)
97
+ end
98
+
99
+ return get_value_integer(value) if([ "uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64" ].include?(type))
100
+ return get_value_real(value) if([ "real" ].include?(type))
101
+ return get_value_boolean(value) if([ "bool" ].include?(type))
102
+ return get_value_string(value) if([ "string" ].include?(type))
103
+ end
104
+
105
+ def in_array?(field, depth)
106
+ field[:type][-1 - depth] == "[]"
107
+ end
108
+
109
+ def in_hash?(field, depth)
110
+ field[:type][-1 - depth] == "{}"
111
+ end
112
+
113
+ def in_container?(field, depth)
114
+ in_array?(field, depth) or in_hash?(field, depth)
115
+ end
116
+
117
+ def prompt_for_action(field, path, type, actions)
118
+ while true do
119
+ console_puts "#{path.join('.').style(:class)}: #{type.style(:field_datatype)}. #{actions.map { |a| "[#{a[0].bold}]#{a[1..-1]}" }.join(', ')}"
120
+ console_print prompt_for_path(path)
121
+ action = get_action(actions.map { |a| a[0] })
122
+ return (actions.select { |a| a[0] == action }).first unless action.nil?
123
+ end
124
+ end
125
+
126
+ def type_at_depth(field, depth)
127
+ field[:type][0 .. -1 - depth].join("")
128
+ end
129
+
130
+ def show_field_help(field, depth=-1)
131
+ depth = field[:type].length - 1 if depth < 0
132
+ console_puts "#{talk.truncated_name(type_at_depth(field, depth)).style(:field_datatype)} #{field[:name].style(:field_name)}\n\t#{field[:description]}"
133
+ end
134
+
135
+ def handle_array_start(field, path, depth)
136
+ actions = [ "build", "null", "?" ]
137
+
138
+ case prompt_for_action(field, path, type_at_depth(field, depth), actions)
139
+ when "?"
140
+ show_field_help(field, depth)
141
+ return handle_array_start(field, path, depth)
142
+ when nil, "null"
143
+ return nil
144
+ when "build"
145
+ collection = []
146
+ building = true
147
+ index = 0
148
+ while true do
149
+ element = handle_array_element(field, path[0..-2] + ["#{path.last}[#{index}]"], depth+1)
150
+ return collection if element.nil?
151
+ collection.push element[:value]
152
+ index += 1
153
+ end
154
+ end
155
+ end
156
+
157
+ def handle_array_element(field, path, depth)
158
+ actions = [ "build", "null", "done", "?" ]
159
+ case prompt_for_action(field, path, type_at_depth(field, depth), actions)
160
+ when "?"
161
+ show_field_help(field, depth)
162
+ return handle_array_element(field, path, depth)
163
+ when nil, "null"
164
+ return { value:nil }
165
+ when "done"
166
+ return nil
167
+ when "build"
168
+ return { value:handle_field(field, path, depth+1, true) }
169
+ end
170
+ end
171
+
172
+ def populate(field, path)
173
+ while true do
174
+ idx = 0
175
+ builders = ObjectBuilder.builders_for_name(field[:type].first)
176
+ builders.each do |builder|
177
+ console_puts("#{idx.to_s.style(:constant_value)} #{builder.identifier.to_s.style(:method_name)}: #{builder.description}")
178
+ idx += 1
179
+ end
180
+
181
+ console_print prompt_for_path(path)
182
+ action = STDIN.gets.chomp rescue nil
183
+
184
+ return handle_complex(field, path) if action.nil? # CTRL+D cancels populate
185
+ index = action.match(/^[0-9]+$/) ? action.to_i : nil
186
+ return builders[index].value if not index.nil? and index < builders.length
187
+ end
188
+ end
189
+
190
+ def has_cache?(field)
191
+ ObjectBuilder.supported_classes(false).each do |name|
192
+ return true if talk.name_matches?(name, field[:type].first)
193
+ end
194
+ false
195
+ end
196
+
197
+ def handle_complex(field, path, assume_build=false)
198
+ return build(talk.class_named(field[:type].first), path) if assume_build
199
+
200
+ actions = if has_cache?(field) then
201
+ [ "build", "populate", "null", "?" ]
202
+ else
203
+ [ "build", "null", "?" ]
204
+ end
205
+
206
+ case prompt_for_action(field, path, field[:type].first, actions)
207
+ when "build"
208
+ build(talk.class_named(field[:type].first), path)
209
+ when nil, "null"
210
+ nil
211
+ when "populate"
212
+ populate(field, path)
213
+ when "?"
214
+ show_field_help(field, field[:type].length-1)
215
+ return handle_complex(field, path)
216
+ end
217
+ end
218
+
219
+ def handle_primitive(field, path)
220
+ get_value(field, path)
221
+ end
222
+
223
+ def handle_field(field, path, depth=0, assume_build=false)
224
+ path += [field[:name]] if depth == 0
225
+ if depth+1 < field[:type].length then
226
+ if in_array?(field, depth)
227
+ return handle_array_start(field, path, depth)
228
+ else
229
+ # TODO: handle dictionary
230
+ return nil
231
+ end
232
+ end
233
+
234
+ if talk.primitive? field[:type].first
235
+ return handle_primitive(field, path)
236
+ else
237
+ return handle_complex(field, path, assume_build)
238
+ end
239
+ end
240
+
241
+ def build(cls, path=[])
242
+ if path.empty? then
243
+ console_puts "Building #{talk.truncated_name cls}..."
244
+ else
245
+ console_puts "#{talk.truncated_name cls} #{path.last}\n\t#{cls[:description]}"
246
+ end
247
+
248
+ result = {}
249
+ cls[:field].each { |field| result[field[:name]] = handle_field(field, path) }
250
+ result
251
+ end
@@ -0,0 +1,12 @@
1
+ category "Shell management"
2
+ description "Change working directory"
3
+ usage "directory"
4
+ validate { args.length <= 2 }
5
+ help "Behaves much like cd in your favorite shell."
6
+
7
+ run do
8
+ p = File.expand_path(args.length == 2 ? args[1] : "~")
9
+ return show_error("Directory does not exist") unless File.directory?(p)
10
+ Dir.chdir(p)
11
+ ""
12
+ end
@@ -0,0 +1,122 @@
1
+ category "Protocol manipulation"
2
+ description "Connect to a remote server"
3
+ usage "url [client_name]"
4
+
5
+ help <<-EOS
6
+ Connect to a remote server, indicated by the URL.
7
+ Supported schemes: http, https, ws, wss.
8
+
9
+ Once connected, commands like #{"send".style(:command)} will use the connected server as a target. Connecting also resets the #{"recap".style(:command)} log.
10
+
11
+ Comet support is not included; therefore, http and https provide synchronous communication only. ws and wss support fully asynchronous communication.
12
+
13
+ #{command.style(:command)} does make special effort to validate that a URL is accessible. For http and https clients, this means that attempts to use this connection will fail. Due to the nature of websockets, #{command.style(:command)} will display an error on exit if these services are unavailable.
14
+ EOS
15
+
16
+ validate { args.length >= 2 }
17
+
18
+ opt :kai, "Connect the Kai Mercury, Central and QATest clients to a particular site ID or host"
19
+
20
+ tab 0 do
21
+ if params[:kai] then
22
+ (usherm[:usherSystemId][:usherSites].map { |site| site[:idSite] } +
23
+ usherm[:usherSystemId][:usherSites].map { |site| site[:name] }) rescue []
24
+ else
25
+ []
26
+ end
27
+ end
28
+
29
+ run do
30
+ if params[:kai] then
31
+ connect_kai
32
+ else
33
+ connect_raw
34
+ end
35
+ end
36
+
37
+ ###
38
+
39
+ def connect_raw
40
+ key = args[2].to_sym rescue :user
41
+ clients = []
42
+ client = AcresClient.new(args[1])
43
+ return show_error("Unable to contact server at #{args[1]}") unless client.transport_connect
44
+
45
+ @shell.set_client(key, client)
46
+ client.on(:connect) { |trigger, msg| @shell.notify_connect(client) }
47
+ client.on(:sent) { |trigger,msg| @shell.notify_outgoing_message(client, msg) }
48
+ client.on(:message) { |trigger,msg| @shell.notify_incoming_message(client, msg) }
49
+ client.on(:disconnect) { |trigger,msg| @shell.notify_disconnect(client) }
50
+
51
+ @shell.set_active_client(key)
52
+ ""
53
+ end
54
+
55
+ def connect_kai
56
+ dest = args[1]
57
+
58
+ if dest.match(/^([a-zA-Z]+):\/\/[a-zA-Z0-9\-\.]+(:[0-9]+)?(\/)?/) then
59
+ connect_to_url(dest)
60
+ elsif dest.match(/^[0-9]+$/) then
61
+ connect_to_site_id(dest.to_i)
62
+ else
63
+ connect_to_usher_host(dest)
64
+ end
65
+ end
66
+
67
+ def request_caches
68
+ version = @shell.client(:config).site_version
69
+ client_interfaces.keys.each { |key| @shell.client(key).version = version }
70
+ @shell.client(:config).machines
71
+ end
72
+
73
+ def connect_to_url(base)
74
+ client_interfaces.each do |key, uri|
75
+ url = URI.join(base, uri).to_s
76
+ @shell.set_client(key, client_for_key(key, url))
77
+ end
78
+
79
+ request_caches
80
+ "Connected to #{@shell.client(:config).version.style(:host)} server"
81
+ end
82
+
83
+ def connect_to_site_id(site_id)
84
+ site = usherm.site_with_id(site_id)
85
+ return show_error("Site ##{site_id}: No such site ID in usher") if site.nil?
86
+ provider = usherm.provider_for_product(site_id, "Mercury")
87
+ return show_error("Site ##{site_id}: Does not offer Mercury") if provider.nil?
88
+ url = usherm.url_for_provider(provider)
89
+ return show_error("Site ##{site_id}: Site appears to be down") if url.nil?
90
+ puts "Connecting to Kai at #{url}"
91
+ connect_to_url(url)
92
+ end
93
+
94
+ def connect_to_usher_host(host)
95
+ site = usherm.site_with_name(host)
96
+ return show_error("Site #{host}: Does not appear to be listed in usher") if site.nil?
97
+ connect_to_site_id(site[:idSite])
98
+ end
99
+
100
+ def client_for_key(key, url)
101
+ case key
102
+ when :mercury
103
+ AcresClient.new(url, @shell.get_env(:kai_user), @shell.get_env(:kai_pass))
104
+ when :central
105
+ AcresClient.new(url, @shell.get_env(:usherm_user), @shell.get_env(:usherm_pass))
106
+ when :config
107
+ KaiConfigClient.new(url, @shell.get_env(:usherm_user), @shell.get_env(:usherm_pass))
108
+ when :qa
109
+ AcresClient.new(url, @shell.get_env(:kai_user), @shell.get_env(:kai_pass))
110
+ else
111
+ AcresClient.new(url, @shell.get_env(:usherm_user), @shell.get_env(:usherm_pass))
112
+ end
113
+ end
114
+
115
+ def client_interfaces
116
+ {
117
+ mercury:"/mercury/client?wrap",
118
+ central:"/central/central?wrap",
119
+ config:"/config/config?wrap",
120
+ qa:"/mercury/qatest?wrap"
121
+ }
122
+ end
@@ -0,0 +1,280 @@
1
+ require 'date'
2
+
3
+ category "Deployment"
4
+ description "Manage deployments"
5
+ usage [
6
+ "list -- list active publications",
7
+ "builds -- list builds",
8
+ "publish -- publish a build"
9
+ ]
10
+
11
+ opt :url, "Deployment server URL"
12
+
13
+ opt :app, "List builds for application", :type => :string
14
+ opt :jeb, "List builds for jeb (i.e. three-word name)", :type => :string
15
+ opt :branch, "List builds for branch", :type => :string
16
+ opt :commit, "List builds for commit hash", :type => :string
17
+ opt :yes, "Automatically include all apps in publication. No effect if --app is supplied."
18
+
19
+ opt :site, "When used with list, only list publications to given site. When used with publish, indicates site to deploy to.", :type => :integer
20
+
21
+
22
+ validate do
23
+ return false if args.length <= 1
24
+
25
+ case args[1]
26
+ when "builds"
27
+ args.length == 2
28
+ when "list"
29
+ true
30
+ when "publish"
31
+ return "Must supply at least one site ID" unless args.length >= 3
32
+ true
33
+ else
34
+ "#{args[1]}: unknown subcommand"
35
+ end
36
+ end
37
+
38
+ tab(:app) { all_apps }
39
+ tab(:jeb) { all_jebs }
40
+ tab(:branch) { all_branches }
41
+ tab(:commit) { all_commits }
42
+
43
+ tab 0 do
44
+ [:list, :builds, :publish]
45
+ end
46
+
47
+ run do
48
+ usherm.ensure_fresh
49
+ deployment.ensure_fresh
50
+ case args[1]
51
+ when "builds"
52
+ builds
53
+ when "list"
54
+ list
55
+ when "publish"
56
+ publish
57
+ end
58
+ end
59
+
60
+ ###
61
+ # Tab completors
62
+
63
+ def all_apps
64
+ all_of_key(:appId)
65
+ end
66
+
67
+ def all_branches
68
+ all_of_key(:branches)
69
+ end
70
+
71
+ def all_commits
72
+ all_of_key(:commitId)
73
+ end
74
+
75
+ def all_jebs
76
+ all_of_key(:buildId)
77
+ end
78
+
79
+ def all_of_key(key)
80
+ instances = {}
81
+
82
+ deployment[:buildList][:builds].each do |build|
83
+ if build[key].is_a? Array then
84
+ build[key].each { |item| instances[item] = true }
85
+ else
86
+ instances[build[key]] = true
87
+ end
88
+ end
89
+
90
+ instances.keys
91
+ end
92
+
93
+ ###
94
+ # List
95
+
96
+ def list
97
+ sites = if args.length >= 3 then
98
+ args[2..-1].map { |arg| arg.to_i }
99
+ else
100
+ (usherm[:usherSystemId][:usherSites].map { |site| site[:idSite] }).sort
101
+ end
102
+
103
+ sites.each do |site_id|
104
+ list_site(site_id, deployment.query(:buildPublicationList, site_id))
105
+ end
106
+ ""
107
+ end
108
+
109
+ def list_site(site_id, publications)
110
+ site = usherm.site_with_id(site_id) || {name:"Unknown site", siteDescription:"Not in usher"}
111
+ lines = []
112
+
113
+ lines.push "Site #{site_id} -- #{site[:name]} (#{site[:siteDescription]})"
114
+ publications[:orders].keys.sort.each do |app|
115
+ next if params[:app] and not talk.name_matches?(app, params[:app])
116
+ lines.push list_site_app(app.to_s, publications[:orders][app])
117
+ end
118
+
119
+ puts lines.join("\n")+"\n" unless lines.length == 1
120
+ end
121
+
122
+ def list_site_app(app, orders)
123
+ pub = orders.first
124
+ return if params[:jeb] and pub[:buildInfo][:buildId] != params[:jeb]
125
+ return if params[:commit] and pub[:buildInfo][:commitId] != params[:commit]
126
+ return if params[:branch] and not(pub[:buildInfo][:branches].include? params[:branch])
127
+ "\t#{timestamp(pub[:publicationTime])} #{app.style(:class)} -- #{build_info(pub[:buildInfo])}"
128
+ end
129
+
130
+ ###
131
+ # Builds
132
+
133
+ def builds
134
+ show_build_set(build_set)
135
+ ""
136
+ end
137
+
138
+ def build_set
139
+ if params[:jeb] then
140
+ builds_for_jeb(params[:jeb])
141
+ elsif params[:commit] then
142
+ builds_for_commit(params[:commit])
143
+ elsif params[:branch] then
144
+ builds_for_branch(params[:branch])
145
+ elsif params[:app] then
146
+ builds_for_app(params[:app])
147
+ else
148
+ deployment[:buildList][:builds]
149
+ end
150
+ end
151
+
152
+ def builds_for_app(app)
153
+ deployment[:buildList][:builds].select { |build| talk.name_matches?(build[:appId], app) }
154
+ end
155
+
156
+ def builds_for_commit(commit)
157
+ set =
158
+ if params[:app] then
159
+ builds_for_app(params[:app])
160
+ else
161
+ deployment[:buildList][:builds]
162
+ end
163
+
164
+ set.select { |build| build[:commitId].start_with? commit }
165
+ end
166
+
167
+ def builds_for_jeb(jeb)
168
+ set =
169
+ if params[:app] then
170
+ builds_for_app(params[:app])
171
+ else
172
+ deployment[:buildList][:builds]
173
+ end
174
+
175
+ set.select { |build| build[:buildId] == jeb }
176
+ end
177
+
178
+ def builds_for_branch(branch)
179
+ set =
180
+ if params[:app] then
181
+ builds_for_app(params[:app])
182
+ else
183
+ deployment[:buildList][:builds]
184
+ end
185
+
186
+ set.select { |build| build[:branches].include? branch }
187
+ end
188
+
189
+ def build_set_by_app(builds)
190
+ apps = {}
191
+ builds.each do |build|
192
+ apps[build[:appId]] ||= []
193
+ apps[build[:appId]].push build
194
+ end
195
+ apps
196
+ end
197
+
198
+ ###
199
+ # Publish
200
+
201
+ def prompt_for_apps(set)
202
+ apps = build_set_by_app(set)
203
+ return apps.keys.sort.map { |app| apps[app].last } if params[:yes]
204
+
205
+ selected = []
206
+ apps.keys.sort.each do |app|
207
+ build = apps[app].last
208
+ cls = sprintf("%s", app).style(:class)
209
+ name = sprintf("%s", build[:buildId]).style(:build_name)
210
+ branch = build[:branches].join(",").style(:branch)
211
+ line = sprintf("%99s", "#{cls}: #{name} [#{branch}]")
212
+
213
+ while true do
214
+ print "#{line} [y/N]> "
215
+ input = STDIN.gets.chomp.upcase rescue nil
216
+ input = "N" if not input.nil? and input.empty?
217
+
218
+ selected.push build if input == "Y"
219
+ puts if input.nil?
220
+ break if ["Y", "N"].include? input
221
+ end
222
+ end
223
+
224
+ selected
225
+ end
226
+
227
+ def publish
228
+ sites = args[2..-1].map { |arg| arg.to_i }
229
+ selected = prompt_for_apps(build_set)
230
+ selected.each do |build|
231
+ puts "Publishing #{build[:appId].style(:class)} \"#{build[:buildId].style(:build_name)}\" to site #{sites.join(",")}..."
232
+ deployment.publish_build(build, sites)
233
+ end
234
+ "Done."
235
+ end
236
+
237
+ ###
238
+ # Other stuff
239
+ def show_app(app, limit=50)
240
+ list = deployment.index_list_by_app[app]
241
+ return show_error("#{app}: No such app in deployment server") if list.nil?
242
+
243
+ list.each do |build|
244
+ puts show_build(build)
245
+ end
246
+
247
+ ""
248
+ end
249
+
250
+ def timestamp(ts)
251
+ DateTime.strptime(ts.to_s,'%s').strftime("%Y-%m-%d %H:%M:%S").style(:timestamp)
252
+ end
253
+
254
+ def build_id(build)
255
+ sprintf("%4d", build[:buildRecordId]).style(:identifier)
256
+ end
257
+
258
+ def branches(build)
259
+ build[:branches].join(",").style(:branch)
260
+ end
261
+
262
+ def author(build)
263
+ build[:author].style(:author) rescue ""
264
+ end
265
+
266
+ def build_info(build)
267
+ name = sprintf("%36s", build[:buildId]).style(:build_name)
268
+ "#{name} [#{branches(build)}] -- #{author(build)}"
269
+ end
270
+
271
+ def show_app(app, builds)
272
+ puts "#{app.style(:class)}: #{builds.length} builds"
273
+ builds.each { |build| puts timestamp(build[:postDate]) + " " + build_info(build) }
274
+ puts
275
+ end
276
+
277
+ def show_build_set(set)
278
+ apps = build_set_by_app(set)
279
+ apps.keys.sort.each { |app| show_app(app, apps[app]) }
280
+ end