travis 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,6 +16,7 @@ module Travis
16
16
  module CLI
17
17
  autoload :Token, 'travis/cli/token'
18
18
  autoload :ApiCommand, 'travis/cli/api_command'
19
+ autoload :Accounts, 'travis/cli/accounts'
19
20
  autoload :Branches, 'travis/cli/branches'
20
21
  autoload :Command, 'travis/cli/command'
21
22
  autoload :Console, 'travis/cli/console'
@@ -28,6 +29,7 @@ module Travis
28
29
  autoload :Init, 'travis/cli/init'
29
30
  autoload :Login, 'travis/cli/login'
30
31
  autoload :Logs, 'travis/cli/logs'
32
+ autoload :Monitor, 'travis/cli/monitor'
31
33
  autoload :Open, 'travis/cli/open'
32
34
  autoload :Parser, 'travis/cli/parser'
33
35
  autoload :Pubkey, 'travis/cli/pubkey'
@@ -0,0 +1,21 @@
1
+ require 'travis/cli'
2
+
3
+ module Travis
4
+ module CLI
5
+ class Accounts < ApiCommand
6
+ def run
7
+ authenticate
8
+ accounts.each do |account|
9
+ color = account.subscribed? ? :green : :info
10
+ say [
11
+ color(account.login, [color, :bold]),
12
+ color("(#{account.name}):", color),
13
+ account.subscribed? ? "subscribed," : "not subscribed,",
14
+ account.repos_count == 1 ? "1 repository" : "#{account.repos_count} repositories"
15
+ ].join(" ")
16
+ end
17
+ say session.config['host'], "To set up a subscription, please visit %s." unless accounts.all?(&:subscribed?)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -79,6 +79,13 @@ module Travis
79
79
 
80
80
  private
81
81
 
82
+ def listen(*args)
83
+ super(*args) do |listener|
84
+ on_signal { listener.disconnect }
85
+ yield listener
86
+ end
87
+ end
88
+
82
89
  def detected_endpoint
83
90
  Travis::Client::ORG_URI
84
91
  end
@@ -58,6 +58,7 @@ module Travis
58
58
  attr_reader :input, :output
59
59
 
60
60
  def initialize(options = {})
61
+ @on_signal = []
61
62
  @formatter = Travis::Tools::Formatter.new
62
63
  self.output = $stdout
63
64
  self.input = $stdin
@@ -124,6 +125,7 @@ module Travis
124
125
  end
125
126
 
126
127
  def execute
128
+ setup_trap
127
129
  check_ruby
128
130
  check_arity(method(:run), *arguments)
129
131
  load_config
@@ -173,8 +175,21 @@ module Travis
173
175
  end
174
176
  end
175
177
 
178
+ def on_signal(&block)
179
+ @on_signal << block
180
+ end
181
+
176
182
  private
177
183
 
184
+ def setup_trap
185
+ [:INT, :TERM].each do |signal|
186
+ trap signal do
187
+ @on_signal.each { |c| c.call }
188
+ exit 1
189
+ end
190
+ end
191
+ end
192
+
178
193
  def format(data, format = nil, style = nil)
179
194
  style ||= :important
180
195
  data = format % color(data, style) if format and interactive?
@@ -20,7 +20,7 @@ module Travis
20
20
  error "--override without --add makes no sense" if override? and not add?
21
21
  self.override |= !config_key.start_with?('env.') if add? and not append?
22
22
 
23
- if args.first =~ %r{\w+/\w+}
23
+ if args.first =~ %r{\w+/\w+} && !args.first.include?("=")
24
24
  warn "WARNING: The name of the repository is now passed to the command with the -r option:"
25
25
  warn " #{command("encrypt [...] -r #{args.first}")}"
26
26
  warn " If you tried to pass the name of the repository as the first argument, you"
@@ -100,4 +100,3 @@ Please add the following to your <[[ color('.travis.yml', :info) ]]> file:
100
100
  %s
101
101
 
102
102
  Pro Tip: You can add it automatically by running with <[[ color('--add', :info) ]]>.
103
-
@@ -1,18 +1,14 @@
1
1
  require 'travis/cli'
2
+ require 'travis/tools/safe_string'
2
3
 
3
4
  module Travis
4
5
  module CLI
5
6
  class Logs < RepoCommand
7
+ include Tools::SafeString
6
8
  def run(number = last_build.number)
7
9
  error "##{number} is not a job, try #{number}.1" unless job = job(number)
8
- say log(job)
10
+ job.log.body { |part| print interactive? ? encoded(part) : clean(part) }
9
11
  end
10
-
11
- private
12
-
13
- def log(job)
14
- interactive? ? job.log.colorized_body : job.log.clean_body
15
- end
16
12
  end
17
13
  end
18
14
  end
@@ -0,0 +1,48 @@
1
+ require 'travis/cli'
2
+
3
+ module Travis
4
+ module CLI
5
+ class Monitor < ApiCommand
6
+ on('-m', '--my-repos', 'Only monitor my own repositories')
7
+ on('-r', '--repo SLUG', 'monitor given repository (can be used more than once)') do |c, slug|
8
+ c.repos << slug
9
+ end
10
+
11
+ attr_reader :repos
12
+
13
+ def initialize(*)
14
+ @repos = []
15
+ super
16
+ end
17
+
18
+ def setup
19
+ super
20
+ repos.map! { |r| repo(r) }
21
+ repos.concat(user.repositories) if my_repos?
22
+ end
23
+
24
+ def description
25
+ case repos.size
26
+ when 0 then session.config['host']
27
+ when 1 then repos.first.slug
28
+ else "#{repos.size} repositories"
29
+ end
30
+ end
31
+
32
+ def run
33
+ listen(*repos) do |listener|
34
+ listener.on_connect { say description, 'Monitoring %s:' }
35
+ listener.on 'build:started', 'job:started', 'build:finished', 'job:finished' do |event|
36
+ entity = event.job || event.build
37
+ time = entity.finished_at || entity.started_at
38
+ say [
39
+ color(formatter.time(time), entity.color),
40
+ color(entity.inspect_info, [entity.color, :bold]),
41
+ color(entity.state, entity.color)
42
+ ].join(" ")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,6 +4,7 @@ module Travis
4
4
  module CLI
5
5
  class Restart < RepoCommand
6
6
  def run(number = last_build.number)
7
+ authenticate
7
8
  entity = job(number) || build(number)
8
9
  entity.restart
9
10
 
@@ -33,6 +33,20 @@ module Travis
33
33
  end
34
34
  end
35
35
 
36
+ def setup_rubygems
37
+ configure 'deploy', 'provider' => 'rubygems' do |config|
38
+ rubygems_file = File.expand_path('.rubygems/authorization', ENV['HOME'])
39
+
40
+ if File.exist? rubygems_file
41
+ config['api_key'] = File.read(rubygems_file)
42
+ end
43
+
44
+ config['api_key'] ||= ask("RubyGems API token: ") { |q| q.echo = "*" }.to_s
45
+ config['on'] = { 'repo' => repository.slug } if agree("Deploy only from #{repository.slug}? ") { |q| q.default = 'yes' }
46
+ encrypt(config, 'api_key') if agree("Encrypt API key? ") { |q| q.default = 'yes' }
47
+ end
48
+ end
49
+
36
50
  def setup_nodejitsu
37
51
  configure 'deploy', 'provider' => 'nodejitsu' do |config|
38
52
  jitsu_file = File.expand_path('.jitsuconf', ENV['HOME'])
@@ -59,8 +73,8 @@ module Travis
59
73
  end
60
74
  end
61
75
 
62
- alias setup_sauce_lab setup_sauce_connect
63
- alias setup_sauce setup_sauce_connect
76
+ alias setup_sauce_labs setup_sauce_connect
77
+ alias setup_sauce setup_sauce_connect
64
78
 
65
79
  private
66
80
 
@@ -4,7 +4,7 @@ module Travis
4
4
  module CLI
5
5
  class Token < ApiCommand
6
6
  def run
7
- error "not logged in, please run #{command("login#{endpoint_option}")}" if access_token.nil?
7
+ authenticate
8
8
  say access_token, "Your access token is %s"
9
9
  end
10
10
  end
@@ -3,7 +3,7 @@ require 'travis/cli'
3
3
  module Travis
4
4
  module CLI
5
5
  class Whatsup < ApiCommand
6
- on('-m', '--my-repos')
6
+ on('-m', '--my-repos', 'Only display my own repositories')
7
7
 
8
8
  def run
9
9
  recent.each do |repo|
@@ -13,6 +13,8 @@ require 'travis/client/job'
13
13
  require 'travis/client/worker'
14
14
  require 'travis/client/namespace'
15
15
  require 'travis/client/account'
16
+ require 'travis/client/broadcast'
17
+ require 'travis/client/listener'
16
18
 
17
19
  module Travis
18
20
  module Client
@@ -3,12 +3,16 @@ require 'travis/client'
3
3
  module Travis
4
4
  module Client
5
5
  class Account < Entity
6
- attributes :name, :login, :type, :repos_count
6
+ attributes :name, :login, :type, :repos_count, :subscribed
7
7
 
8
8
  one :account
9
9
  many :accounts
10
10
 
11
11
  inspect_info :login
12
+
13
+ def subscribed
14
+ attributes.fetch('subscribed') { true }
15
+ end
12
16
  end
13
17
  end
14
18
  end
@@ -1,9 +1,12 @@
1
1
  # encoding: utf-8
2
2
  require 'travis/client'
3
+ require 'travis/tools/safe_string'
3
4
 
4
5
  module Travis
5
6
  module Client
6
7
  class Artifact < Entity
8
+ CHUNKED = "application/json; chunked=true; version=2, application/json; version=2"
9
+
7
10
  # @!parse attr_reader :job_id, :type, :body
8
11
  attributes :job_id, :type, :body
9
12
 
@@ -11,16 +14,49 @@ module Travis
11
14
  has :job
12
15
 
13
16
  def encoded_body
14
- return body unless body.respond_to? :encode
15
- body.encode 'utf-8'
17
+ Tools::SafeString.encoded(body)
16
18
  end
17
19
 
18
20
  def colorized_body
19
- attributes['colorized_body'] ||= encoded_body.gsub(/[^[:print:]\e\n]/, '')
21
+ attributes['colorized_body'] ||= Tools::SafeString.colorized(body)
20
22
  end
21
23
 
22
24
  def clean_body
23
- attributes['clean_body'] ||= colorized_body.gsub(/\e[^m]+m/, '')
25
+ attributes['clean_body'] ||= Tools::SafeString.clean(body)
26
+ end
27
+
28
+ def body(stream = block_given?)
29
+ return load_attribute('body') unless block_given? or stream
30
+ return yield(load_attribute('body')) unless stream and job.pending?
31
+ number = 0
32
+
33
+ session.listen(self) do |listener|
34
+ listener.on 'job:log' do |event|
35
+ next unless event.payload['number'] > number
36
+ number = event.payload['number']
37
+ yield event.payload['_log']
38
+ listener.disconnect if event.payload['final']
39
+ end
40
+
41
+ listener.on 'job:finished' do |event|
42
+ listener.disconnect
43
+ end
44
+
45
+ listener.on_connect do
46
+ data = session.get_raw("/artifacts/#{id}", nil, "Accept" => CHUNKED)['log']
47
+ if data['parts']
48
+ data['parts'].each { |p| yield p['content'] }
49
+ number = data['parts'].last['number'] if data['parts'].any?
50
+ else
51
+ yield data['body']
52
+ listener.disconnect
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def pusher_entity
59
+ job
24
60
  end
25
61
 
26
62
  one :log
@@ -0,0 +1,14 @@
1
+ require 'travis/client'
2
+
3
+ module Travis
4
+ module Client
5
+ class Broadcast < Entity
6
+ attributes :recipient_id, :recipient_type, :kind, :message, :expired, :created_at, :updated_at
7
+
8
+ one :broadcast
9
+ many :broadcasts
10
+
11
+ inspect_info :message
12
+ end
13
+ end
14
+ end
@@ -34,6 +34,10 @@ module Travis
34
34
  pull_request? ? "Pull Request ##{pr_number}" : commit.branch
35
35
  end
36
36
 
37
+ def pusher_channels
38
+ repository.pusher_channels
39
+ end
40
+
37
41
  def inspect_info
38
42
  "#{repository.slug}##{number}"
39
43
  end
@@ -21,8 +21,8 @@ module Travis
21
21
  MAP.fetch(key)
22
22
  end
23
23
 
24
- def self.aka(name)
25
- MAP[name.to_s] = self
24
+ def self.aka(*names)
25
+ names.each { |n| MAP[n.to_s] = self }
26
26
  end
27
27
 
28
28
  def self.one(key = nil)
@@ -48,6 +48,10 @@ module Travis
48
48
  end
49
49
  end
50
50
 
51
+ def pusher_channels
52
+ build.pusher_channels + ["job-#{id}"]
53
+ end
54
+
51
55
  def inspect_info
52
56
  "#{repository.slug}##{number}"
53
57
  end
@@ -0,0 +1,149 @@
1
+ require 'travis/client'
2
+ require 'forwardable'
3
+ require 'json'
4
+
5
+ if require 'pusher-client'
6
+ # it's us that has been loading pusher-client
7
+ # so let's assume we can mess with it - yay for global state
8
+ PusherClient.logger.level = 2
9
+ end
10
+
11
+ module Travis
12
+ module Client
13
+ class Listener
14
+ class Socket < PusherClient::Socket
15
+ attr_accessor :session, :signatures
16
+ def initialize(application_key, options = {})
17
+ @session = options.fetch(:session)
18
+ @signatures = {}
19
+ super
20
+ end
21
+
22
+ def subscribe_all
23
+ # bulk auth on connect
24
+ fetch_auth(*channels.channels.keys)
25
+ super
26
+ end
27
+
28
+ def fetch_auth(*channels)
29
+ channels.select! { |c| signatures[c].nil? if c.start_with? 'private-' }
30
+ signatures.merge! session.post_raw('/pusher/auth', :channels => channels, :socket_id => socket_id)['channels'] if channels.any?
31
+ end
32
+
33
+ def get_private_auth(channel)
34
+ fetch_auth(channel.name)
35
+ signatures[channel.name]
36
+ end
37
+ end
38
+
39
+ EVENTS = %w[
40
+ build:created build:started build:finished
41
+ job:created job:started job:log job:finished
42
+ ]
43
+
44
+ Event = Struct.new(:type, :repository, :build, :job, :payload)
45
+
46
+ class EntityListener
47
+ attr_reader :listener, :entities
48
+
49
+ extend Forwardable
50
+ def_delegators :listener, :disconnect, :on_connect, :subscribe
51
+
52
+ def initialize(listener, entities)
53
+ @listener, @entities = listener, Array(entities)
54
+ end
55
+
56
+ def on(*events)
57
+ listener.on(*events) { |e| yield(e) if dispatch?(e) }
58
+ end
59
+
60
+ private
61
+
62
+ def dispatch?(event)
63
+ entities.include? event.repository or
64
+ entities.include? event.build or
65
+ entities.include? event.job
66
+ end
67
+ end
68
+
69
+ attr_reader :session, :socket
70
+
71
+ def initialize(session)
72
+ @session = session
73
+ @socket = Socket.new(pusher_key, :encrypted => true, :session => session)
74
+ @channels = []
75
+ @callbacks = []
76
+ end
77
+
78
+ def subscribe(*entities)
79
+ entities = entities.map do |entity|
80
+ entity = entity.pusher_entity while entity.respond_to? :pusher_entity
81
+ @channels.concat(entity.pusher_channels)
82
+ entity
83
+ end
84
+
85
+ yield entities.any? ? EntityListener.new(self, entities) : self if block_given?
86
+ end
87
+
88
+ def on(*events, &block)
89
+ events = events.flat_map { |e| e.respond_to?(:to_str) ? e.to_str : EVENTS.grep(e) }.uniq
90
+ events.each { |e| @callbacks << [e, block] }
91
+ end
92
+
93
+ def on_connect
94
+ socket.bind('pusher:connection_established') { yield }
95
+ end
96
+
97
+ def listen
98
+ @channels = default_channels if @channels.empty?
99
+ @channels.map! { |c| c.start_with?('private-') ? c : "private-#{c}" } if session.private_channels?
100
+ @channels.uniq.each { |c| socket.subscribe(c) }
101
+ @callbacks.each { |e,b| socket.bind(e) { |d| dispatch(e, d, &b) } }
102
+ socket.connect
103
+ end
104
+
105
+ def disconnect
106
+ socket.disconnect
107
+ end
108
+
109
+ private
110
+
111
+ def dispatch(type, json)
112
+ payload = JSON.parse(json)
113
+ entities = session.load format_payload(type, payload)
114
+ yield Event.new(type, entities['repository'], entities['build'], entities['job'], payload)
115
+ end
116
+
117
+ def format_payload(type, payload)
118
+ case type
119
+ when "job:log" then format_log(payload)
120
+ when /job:/ then format_job(payload)
121
+ else payload
122
+ end
123
+ end
124
+
125
+ def format_job(payload)
126
+ build = { "id" => payload["build_id"], "repository_id" => payload["repository_id"] }
127
+ repo = { "id" => payload["repository_id"], "slug" => payload["repository_slug"] }
128
+ build["number"] = payload["number"][/^[^\.]+/] if payload["number"]
129
+ { "job" => payload, "build" => build, "repository" => repo }
130
+ end
131
+
132
+ def format_log(payload)
133
+ job = session.job(payload['id'])
134
+ { "job" => { "id" => job.id }, "build" => { "id" => job.build.id }, "repository" => { "id" => job.repository.id } }
135
+ end
136
+
137
+ def default_channels
138
+ return ['common'] if session.access_token.nil?
139
+ session.user.channels
140
+ end
141
+
142
+ def pusher_key
143
+ session.config.fetch('pusher').fetch('key')
144
+ rescue IndexError
145
+ raise Travis::Client::Error, "#{session.api_endpoint} is missing pusher key"
146
+ end
147
+ end
148
+ end
149
+ end